Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
+184
View File
@@ -0,0 +1,184 @@
// Package api is for building api gateways
package api
import (
"context"
"errors"
"regexp"
"strings"
"go-micro.dev/v4/api/router"
"go-micro.dev/v4/client"
"go-micro.dev/v4/registry"
"go-micro.dev/v4/server"
)
// API interface provides a way to
// create composable API gateways.
type Api interface {
// Initialize options
Init(...Option) error
// Get the options
Options() Options
// Register an endpoint
Register(*Endpoint) error
// Deregister an endpoint
Deregister(*Endpoint) error
// Run the api
Run(context.Context) error
// Implemenation of api e.g http
String() string
}
// Options are API options.
type Options struct {
// Router for resolving routes
Router router.Router
// Client to use for RPC
Client client.Client
// Address of the server
Address string
}
// Option type are API option args.
type Option func(*Options) error
// Endpoint is a mapping between an RPC method and HTTP endpoint.
type Endpoint struct {
// RPC Method e.g. Greeter.Hello
Name string
// Description e.g what's this endpoint for
Description string
// API Handler e.g rpc, proxy
Handler string
// HTTP Host e.g example.com
Host []string
// HTTP Methods e.g GET, POST
Method []string
// HTTP Path e.g /greeter. Expect POSIX regex
Path []string
// Stream flag
Stream bool
}
// Service represents an API service.
type Service struct {
// Name of service
Name string
// The endpoint for this service
Endpoint *Endpoint
// Versions of this service
Versions []*registry.Service
}
func strip(s string) string {
return strings.TrimSpace(s)
}
func slice(s string) []string {
var sl []string
for _, p := range strings.Split(s, ",") {
if str := strip(p); len(str) > 0 {
sl = append(sl, strip(p))
}
}
return sl
}
// Encode encodes an endpoint to endpoint metadata.
func Encode(e *Endpoint) map[string]string {
if e == nil {
return nil
}
// endpoint map
em := make(map[string]string)
// set vals only if they exist
set := func(k, v string) {
if len(v) == 0 {
return
}
em[k] = v
}
set("endpoint", e.Name)
set("description", e.Description)
set("handler", e.Handler)
set("method", strings.Join(e.Method, ","))
set("path", strings.Join(e.Path, ","))
set("host", strings.Join(e.Host, ","))
return em
}
// Decode decodes endpoint metadata into an endpoint.
func Decode(e map[string]string) *Endpoint {
if e == nil {
return nil
}
return &Endpoint{
Name: e["endpoint"],
Description: e["description"],
Method: slice(e["method"]),
Path: slice(e["path"]),
Host: slice(e["host"]),
Handler: e["handler"],
}
}
// Validate validates an endpoint to guarantee it won't blow up when being served.
func Validate(e *Endpoint) error {
if e == nil {
return errors.New("endpoint is nil")
}
if len(e.Name) == 0 {
return errors.New("name required")
}
for _, p := range e.Path {
ps := p[0]
pe := p[len(p)-1]
if ps == '^' && pe == '$' {
_, err := regexp.CompilePOSIX(p)
if err != nil {
return err
}
} else if ps == '^' && pe != '$' {
return errors.New("invalid path")
} else if ps != '^' && pe == '$' {
return errors.New("invalid path")
}
}
if len(e.Handler) == 0 {
return errors.New("invalid handler")
}
return nil
}
// WithEndpoint returns a server.HandlerOption with endpoint metadata set
//
// Usage:
//
// proto.RegisterHandler(service.Server(), new(Handler), api.WithEndpoint(
// &api.Endpoint{
// Name: "Greeter.Hello",
// Path: []string{"/greeter"},
// },
// ))
func WithEndpoint(e *Endpoint) server.HandlerOption {
return server.EndpointMetadata(e.Name, Encode(e))
}
// NewApi returns a new api gateway.
func NewApi(opts ...Option) Api {
return newApi(opts...)
}
+87
View File
@@ -0,0 +1,87 @@
package api
import (
"context"
"go-micro.dev/v4/api/handler"
"go-micro.dev/v4/api/handler/rpc"
"go-micro.dev/v4/api/router/registry"
"go-micro.dev/v4/api/server"
"go-micro.dev/v4/api/server/http"
)
type api struct {
options Options
server server.Server
}
func newApi(opts ...Option) Api {
options := NewOptions(opts...)
rtr := options.Router
if rtr == nil {
// TODO: make configurable
rtr = registry.NewRouter()
}
// TODO: make configurable
hdlr := rpc.NewHandler(
handler.WithRouter(rtr),
)
// TODO: make configurable
// create a new server
srv := http.NewServer(options.Address)
// TODO: allow multiple handlers
// define the handler
srv.Handle("/", hdlr)
return &api{
options: options,
server: srv,
}
}
func (a *api) Init(opts ...Option) error {
for _, o := range opts {
o(&a.options)
}
return nil
}
// Get the options.
func (a *api) Options() Options {
return a.options
}
// Register a http handler.
func (a *api) Register(*Endpoint) error {
return nil
}
// Register a route.
func (a *api) Deregister(*Endpoint) error {
return nil
}
func (a *api) Run(ctx context.Context) error {
if err := a.server.Start(); err != nil {
return err
}
// wait to finish
<-ctx.Done()
if err := a.server.Stop(); err != nil {
return err
}
return nil
}
func (a *api) String() string {
return "http"
}
+14
View File
@@ -0,0 +1,14 @@
// Package handler provides http handlers
package handler
import (
"net/http"
)
// Handler represents a HTTP handler that manages a request.
type Handler interface {
// standard http handler
http.Handler
// name of handler
String() string
}
+84
View File
@@ -0,0 +1,84 @@
package handler
import (
"go-micro.dev/v4/api/router"
"go-micro.dev/v4/client"
"go-micro.dev/v4/logger"
)
var (
// DefaultMaxRecvSize is 10MiB.
DefaultMaxRecvSize int64 = 1024 * 1024 * 100
)
// Options is the list of api Options.
type Options struct {
Router router.Router
Client client.Client
Logger logger.Logger
Namespace string
MaxRecvSize int64
}
// Option is a api Option.
type Option func(o *Options)
// NewOptions fills in the blanks.
func NewOptions(opts ...Option) Options {
options := Options{
Logger: logger.DefaultLogger,
}
for _, o := range opts {
o(&options)
}
if options.Client == nil {
WithClient(client.DefaultClient)(&options)
}
if options.MaxRecvSize == 0 {
options.MaxRecvSize = DefaultMaxRecvSize
}
if options.Logger == nil {
options.Logger = logger.LoggerOrDefault(options.Logger)
}
return options
}
// WithNamespace specifies the namespace for the handler.
func WithNamespace(s string) Option {
return func(o *Options) {
o.Namespace = s
}
}
// WithRouter specifies a router to be used by the handler.
func WithRouter(r router.Router) Option {
return func(o *Options) {
o.Router = r
}
}
// WithClient sets the client for the handler.
func WithClient(c client.Client) Option {
return func(o *Options) {
o.Client = c
}
}
// WithMaxRecvSize specifies max body size.
func WithMaxRecvSize(size int64) Option {
return func(o *Options) {
o.MaxRecvSize = size
}
}
// WithLogger specifies the logger.
func WithLogger(l logger.Logger) Option {
return func(o *Options) {
o.Logger = l
}
}
+564
View File
@@ -0,0 +1,564 @@
// Package rpc is a go-micro rpc handler.
package rpc
import (
"encoding/json"
"io"
"net/http"
"net/textproto"
"strconv"
"strings"
jsonpatch "github.com/evanphx/json-patch/v5"
"github.com/oxtoacart/bpool"
"go-micro.dev/v4/api/handler"
"go-micro.dev/v4/api/internal/proto"
"go-micro.dev/v4/api/router"
"go-micro.dev/v4/client"
"go-micro.dev/v4/codec"
"go-micro.dev/v4/codec/jsonrpc"
"go-micro.dev/v4/codec/protorpc"
"go-micro.dev/v4/errors"
log "go-micro.dev/v4/logger"
"go-micro.dev/v4/metadata"
"go-micro.dev/v4/registry"
"go-micro.dev/v4/selector"
"go-micro.dev/v4/util/ctx"
"go-micro.dev/v4/util/qson"
)
const (
// Handler is the name of this handler.
Handler = "rpc"
packageID = "go.micro.api"
)
var (
// supported json codecs.
jsonCodecs = []string{
"application/grpc+json",
"application/json",
"application/json-rpc",
}
// support proto codecs.
protoCodecs = []string{
"application/grpc",
"application/grpc+proto",
"application/proto",
"application/protobuf",
"application/proto-rpc",
"application/octet-stream",
}
bufferPool = bpool.NewSizedBufferPool(1024, 8)
)
type rpcHandler struct {
opts handler.Options
}
type buffer struct {
io.ReadCloser
}
func (b *buffer) Write(_ []byte) (int, error) {
return 0, nil
}
// strategy is a hack for selection.
func strategy(services []*registry.Service) selector.Strategy {
return func(_ []*registry.Service) selector.Next {
// ignore input to this function, use services above
return selector.Random(services)
}
}
func (h *rpcHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
logger := h.opts.Logger
bsize := handler.DefaultMaxRecvSize
if h.opts.MaxRecvSize > 0 {
bsize = h.opts.MaxRecvSize
}
r.Body = http.MaxBytesReader(w, r.Body, bsize)
defer r.Body.Close()
var service *router.Route
if h.opts.Router != nil {
// try get service from router
s, err := h.opts.Router.Route(r)
if err != nil {
werr := writeError(w, r, errors.InternalServerError(packageID, err.Error()))
if werr != nil {
logger.Log(log.ErrorLevel, werr)
}
return
}
service = s
} else {
// we have no way of routing the request
werr := writeError(w, r, errors.InternalServerError(packageID, "no route found"))
if werr != nil {
logger.Log(log.ErrorLevel, werr)
}
return
}
contentType := r.Header.Get("Content-Type")
// Strip charset from Content-Type (like `application/json; charset=UTF-8`)
if idx := strings.IndexRune(contentType, ';'); idx >= 0 {
contentType = contentType[:idx]
}
// micro client
myClient := h.opts.Client
// create context
myContext := ctx.FromRequest(r)
// get context from http handler wrappers
md, ok := metadata.FromContext(r.Context())
if !ok {
md = make(metadata.Metadata)
}
// fill contex with http headers
md["Host"] = r.Host
md["Method"] = r.Method
// get canonical headers
for k := range r.Header {
// may be need to get all values for key like r.Header.Values() provide in go 1.14
md[textproto.CanonicalMIMEHeaderKey(k)] = r.Header.Get(k)
}
// merge context with overwrite
myContext = metadata.MergeContext(myContext, md, true)
// set merged context to request
*r = *r.Clone(myContext)
// if stream we currently only support json
if isStream(r, service) {
// drop older context as it can have timeouts and create new
// md, _ := metadata.FromContext(cx)
// serveWebsocket(context.TODO(), w, r, service, c)
if err := serveWebsocket(myContext, w, r, service, myClient); err != nil {
logger.Log(log.ErrorLevel, err)
}
return
}
// create strategy
mySelector := selector.WithStrategy(strategy(service.Versions))
// walk the standard call path
// get payload
br, err := requestPayload(r)
if err != nil {
if werr := writeError(w, r, err); werr != nil {
logger.Log(log.ErrorLevel, werr)
}
return
}
var rsp []byte
switch {
// proto codecs
case hasCodec(contentType, protoCodecs):
request := &proto.Message{}
// if the extracted payload isn't empty lets use it
if len(br) > 0 {
request = proto.NewMessage(br)
}
// create request/response
response := &proto.Message{}
req := myClient.NewRequest(
service.Service,
service.Endpoint.Name,
request,
client.WithContentType(contentType),
)
// make the call
if err := myClient.Call(myContext, req, response, client.WithSelectOption(mySelector)); err != nil {
if werr := writeError(w, r, err); werr != nil {
logger.Log(log.ErrorLevel, werr)
}
return
}
// marshall response
rsp, err = response.Marshal()
if err != nil {
if werr := writeError(w, r, err); werr != nil {
logger.Log(log.ErrorLevel, werr)
}
return
}
default:
// if json codec is not present set to json
if !hasCodec(contentType, jsonCodecs) {
contentType = "application/json"
}
// default to trying json
var request json.RawMessage
// if the extracted payload isn't empty lets use it
if len(br) > 0 {
request = json.RawMessage(br)
}
// create request/response
var response json.RawMessage
req := myClient.NewRequest(
service.Service,
service.Endpoint.Name,
&request,
client.WithContentType(contentType),
)
// make the call
if err := myClient.Call(myContext, req, &response, client.WithSelectOption(mySelector)); err != nil {
if werr := writeError(w, r, err); werr != nil {
logger.Log(log.ErrorLevel, werr)
}
return
}
// marshall response
rsp, err = response.MarshalJSON()
if err != nil {
if werr := writeError(w, r, err); werr != nil {
logger.Log(log.ErrorLevel, werr)
}
return
}
}
// write the response
if err := writeResponse(w, r, rsp); err != nil {
logger.Log(log.ErrorLevel, err)
}
}
func (h *rpcHandler) String() string {
return Handler
}
func hasCodec(ct string, codecs []string) bool {
for _, codec := range codecs {
if ct == codec {
return true
}
}
return false
}
// requestPayload takes a *http.Request.
// If the request is a GET the query string parameters are extracted and marshaled to JSON and the raw bytes are returned.
// If the request method is a POST the request body is read and returned.
func requestPayload(r *http.Request) ([]byte, error) {
var err error
// we have to decode json-rpc and proto-rpc because we suck
// well actually because there's no proxy codec right now
myCt := r.Header.Get("Content-Type")
switch {
case strings.Contains(myCt, "application/json-rpc"):
msg := codec.Message{
Type: codec.Request,
Header: make(map[string]string),
}
c := jsonrpc.NewCodec(&buffer{r.Body})
if err = c.ReadHeader(&msg, codec.Request); err != nil {
return nil, err
}
var raw json.RawMessage
if err = c.ReadBody(&raw); err != nil {
return nil, err
}
return ([]byte)(raw), nil
case strings.Contains(myCt, "application/proto-rpc"), strings.Contains(myCt, "application/octet-stream"):
msg := codec.Message{
Type: codec.Request,
Header: make(map[string]string),
}
c := protorpc.NewCodec(&buffer{r.Body})
if err = c.ReadHeader(&msg, codec.Request); err != nil {
return nil, err
}
var raw proto.Message
if err = c.ReadBody(&raw); err != nil {
return nil, err
}
return raw.Marshal()
case strings.Contains(myCt, "application/www-x-form-urlencoded"), strings.Contains(myCt, "application/x-www-form-urlencoded"):
if err := r.ParseForm(); err != nil {
return nil, err
}
// generate a new set of values from the form
vals := make(map[string]string)
for k, v := range r.Form {
vals[k] = strings.Join(v, ",")
}
// marshal
return json.Marshal(vals)
// TODO: application/grpc
}
// otherwise as per usual
ctx := r.Context()
// dont user meadata.FromContext as it mangles names
md, ok := metadata.FromContext(ctx)
if !ok {
md = make(map[string]string)
}
// allocate maximum
matches := make(map[string]interface{}, len(md))
bodydst := ""
// get fields from url path
for k, v := range md {
k = strings.ToLower(k)
// filter own keys
if strings.HasPrefix(k, "x-api-field-") {
matches[strings.TrimPrefix(k, "x-api-field-")] = v
delete(md, k)
} else if k == "x-api-body" {
bodydst = v
delete(md, k)
}
}
// map of all fields
req := make(map[string]interface{}, len(md))
// get fields from url values
if len(r.URL.RawQuery) > 0 {
umd := make(map[string]interface{})
err = qson.Unmarshal(&umd, r.URL.RawQuery)
if err != nil {
return nil, err
}
for k, v := range umd {
matches[k] = v
}
}
// restore context without fields
*r = *r.Clone(metadata.NewContext(ctx, md))
for k, v := range matches {
ps := strings.Split(k, ".")
if len(ps) == 1 {
req[k] = v
continue
}
em := make(map[string]interface{})
em[ps[len(ps)-1]] = v
for i := len(ps) - 2; i > 0; i-- {
nm := make(map[string]interface{})
nm[ps[i]] = em
em = nm
}
if vm, ok := req[ps[0]]; ok {
// nested map
nm := vm.(map[string]interface{})
for vk, vv := range em {
nm[vk] = vv
}
req[ps[0]] = nm
} else {
req[ps[0]] = em
}
}
pathbuf := []byte("{}")
if len(req) > 0 {
pathbuf, err = json.Marshal(req)
if err != nil {
return nil, err
}
}
urlbuf := []byte("{}")
out, err := jsonpatch.MergeMergePatches(urlbuf, pathbuf)
if err != nil {
return nil, err
}
switch r.Method {
case http.MethodGet:
// empty response
if strings.Contains(myCt, "application/json") && string(out) == "{}" {
return out, nil
} else if string(out) == "{}" && !strings.Contains(myCt, "application/json") {
return []byte{}, nil
}
return out, nil
case http.MethodPatch, http.MethodPost, http.MethodPut, http.MethodDelete:
bodybuf := []byte("{}")
buf := bufferPool.Get()
defer bufferPool.Put(buf)
if _, err := buf.ReadFrom(r.Body); err != nil {
return nil, err
}
if b := buf.Bytes(); len(b) > 0 {
bodybuf = b
}
if bodydst == "" || bodydst == "*" {
if out, err = jsonpatch.MergeMergePatches(out, bodybuf); err == nil {
return out, nil
}
}
var jsonbody map[string]interface{}
if json.Valid(bodybuf) {
if err = json.Unmarshal(bodybuf, &jsonbody); err != nil {
return nil, err
}
}
dstmap := make(map[string]interface{})
ps := strings.Split(bodydst, ".")
if len(ps) == 1 {
if jsonbody != nil {
dstmap[ps[0]] = jsonbody
} else {
// old unexpected behavior
dstmap[ps[0]] = bodybuf
}
} else {
em := make(map[string]interface{})
if jsonbody != nil {
em[ps[len(ps)-1]] = jsonbody
} else {
// old unexpected behavior
em[ps[len(ps)-1]] = bodybuf
}
for i := len(ps) - 2; i > 0; i-- {
nm := make(map[string]interface{})
nm[ps[i]] = em
em = nm
}
dstmap[ps[0]] = em
}
bodyout, err := json.Marshal(dstmap)
if err != nil {
return nil, err
}
if out, err = jsonpatch.MergeMergePatches(out, bodyout); err == nil {
return out, nil
}
return bodybuf, nil
}
return []byte{}, nil
}
func writeError(rsp http.ResponseWriter, req *http.Request, err error) error {
ce := errors.Parse(err.Error())
switch ce.Code {
case 0:
// assuming it's totally screwed
ce.Code = http.StatusInternalServerError
ce.Id = packageID
ce.Status = http.StatusText(http.StatusInternalServerError)
ce.Detail = "error during request: " + ce.Detail
rsp.WriteHeader(http.StatusInternalServerError)
default:
rsp.WriteHeader(int(ce.Code))
}
// response content type
rsp.Header().Set("Content-Type", "application/json")
// Set trailers
if strings.Contains(req.Header.Get("Content-Type"), "application/grpc") {
rsp.Header().Set("Trailer", "grpc-status")
rsp.Header().Set("Trailer", "grpc-message")
rsp.Header().Set("grpc-status", "13")
rsp.Header().Set("grpc-message", ce.Detail)
}
_, werr := rsp.Write([]byte(ce.Error()))
return werr
}
func writeResponse(w http.ResponseWriter, r *http.Request, rsp []byte) error {
w.Header().Set("Content-Type", r.Header.Get("Content-Type"))
w.Header().Set("Content-Length", strconv.Itoa(len(rsp)))
// Set trailers
if strings.Contains(r.Header.Get("Content-Type"), "application/grpc") {
w.Header().Set("Trailer", "grpc-status")
w.Header().Set("Trailer", "grpc-message")
w.Header().Set("grpc-status", "0")
w.Header().Set("grpc-message", "")
}
// write 204 status if rsp is nil
if len(rsp) == 0 {
w.WriteHeader(http.StatusNoContent)
}
// write response
_, err := w.Write(rsp)
return err
}
// NewHandler returns a new RPC handler.
func NewHandler(opts ...handler.Option) handler.Handler {
options := handler.NewOptions(opts...)
return &rpcHandler{
opts: options,
}
}
+245
View File
@@ -0,0 +1,245 @@
package rpc
import (
"bytes"
"context"
"encoding/json"
"io"
"net/http"
"strings"
"time"
"github.com/gobwas/httphead"
"github.com/gobwas/ws"
"github.com/gobwas/ws/wsutil"
"go-micro.dev/v4/api/router"
"go-micro.dev/v4/client"
raw "go-micro.dev/v4/codec/bytes"
"go-micro.dev/v4/selector"
)
// serveWebsocket will stream rpc back over websockets assuming json.
func serveWebsocket(ctx context.Context, w http.ResponseWriter, r *http.Request, service *router.Route, c client.Client) (err error) {
var opCode ws.OpCode
myCt := r.Header.Get("Content-Type")
// Strip charset from Content-Type (like `application/json; charset=UTF-8`)
if idx := strings.IndexRune(myCt, ';'); idx >= 0 {
myCt = myCt[:idx]
}
// check proto from request
switch myCt {
case "application/json":
opCode = ws.OpText
default:
opCode = ws.OpBinary
}
hdr := make(http.Header)
if proto, ok := r.Header["Sec-Websocket-Protocol"]; ok {
for _, p := range proto {
if p == "binary" {
hdr["Sec-WebSocket-Protocol"] = []string{"binary"}
opCode = ws.OpBinary
}
}
}
payload, err := requestPayload(r)
if err != nil {
return
}
upgrader := ws.HTTPUpgrader{Timeout: 5 * time.Second,
Protocol: func(proto string) bool {
if strings.Contains(proto, "binary") {
return true
}
// fallback to support all protocols now
return true
},
Extension: func(httphead.Option) bool {
// disable extensions for compatibility
return false
},
Header: hdr,
}
conn, uRw, _, err := upgrader.Upgrade(r, w)
if err != nil {
return
}
defer func() {
if cErr := conn.Close(); cErr != nil && err == nil {
err = cErr
}
}()
var request interface{}
if !bytes.Equal(payload, []byte(`{}`)) {
switch myCt {
case "application/json", "":
m := json.RawMessage(payload)
request = &m
default:
request = &raw.Frame{Data: payload}
}
}
// we always need to set content type for message
if myCt == "" {
myCt = "application/json"
}
req := c.NewRequest(
service.Service,
service.Endpoint.Name,
request,
client.WithContentType(myCt),
client.StreamingRequest(),
)
cCtx, cancel := context.WithCancel(ctx)
defer cancel()
so := selector.WithStrategy(strategy(service.Versions))
// create a new stream
stream, err := c.Stream(cCtx, req, client.WithSelectOption(so))
if err != nil {
return
}
if request != nil {
if err = stream.Send(request); err != nil {
return
}
}
go func() {
if wErr := writeLoop(uRw, stream); wErr != nil && err == nil {
err = wErr
}
}()
rsp := stream.Response()
// receive from stream and send to client
for {
select {
case <-ctx.Done():
return nil
case <-stream.Context().Done():
return nil
default:
// read backend response body
buf, err := rsp.Read()
if err != nil {
// wants to avoid import grpc/status.Status
if strings.Contains(err.Error(), "context canceled") {
return nil
}
return err
}
// write the response
if err = wsutil.WriteServerMessage(uRw, opCode, buf); err != nil {
return err
}
if err = uRw.Flush(); err != nil {
return err
}
}
}
}
// writeLoop.
func writeLoop(rw io.ReadWriter, stream client.Stream) error {
// close stream when done
defer stream.Close()
for {
select {
case <-stream.Context().Done():
return nil
default:
buf, op, err := wsutil.ReadClientData(rw)
if err != nil {
if wserr, ok := err.(wsutil.ClosedError); ok {
switch wserr.Code {
case ws.StatusGoingAway:
// this happens when user leave the page
return nil
case ws.StatusNormalClosure, ws.StatusNoStatusRcvd:
// this happens when user close ws connection, or we don't get any status
return nil
default:
return err
}
}
}
switch op {
default:
// not relevant
continue
case ws.OpText, ws.OpBinary:
break
}
// send to backend
// default to trying json
// if the extracted payload isn't empty lets use it
request := &raw.Frame{Data: buf}
if err := stream.Send(request); err != nil {
return err
}
}
}
}
func isStream(r *http.Request, srv *router.Route) bool {
// check if it's a web socket
if !isWebSocket(r) {
return false
}
// check if the endpoint supports streaming
for _, service := range srv.Versions {
for _, ep := range service.Endpoints {
// skip if it doesn't match the name
if ep.Name != srv.Endpoint.Name {
continue
}
// matched if the name
if v := ep.Metadata["stream"]; v == "true" {
return true
}
}
}
return false
}
func isWebSocket(r *http.Request) bool {
contains := func(key, val string) bool {
vv := strings.Split(r.Header.Get(key), ",")
for _, v := range vv {
if val == strings.ToLower(strings.TrimSpace(v)) {
return true
}
}
return false
}
if contains("Connection", "upgrade") && contains("Upgrade", "websocket") {
return true
}
return false
}
@@ -0,0 +1,28 @@
package proto
type Message struct {
data []byte
}
func (m *Message) ProtoMessage() {}
func (m *Message) Reset() {
*m = Message{}
}
func (m *Message) String() string {
return string(m.data)
}
func (m *Message) Marshal() ([]byte, error) {
return m.data, nil
}
func (m *Message) Unmarshal(data []byte) error {
m.data = data
return nil
}
func NewMessage(data []byte) *Message {
return &Message{data}
}
+46
View File
@@ -0,0 +1,46 @@
package api
import (
"go-micro.dev/v4/api/router"
registry2 "go-micro.dev/v4/api/router/registry"
"go-micro.dev/v4/client"
"go-micro.dev/v4/registry"
)
func NewOptions(opts ...Option) Options {
options := Options{
Address: ":8080",
}
for _, o := range opts {
o(&options)
}
return options
}
// WithAddress sets the address to listen
func WithAddress(addr string) Option {
return func(o *Options) error {
o.Address = addr
return nil
}
}
// WithRouter sets the router to use e.g static or registry.
func WithRouter(r router.Router) Option {
return func(o *Options) error {
o.Router = r
return nil
}
}
// WithRegistry sets the api's client and router to use registry.
func WithRegistry(r registry.Registry) Option {
return func(o *Options) error {
o.Client = client.NewClient(client.Registry(r))
o.Router = registry2.NewRouter(router.WithRegistry(r))
return nil
}
}
+33
View File
@@ -0,0 +1,33 @@
package resolver
import (
"net/http"
)
// NewOptions wires options together.
func NewOptions(opts ...Option) Options {
var options Options
for _, o := range opts {
o(&options)
}
if options.Namespace == nil {
options.Namespace = StaticNamespace("go.micro")
}
return options
}
// WithHandler sets the handler being used.
func WithHandler(h string) Option {
return func(o *Options) {
o.Handler = h
}
}
// WithNamespace sets the function which determines the namespace for a request.
func WithNamespace(n func(*http.Request) string) Option {
return func(o *Options) {
o.Namespace = n
}
}
+46
View File
@@ -0,0 +1,46 @@
// Package resolver resolves a http request to an endpoint
package resolver
import (
"errors"
"net/http"
)
var (
ErrNotFound = errors.New("not found")
ErrInvalidPath = errors.New("invalid path")
)
// Resolver resolves requests to endpoints.
type Resolver interface {
Resolve(r *http.Request) (*Endpoint, error)
String() string
}
// Endpoint is the endpoint for a http request.
type Endpoint struct {
// e.g greeter
Name string
// HTTP Host e.g example.com
Host string
// HTTP Methods e.g GET, POST
Method string
// HTTP Path e.g /greeter.
Path string
}
// Options is a struct of available options.
type Options struct {
Namespace func(*http.Request) string
Handler string
}
// Option is a helper for a single option.
type Option func(o *Options)
// StaticNamespace returns the same namespace for each request.
func StaticNamespace(ns string) func(*http.Request) string {
return func(*http.Request) string {
return ns
}
}
@@ -0,0 +1,72 @@
// Package vpath resolves using http path and recognized versioned urls
package vpath
import (
"errors"
"net/http"
"regexp"
"strings"
"go-micro.dev/v4/api/resolver"
)
// NewResolver returns a new vpath resolver.
func NewResolver(opts ...resolver.Option) resolver.Resolver {
return &Resolver{opts: resolver.NewOptions(opts...)}
}
// Resolver is a vpath resolver.
type Resolver struct {
opts resolver.Options
}
var (
re = regexp.MustCompile("^v[0-9]+$")
)
// Resolve resolves a http.Request to an grpc Endpoint.
func (r *Resolver) Resolve(req *http.Request) (*resolver.Endpoint, error) {
if req.URL.Path == "/" {
return nil, errors.New("unknown name")
}
parts := strings.Split(req.URL.Path[1:], "/")
if len(parts) == 1 {
return &resolver.Endpoint{
Name: r.withNamespace(req, parts...),
Host: req.Host,
Method: req.Method,
Path: req.URL.Path,
}, nil
}
// /v1/foo
if re.MatchString(parts[0]) {
return &resolver.Endpoint{
Name: r.withNamespace(req, parts[0:2]...),
Host: req.Host,
Method: req.Method,
Path: req.URL.Path,
}, nil
}
return &resolver.Endpoint{
Name: r.withNamespace(req, parts[0]),
Host: req.Host,
Method: req.Method,
Path: req.URL.Path,
}, nil
}
func (r *Resolver) String() string {
return "path"
}
func (r *Resolver) withNamespace(req *http.Request, parts ...string) string {
ns := r.opts.Namespace(req)
if len(ns) == 0 {
return strings.Join(parts, ".")
}
return strings.Join(append([]string{ns}, parts...), ".")
}
+99
View File
@@ -0,0 +1,99 @@
package router
import (
"errors"
"regexp"
"strings"
)
func strip(s string) string {
return strings.TrimSpace(s)
}
func slice(s string) []string {
var sl []string
for _, p := range strings.Split(s, ",") {
if str := strip(p); len(str) > 0 {
sl = append(sl, strip(p))
}
}
return sl
}
// Encode encodes an endpoint to endpoint metadata.
func Encode(e *Endpoint) map[string]string {
if e == nil {
return nil
}
// endpoint map
ep := make(map[string]string)
// set vals only if they exist
set := func(k, v string) {
if len(v) == 0 {
return
}
ep[k] = v
}
set("endpoint", e.Name)
set("description", e.Description)
set("handler", e.Handler)
set("method", strings.Join(e.Method, ","))
set("path", strings.Join(e.Path, ","))
set("host", strings.Join(e.Host, ","))
return ep
}
// Decode decodes endpoint metadata into an endpoint.
func Decode(e map[string]string) *Endpoint {
if e == nil {
return nil
}
return &Endpoint{
Name: e["endpoint"],
Description: e["description"],
Method: slice(e["method"]),
Path: slice(e["path"]),
Host: slice(e["host"]),
Handler: e["handler"],
}
}
// Validate validates an endpoint to guarantee it won't blow up when being served.
func Validate(e *Endpoint) error {
if e == nil {
return errors.New("endpoint is nil")
}
if len(e.Name) == 0 {
return errors.New("name required")
}
for _, p := range e.Path {
ps := p[0]
pe := p[len(p)-1]
if ps == '^' && pe == '$' {
_, err := regexp.CompilePOSIX(p)
if err != nil {
return err
}
} else if ps == '^' && pe != '$' {
return errors.New("invalid path")
} else if ps != '^' && pe == '$' {
return errors.New("invalid path")
}
}
if len(e.Handler) == 0 {
return errors.New("invalid handler")
}
return nil
}
+65
View File
@@ -0,0 +1,65 @@
package router
import (
"go-micro.dev/v4/api/resolver"
"go-micro.dev/v4/api/resolver/vpath"
"go-micro.dev/v4/logger"
"go-micro.dev/v4/registry"
)
// Options is a struct of options available.
type Options struct {
Registry registry.Registry
Resolver resolver.Resolver
Logger logger.Logger
Handler string
}
// Option is a helper for a single options.
type Option func(o *Options)
// NewOptions wires options together.
func NewOptions(opts ...Option) Options {
options := Options{
Handler: "meta",
Registry: registry.DefaultRegistry,
Logger: logger.DefaultLogger,
}
for _, o := range opts {
o(&options)
}
if options.Resolver == nil {
options.Resolver = vpath.NewResolver(
resolver.WithHandler(options.Handler),
)
}
return options
}
func WithHandler(h string) Option {
return func(o *Options) {
o.Handler = h
}
}
func WithRegistry(r registry.Registry) Option {
return func(o *Options) {
o.Registry = r
}
}
func WithResolver(r resolver.Resolver) Option {
return func(o *Options) {
o.Resolver = r
}
}
// WithLogger sets the underline logger.
func WithLogger(l logger.Logger) Option {
return func(o *Options) {
o.Logger = l
}
}
@@ -0,0 +1,514 @@
// Package registry provides a dynamic api service router
package registry
import (
"errors"
"fmt"
"net/http"
"regexp"
"strings"
"sync"
"time"
"go-micro.dev/v4/api/router"
"go-micro.dev/v4/api/router/util"
log "go-micro.dev/v4/logger"
"go-micro.dev/v4/metadata"
"go-micro.dev/v4/registry"
"go-micro.dev/v4/registry/cache"
)
// endpoint struct, that holds compiled pcre.
type endpoint struct {
hostregs []*regexp.Regexp
pathregs []util.Pattern
pcreregs []*regexp.Regexp
}
// router is the default router.
type registryRouter struct {
opts router.Options
// registry cache
rc cache.Cache
exit chan bool
eps map[string]*router.Route
// compiled regexp for host and path
ceps map[string]*endpoint
sync.RWMutex
}
func (r *registryRouter) isStopped() bool {
select {
case <-r.exit:
return true
default:
return false
}
}
// refresh list of api services.
func (r *registryRouter) refresh() {
var attempts int
logger := r.Options().Logger
for {
services, err := r.opts.Registry.ListServices()
if err != nil {
attempts++
logger.Logf(log.ErrorLevel, "unable to list services: %v", err)
time.Sleep(time.Duration(attempts) * time.Second)
continue
}
attempts = 0
// for each service, get service and store endpoints
for _, s := range services {
service, err := r.rc.GetService(s.Name)
if err != nil {
logger.Logf(log.ErrorLevel, "unable to get service: %v", err)
continue
}
r.store(service)
}
// refresh list in 10 minutes... cruft
// use registry watching
select {
case <-time.After(time.Minute * 10):
case <-r.exit:
return
}
}
}
// process watch event.
func (r *registryRouter) process(res *registry.Result) {
logger := r.Options().Logger
// skip these things
if res == nil || res.Service == nil {
return
}
// get entry from cache
service, err := r.rc.GetService(res.Service.Name)
if err != nil {
logger.Logf(log.ErrorLevel, "unable to get service: %v", err)
return
}
// update our local endpoints
r.store(service)
}
// store local endpoint cache.
func (r *registryRouter) store(services []*registry.Service) {
logger := r.Options().Logger
// endpoints
eps := map[string]*router.Route{}
// services
names := map[string]bool{}
// create a new endpoint mapping
for _, service := range services {
// set names we need later
names[service.Name] = true
// map per endpoint
for _, sep := range service.Endpoints {
// create a key service:endpoint_name
key := fmt.Sprintf("%s.%s", service.Name, sep.Name)
// decode endpoint
end := router.Decode(sep.Metadata)
// if we got nothing skip
if err := router.Validate(end); err != nil {
logger.Logf(log.TraceLevel, "endpoint validation failed: %v", err)
continue
}
// try get endpoint
ep, ok := eps[key]
if !ok {
ep = &router.Route{Service: service.Name}
}
// overwrite the endpoint
ep.Endpoint = end
// append services
ep.Versions = append(ep.Versions, service)
// store it
eps[key] = ep
}
}
r.Lock()
defer r.Unlock()
// delete any existing eps for services we know
for key, route := range r.eps {
// skip what we don't care about
if !names[route.Service] {
continue
}
// ok we know this thing
// delete delete delete
delete(r.eps, key)
}
// now set the eps we have
for name, ep := range eps {
r.eps[name] = ep
cep := &endpoint{}
for _, h := range ep.Endpoint.Host {
if h == "" || h == "*" {
continue
}
hostreg, err := regexp.CompilePOSIX(h)
if err != nil {
logger.Logf(log.TraceLevel, "endpoint have invalid host regexp: %v", err)
continue
}
cep.hostregs = append(cep.hostregs, hostreg)
}
for _, p := range ep.Endpoint.Path {
var pcreok bool
if p[0] == '^' && p[len(p)-1] == '$' {
pcrereg, err := regexp.CompilePOSIX(p)
if err == nil {
cep.pcreregs = append(cep.pcreregs, pcrereg)
pcreok = true
}
}
rule, err := util.Parse(p)
if err != nil && !pcreok {
logger.Logf(log.TraceLevel, "endpoint have invalid path pattern: %v", err)
continue
} else if err != nil && pcreok {
continue
}
tpl := rule.Compile()
pathreg, err := util.NewPattern(tpl.Version, tpl.OpCodes, tpl.Pool, "", util.PatternLogger(logger))
if err != nil {
logger.Logf(log.TraceLevel, "endpoint have invalid path pattern: %v", err)
continue
}
cep.pathregs = append(cep.pathregs, pathreg)
}
r.ceps[name] = cep
}
}
// watch for endpoint changes.
func (r *registryRouter) watch() {
var attempts int
logger := r.Options().Logger
for {
if r.isStopped() {
return
}
// watch for changes
w, err := r.opts.Registry.Watch()
if err != nil {
attempts++
logger.Logf(log.ErrorLevel, "error watching endpoints: %v", err)
time.Sleep(time.Duration(attempts) * time.Second)
continue
}
ch := make(chan bool)
go func() {
select {
case <-ch:
w.Stop()
case <-r.exit:
w.Stop()
}
}()
// reset if we get here
attempts = 0
for {
// process next event
res, err := w.Next()
if err != nil {
logger.Logf(log.ErrorLevel, "error getting next endoint: %v", err)
close(ch)
break
}
r.process(res)
}
}
}
func (r *registryRouter) Options() router.Options {
return r.opts
}
func (r *registryRouter) Stop() error {
select {
case <-r.exit:
return nil
default:
close(r.exit)
r.rc.Stop()
}
return nil
}
func (r *registryRouter) Register(ep *router.Route) error {
return nil
}
func (r *registryRouter) Deregister(ep *router.Route) error {
return nil
}
func (r *registryRouter) Endpoint(req *http.Request) (*router.Route, error) {
logger := r.Options().Logger
if r.isStopped() {
return nil, errors.New("router closed")
}
r.RLock()
defer r.RUnlock()
var idx int
if len(req.URL.Path) > 0 && req.URL.Path != "/" {
idx = 1
}
path := strings.Split(req.URL.Path[idx:], "/")
// use the first match
// TODO: weighted matching
for n, endpoint := range r.eps {
cep, ok := r.ceps[n]
if !ok {
continue
}
ep := endpoint.Endpoint
var mMatch, hMatch, pMatch bool
// 1. try method
for _, m := range ep.Method {
if m == req.Method {
mMatch = true
break
}
}
if !mMatch {
continue
}
logger.Logf(log.DebugLevel, "api method match %s", req.Method)
// 2. try host
if len(ep.Host) == 0 {
hMatch = true
} else {
for idx, h := range ep.Host {
if h == "" || h == "*" {
hMatch = true
break
} else if cep.hostregs[idx].MatchString(req.URL.Host) {
hMatch = true
break
}
}
}
if !hMatch {
continue
}
logger.Logf(log.DebugLevel, "api host match %s", req.URL.Host)
// 3. try path via google.api path matching
for _, pathreg := range cep.pathregs {
matches, err := pathreg.Match(path, "")
if err != nil {
logger.Logf(log.DebugLevel, "api gpath not match %s != %v", path, pathreg)
continue
}
logger.Logf(log.DebugLevel, "api gpath match %s = %v", path, pathreg)
pMatch = true
ctx := req.Context()
md, ok := metadata.FromContext(ctx)
if !ok {
md = make(metadata.Metadata)
}
for k, v := range matches {
md[fmt.Sprintf("x-api-field-%s", k)] = v
}
*req = *req.Clone(metadata.NewContext(ctx, md))
break
}
if !pMatch {
// 4. try path via pcre path matching
for _, pathreg := range cep.pcreregs {
if !pathreg.MatchString(req.URL.Path) {
logger.Logf(log.DebugLevel, "api pcre path not match %s != %v", path, pathreg)
continue
}
logger.Logf(log.DebugLevel, "api pcre path match %s != %v", path, pathreg)
pMatch = true
break
}
}
if !pMatch {
continue
}
// TODO: Percentage traffic
// we got here, so its a match
return endpoint, nil
}
// no match
return nil, errors.New("not found")
}
func (r *registryRouter) Route(req *http.Request) (*router.Route, error) {
if r.isStopped() {
return nil, errors.New("router closed")
}
// try get an endpoint
ep, err := r.Endpoint(req)
if err == nil {
return ep, nil
}
// error not nil
// ignore that shit
// TODO: don't ignore that shit
// get the service name
rsp, err := r.opts.Resolver.Resolve(req)
if err != nil {
return nil, err
}
// service name
name := rsp.Name
// get service
services, err := r.rc.GetService(name)
if err != nil {
return nil, err
}
// only use endpoint matching when the meta handler is set aka api.Default
switch r.opts.Handler {
// rpc handlers
case "meta", "api", "rpc":
handler := r.opts.Handler
// set default handler to api
if r.opts.Handler == "meta" {
handler = "rpc"
}
// extract endpoint from Path, case-sensitive
// just test it in this case, maybe should put the code somewhere else
ep_name := rsp.Method
comps := strings.Split(rsp.Path, "/")
switch len(comps) {
case 3:
ep_name = comps[1] + "." + comps[2]
case 4:
ep_name = comps[2] + "." + comps[3]
}
// construct api service
return &router.Route{
Service: name,
Endpoint: &router.Endpoint{
Name: ep_name,
Handler: handler,
},
Versions: services,
}, nil
// http handler
case "http", "proxy", "web":
// construct api service
return &router.Route{
Service: name,
Endpoint: &router.Endpoint{
Name: req.URL.String(),
Handler: r.opts.Handler,
Host: []string{req.Host},
Method: []string{req.Method},
Path: []string{req.URL.Path},
},
Versions: services,
}, nil
}
return nil, errors.New("unknown handler")
}
func newRouter(opts ...router.Option) *registryRouter {
options := router.NewOptions(opts...)
r := &registryRouter{
exit: make(chan bool),
opts: options,
rc: cache.New(options.Registry),
eps: make(map[string]*router.Route),
ceps: make(map[string]*endpoint),
}
go r.watch()
go r.refresh()
return r
}
// NewRouter returns the default router.
func NewRouter(opts ...router.Option) router.Router {
return newRouter(opts...)
}
+49
View File
@@ -0,0 +1,49 @@
// Package router provides api service routing
package router
import (
"net/http"
"go-micro.dev/v4/registry"
)
// Router is used to determine an endpoint for a request.
type Router interface {
// Returns options
Options() Options
// Register endpoint in router
Register(r *Route) error
// Deregister endpoint from router
Deregister(r *Route) error
// Route returns an api.Service route
Route(r *http.Request) (*Route, error)
// Stop the router
Stop() error
}
type Route struct {
// Name of service
Service string
// The endpoint for this service
Endpoint *Endpoint
// Versions of this service
Versions []*registry.Service
}
// Endpoint is a mapping between an RPC method and HTTP endpoint.
type Endpoint struct {
// RPC Method e.g. Greeter.Hello
Name string
// What the endpoint is for
Description string
// API Handler e.g rpc, proxy
Handler string
// HTTP Host e.g example.com
Host []string
// HTTP Methods e.g GET, POST
Method []string
// HTTP Path e.g /greeter. Expect POSIX regex
Path []string
// Stream flag
Stream bool
}
@@ -0,0 +1,27 @@
Copyright (c) 2015, Gengo, Inc.
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of Gengo, Inc. nor the names of its
contributors may be used to endorse or promote products derived from this
software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+122
View File
@@ -0,0 +1,122 @@
package util
// download from
// https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/protoc-gen-grpc-gateway/httprule/compile.go
const (
opcodeVersion = 1
)
// Template is a compiled representation of path templates.
type Template struct {
// Verb is a VERB part in the template.
Verb string
// Original template (example: /v1/a_bit_of_everything)
Template string
// OpCodes is a sequence of operations.
OpCodes []int
// Pool is a constant pool
Pool []string
// Fields is a list of field paths bound in this template.
Fields []string
// Version is the version number of the format.
Version int
}
// Compiler compiles utilities representation of path templates into marshallable operations.
// They can be unmarshalled by runtime.NewPattern.
type Compiler interface {
Compile() Template
}
type op struct {
// str is a string operand of the code.
// operand is ignored if str is not empty.
str string
// code is the opcode of the operation
code OpCode
// operand is a numeric operand of the code.
operand int
}
func (w wildcard) compile() []op {
return []op{
{code: OpPush},
}
}
func (w deepWildcard) compile() []op {
return []op{
{code: OpPushM},
}
}
func (l literal) compile() []op {
return []op{
{
code: OpLitPush,
str: string(l),
},
}
}
func (v variable) compile() []op {
var ops []op
for _, s := range v.segments {
ops = append(ops, s.compile()...)
}
ops = append(ops, op{
code: OpConcatN,
operand: len(v.segments),
}, op{
code: OpCapture,
str: v.path,
})
return ops
}
func (t template) Compile() Template {
var rawOps []op
for _, s := range t.segments {
rawOps = append(rawOps, s.compile()...)
}
var (
ops []int
pool []string
fields []string
)
consts := make(map[string]int)
for _, op := range rawOps {
ops = append(ops, int(op.code))
if op.str == "" {
ops = append(ops, op.operand)
} else {
if _, ok := consts[op.str]; !ok {
consts[op.str] = len(pool)
pool = append(pool, op.str)
}
ops = append(ops, consts[op.str])
}
if op.code == OpCapture {
fields = append(fields, op.str)
}
}
return Template{
Version: opcodeVersion,
OpCodes: ops,
Pool: pool,
Verb: t.verb,
Fields: fields,
Template: t.template,
}
}
+396
View File
@@ -0,0 +1,396 @@
package util
// download from
// https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/protoc-gen-grpc-gateway/httprule/parse.go
import (
"fmt"
"strings"
log "go-micro.dev/v4/logger"
)
// InvalidTemplateError indicates that the path template is not valid.
type InvalidTemplateError struct {
tmpl string
msg string
}
func (e InvalidTemplateError) Error() string {
return fmt.Sprintf("%s: %s", e.msg, e.tmpl)
}
// Parse parses the string representation of path template.
func Parse(tmpl string) (Compiler, error) {
if !strings.HasPrefix(tmpl, "/") {
return template{}, InvalidTemplateError{tmpl: tmpl, msg: "no leading /"}
}
tokens, verb := tokenize(tmpl[1:])
p := parser{tokens: tokens}
segs, err := p.topLevelSegments()
if err != nil {
return template{}, InvalidTemplateError{tmpl: tmpl, msg: err.Error()}
}
return template{
segments: segs,
verb: verb,
template: tmpl,
}, nil
}
func tokenize(path string) (tokens []string, verb string) {
if path == "" {
return []string{eof}, ""
}
const (
init = iota
field
nested
)
var (
st = init
)
for path != "" {
var idx int
switch st {
case init:
idx = strings.IndexAny(path, "/{")
case field:
idx = strings.IndexAny(path, ".=}")
case nested:
idx = strings.IndexAny(path, "/}")
}
if idx < 0 {
tokens = append(tokens, path)
break
}
switch r := path[idx]; r {
case '/', '.':
case '{':
st = field
case '=':
st = nested
case '}':
st = init
}
if idx == 0 {
tokens = append(tokens, path[idx:idx+1])
} else {
tokens = append(tokens, path[:idx], path[idx:idx+1])
}
path = path[idx+1:]
}
l := len(tokens)
t := tokens[l-1]
if idx := strings.LastIndex(t, ":"); idx == 0 {
tokens, verb = tokens[:l-1], t[1:]
} else if idx > 0 {
tokens[l-1], verb = t[:idx], t[idx+1:]
}
tokens = append(tokens, eof)
return tokens, verb
}
// parser is a parser of the template syntax defined in github.com/googleapis/googleapis/google/api/http.proto.
type parser struct {
logger log.Logger
tokens []string
accepted []string
}
// topLevelSegments is the target of this parser.
func (p *parser) topLevelSegments() ([]segment, error) {
logger := log.LoggerOrDefault(p.logger)
logger.Logf(log.DebugLevel, "Parsing %q", p.tokens)
segs, err := p.segments()
if err != nil {
return nil, err
}
logger.Logf(log.DebugLevel, "accept segments: %q; %q", p.accepted, p.tokens)
if _, err := p.accept(typeEOF); err != nil {
return nil, fmt.Errorf("unexpected token %q after segments %q", p.tokens[0], strings.Join(p.accepted, ""))
}
logger.Logf(log.DebugLevel, "accept eof: %q; %q", p.accepted, p.tokens)
return segs, nil
}
func (p *parser) segments() ([]segment, error) {
logger := log.LoggerOrDefault(p.logger)
s, err := p.segment()
if err != nil {
return nil, err
}
logger.Logf(log.DebugLevel, "accept segment: %q; %q", p.accepted, p.tokens)
segs := []segment{s}
for {
if _, err := p.accept("/"); err != nil {
return segs, nil
}
s, err := p.segment()
if err != nil {
return segs, err
}
segs = append(segs, s)
logger.Logf(log.DebugLevel, "accept segment: %q; %q", p.accepted, p.tokens)
}
}
func (p *parser) segment() (segment, error) {
if _, err := p.accept("*"); err == nil {
return wildcard{}, nil
}
if _, err := p.accept("**"); err == nil {
return deepWildcard{}, nil
}
if l, err := p.literal(); err == nil {
return l, nil
}
v, err := p.variable()
if err != nil {
return nil, fmt.Errorf("segment neither wildcards, literal or variable: %w", err)
}
return v, err
}
func (p *parser) literal() (segment, error) {
lit, err := p.accept(typeLiteral)
if err != nil {
return nil, err
}
return literal(lit), nil
}
func (p *parser) variable() (segment, error) {
if _, err := p.accept("{"); err != nil {
return nil, err
}
path, err := p.fieldPath()
if err != nil {
return nil, err
}
var segs []segment
if _, err := p.accept("="); err == nil {
segs, err = p.segments()
if err != nil {
return nil, fmt.Errorf("invalid segment in variable %q: %w", path, err)
}
} else {
segs = []segment{wildcard{}}
}
if _, err := p.accept("}"); err != nil {
return nil, fmt.Errorf("unterminated variable segment: %s", path)
}
return variable{
path: path,
segments: segs,
}, nil
}
func (p *parser) fieldPath() (string, error) {
c, err := p.accept(typeIdent)
if err != nil {
return "", err
}
components := []string{c}
for {
if _, err = p.accept("."); err != nil {
return strings.Join(components, "."), nil
}
c, err := p.accept(typeIdent)
if err != nil {
return "", fmt.Errorf("invalid field path component: %v", err)
}
components = append(components, c)
}
}
// A termType is a type of terminal symbols.
type termType string
// These constants define some of valid values of termType.
// They improve readability of parse functions.
//
// You can also use "/", "*", "**", "." or "=" as valid values.
const (
typeIdent = termType("ident")
typeLiteral = termType("literal")
typeEOF = termType("$")
)
const (
// eof is the terminal symbol which always appears at the end of token sequence.
eof = "\u0000"
)
// accept tries to accept a token in "p".
// This function consumes a token and returns it if it matches to the specified "term".
// If it doesn't match, the function does not consume any tokens and return an error.
func (p *parser) accept(term termType) (string, error) {
t := p.tokens[0]
switch term {
case "/", "*", "**", ".", "=", "{", "}":
if t != string(term) && t != "/" {
return "", fmt.Errorf("expected %q but got %q", term, t)
}
case typeEOF:
if t != eof {
return "", fmt.Errorf("expected EOF but got %q", t)
}
case typeIdent:
if err := expectIdent(t); err != nil {
return "", err
}
case typeLiteral:
if err := expectPChars(t); err != nil {
return "", err
}
default:
return "", fmt.Errorf("unknown termType %q", term)
}
p.tokens = p.tokens[1:]
p.accepted = append(p.accepted, t)
return t, nil
}
// expectPChars determines if "t" consists of only pchars defined in RFC3986.
//
// https://www.ietf.org/rfc/rfc3986.txt, P.49
//
// pchar = unreserved / pct-encoded / sub-delims / ":" / "@"
// unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
// sub-delims = "!" / "$" / "&" / "'" / "(" / ")"
// / "*" / "+" / "," / ";" / "="
// pct-encoded = "%" HEXDIG HEXDIG
func expectPChars(t string) error {
const (
init = iota
pct1
pct2
)
st := init
for _, r := range t {
if st != init {
if !isHexDigit(r) {
return fmt.Errorf("invalid hexdigit: %c(%U)", r, r)
}
switch st {
case pct1:
st = pct2
case pct2:
st = init
}
continue
}
// unreserved
switch {
case 'A' <= r && r <= 'Z':
continue
case 'a' <= r && r <= 'z':
continue
case '0' <= r && r <= '9':
continue
}
switch r {
case '-', '.', '_', '~':
// unreserved
case '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '=':
// sub-delims
case ':', '@':
// rest of pchar
case '%':
// pct-encoded
st = pct1
default:
return fmt.Errorf("invalid character in path segment: %q(%U)", r, r)
}
}
if st != init {
return fmt.Errorf("invalid percent-encoding in %q", t)
}
return nil
}
// expectIdent determines if "ident" is a valid identifier in .proto schema ([[:alpha:]_][[:alphanum:]_]*).
func expectIdent(ident string) error {
if ident == "" {
return fmt.Errorf("empty identifier")
}
for pos, r := range ident {
switch {
case '0' <= r && r <= '9':
if pos == 0 {
return fmt.Errorf("identifier starting with digit: %s", ident)
}
continue
case 'A' <= r && r <= 'Z':
continue
case 'a' <= r && r <= 'z':
continue
case r == '_':
continue
default:
return fmt.Errorf("invalid character %q(%U) in identifier: %s", r, r, ident)
}
}
return nil
}
func isHexDigit(r rune) bool {
switch {
case '0' <= r && r <= '9':
return true
case 'A' <= r && r <= 'F':
return true
case 'a' <= r && r <= 'f':
return true
}
return false
}
@@ -0,0 +1,24 @@
package util
// download from https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/utilities/pattern.go
// An OpCode is a opcode of compiled path patterns.
type OpCode int
// These constants are the valid values of OpCode.
const (
// OpNop does nothing.
OpNop = OpCode(iota)
// OpPush pushes a component to stack.
OpPush
// OpLitPush pushes a component to stack if it matches to the literal.
OpLitPush
// OpPushM concatenates the remaining components and pushes it to stack.
OpPushM
// OpConcatN pops N items from stack, concatenates them and pushes it back to stack.
OpConcatN
// OpCapture pops an item and binds it to the variable.
OpCapture
// OpEnd is the least positive invalid opcode.
OpEnd
)
+282
View File
@@ -0,0 +1,282 @@
package util
// download from https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/runtime/pattern.go
import (
"errors"
"fmt"
"strings"
log "go-micro.dev/v4/logger"
)
var (
// ErrNotMatch indicates that the given HTTP request path does not match to the pattern.
ErrNotMatch = errors.New("not match to the path pattern")
// ErrInvalidPattern indicates that the given definition of Pattern is not valid.
ErrInvalidPattern = errors.New("invalid pattern")
)
type rop struct {
code OpCode
operand int
}
// Pattern is a template pattern of http request paths defined in github.com/googleapis/googleapis/google/api/http.proto.
type Pattern struct {
// verb is the VERB part of the path pattern. It is empty if the pattern does not have VERB part.
verb string
// ops is a list of operations
ops []rop
// pool is a constant pool indexed by the operands or vars.
pool []string
// vars is a list of variables names to be bound by this pattern
vars []string
// stacksize is the max depth of the stack
stacksize int
// tailLen is the length of the fixed-size segments after a deep wildcard
tailLen int
// assumeColonVerb indicates whether a path suffix after a final
// colon may only be interpreted as a verb.
assumeColonVerb bool
}
type patternOptions struct {
logger log.Logger
assumeColonVerb bool
}
// PatternOpt is an option for creating Patterns.
type PatternOpt func(*patternOptions)
// PatternLogger sets the logger.
func PatternLogger(l log.Logger) PatternOpt {
return func(po *patternOptions) {
po.logger = l
}
}
// NewPattern returns a new Pattern from the given definition values.
// "ops" is a sequence of op codes. "pool" is a constant pool.
// "verb" is the verb part of the pattern. It is empty if the pattern does not have the part.
// "version" must be 1 for now.
// It returns an error if the given definition is invalid.
func NewPattern(version int, ops []int, pool []string, verb string, opts ...PatternOpt) (Pattern, error) {
options := patternOptions{
assumeColonVerb: true,
}
for _, o := range opts {
o(&options)
}
logger := log.LoggerOrDefault(options.logger)
if version != 1 {
logger.Logf(log.DebugLevel, "unsupported version: %d", version)
return Pattern{}, ErrInvalidPattern
}
l := len(ops)
if l%2 != 0 {
logger.Logf(log.DebugLevel, "odd number of ops codes: %d", l)
return Pattern{}, ErrInvalidPattern
}
var (
typedOps []rop
stack, maxstack int
tailLen int
pushMSeen bool
vars []string
)
for i := 0; i < l; i += 2 {
op := rop{code: OpCode(ops[i]), operand: ops[i+1]}
switch op.code {
case OpNop:
continue
case OpPush:
if pushMSeen {
tailLen++
}
stack++
case OpPushM:
if pushMSeen {
logger.Logf(log.DebugLevel, "pushM appears twice")
return Pattern{}, ErrInvalidPattern
}
pushMSeen = true
stack++
case OpLitPush:
if op.operand < 0 || len(pool) <= op.operand {
logger.Logf(log.DebugLevel, "negative literal index: %d", op.operand)
return Pattern{}, ErrInvalidPattern
}
if pushMSeen {
tailLen++
}
stack++
case OpConcatN:
if op.operand <= 0 {
logger.Logf(log.DebugLevel, "negative concat size: %d", op.operand)
return Pattern{}, ErrInvalidPattern
}
stack -= op.operand
if stack < 0 {
logger.Logf(log.DebugLevel, "stack underflow")
return Pattern{}, ErrInvalidPattern
}
stack++
case OpCapture:
if op.operand < 0 || len(pool) <= op.operand {
logger.Logf(log.DebugLevel, "variable name index out of bound: %d", op.operand)
return Pattern{}, ErrInvalidPattern
}
v := pool[op.operand]
op.operand = len(vars)
vars = append(vars, v)
stack--
if stack < 0 {
logger.Logf(log.DebugLevel, "stack underflow")
return Pattern{}, ErrInvalidPattern
}
default:
logger.Logf(log.DebugLevel, "invalid opcode: %d", op.code)
return Pattern{}, ErrInvalidPattern
}
if maxstack < stack {
maxstack = stack
}
typedOps = append(typedOps, op)
}
return Pattern{
ops: typedOps,
pool: pool,
vars: vars,
stacksize: maxstack,
tailLen: tailLen,
verb: verb,
assumeColonVerb: options.assumeColonVerb,
}, nil
}
// MustPattern is a helper function which makes it easier to call NewPattern in variable initialization.
func MustPattern(p Pattern, err error) Pattern {
if err != nil {
log.Logf(log.FatalLevel, "Pattern initialization failed: %v", err)
}
return p
}
// Match examines components if it matches to the Pattern.
// If it matches, the function returns a mapping from field paths to their captured values.
// If otherwise, the function returns an error.
func (p Pattern) Match(components []string, verb string) (map[string]string, error) {
if p.verb != verb {
if p.assumeColonVerb || p.verb != "" {
return nil, ErrNotMatch
}
if len(components) == 0 {
components = []string{":" + verb}
} else {
components = append([]string{}, components...)
components[len(components)-1] += ":" + verb
}
verb = ""
}
var pos int
stack := make([]string, 0, p.stacksize)
captured := make([]string, len(p.vars))
l := len(components)
for _, op := range p.ops {
switch op.code {
case OpNop:
continue
case OpPush, OpLitPush:
if pos >= l {
return nil, ErrNotMatch
}
c := components[pos]
if op.code == OpLitPush {
if lit := p.pool[op.operand]; c != lit {
return nil, ErrNotMatch
}
}
stack = append(stack, c)
pos++
case OpPushM:
end := len(components)
if end < pos+p.tailLen {
return nil, ErrNotMatch
}
end -= p.tailLen
stack = append(stack, strings.Join(components[pos:end], "/"))
pos = end
case OpConcatN:
n := op.operand
l := len(stack) - n
stack = append(stack[:l], strings.Join(stack[l:], "/"))
case OpCapture:
n := len(stack) - 1
captured[op.operand] = stack[n]
stack = stack[:n]
}
}
if pos < l {
return nil, ErrNotMatch
}
bindings := make(map[string]string)
for i, val := range captured {
bindings[p.vars[i]] = val
}
return bindings, nil
}
// Verb returns the verb part of the Pattern.
func (p Pattern) Verb() string { return p.verb }
func (p Pattern) String() string {
var stack []string
for _, op := range p.ops {
switch op.code {
case OpNop:
continue
case OpPush:
stack = append(stack, "*")
case OpLitPush:
stack = append(stack, p.pool[op.operand])
case OpPushM:
stack = append(stack, "**")
case OpConcatN:
n := op.operand
l := len(stack) - n
stack = append(stack[:l], strings.Join(stack[l:], "/"))
case OpCapture:
n := len(stack) - 1
stack[n] = fmt.Sprintf("{%s=%s}", p.vars[op.operand], stack[n])
}
}
segs := strings.Join(stack, "/")
if p.verb != "" {
return fmt.Sprintf("/%s:%s", segs, p.verb)
}
return "/" + segs
}
// AssumeColonVerbOpt indicates whether a path suffix after a final
// colon may only be interpreted as a verb.
func AssumeColonVerbOpt(val bool) PatternOpt {
return PatternOpt(func(o *patternOptions) {
o.assumeColonVerb = val
})
}
+66
View File
@@ -0,0 +1,66 @@
package util
// download from
// https://raw.githubusercontent.com/grpc-ecosystem/grpc-gateway/master/protoc-gen-grpc-gateway/httprule/types.go
import (
"fmt"
"strings"
)
type template struct {
verb string
template string
segments []segment
}
type segment interface {
fmt.Stringer
compile() (ops []op)
}
type wildcard struct{}
type deepWildcard struct{}
type literal string
type variable struct {
path string
segments []segment
}
func (wildcard) String() string {
return "*"
}
func (deepWildcard) String() string {
return "**"
}
func (l literal) String() string {
return string(l)
}
func (v variable) String() string {
var segs []string
for _, s := range v.segments {
segs = append(segs, s.String())
}
return fmt.Sprintf("{%s=%s}", v.path, strings.Join(segs, "/"))
}
func (t template) String() string {
var segs []string
for _, s := range t.segments {
segs = append(segs, s.String())
}
str := strings.Join(segs, "/")
if t.verb != "" {
str = fmt.Sprintf("%s:%s", str, t.verb)
}
return "/" + str
}
+28
View File
@@ -0,0 +1,28 @@
// Package acme abstracts away various ACME libraries
package acme
import (
"crypto/tls"
"errors"
"net"
)
var (
// ErrProviderNotImplemented can be returned when attempting to
// instantiate an unimplemented provider.
ErrProviderNotImplemented = errors.New("Provider not implemented")
)
// Provider is a ACME provider interface.
type Provider interface {
// Listen returns a new listener
Listen(...string) (net.Listener, error)
// TLSConfig returns a tls config
TLSConfig(...string) (*tls.Config, error)
}
// The Let's Encrypt ACME endpoints.
const (
LetsEncryptStagingCA = "https://acme-staging-v02.api.letsencrypt.org/directory"
LetsEncryptProductionCA = "https://acme-v02.api.letsencrypt.org/directory"
)
@@ -0,0 +1,86 @@
package acme
import (
"github.com/go-acme/lego/v4/challenge"
"go-micro.dev/v4/logger"
)
// Option (or Options) are passed to New() to configure providers.
type Option func(o *Options)
// Options represents various options you can present to ACME providers.
type Options struct {
// ChallengeProvider is a go-acme/lego challenge provider. Set this if you
// want to use DNS Challenges. Otherwise, tls-alpn-01 will be used
ChallengeProvider challenge.Provider
// Cache is a storage interface. Most ACME libraries have an cache, but
// there's no defined interface, so if you consume this option
// sanity check it before using.
Cache interface{}
// Logger is the underling logging framework
Logger logger.Logger
// CA is the CA to use
CA string
// AcceptTLS must be set to true to indicate that you have read your
// provider's terms of service.
AcceptToS bool
// Issue certificates for domains on demand. Otherwise, certs will be
// retrieved / issued on start-up.
OnDemand bool
}
// AcceptToS indicates whether you accept your CA's terms of service.
func AcceptToS(b bool) Option {
return func(o *Options) {
o.AcceptToS = b
}
}
// CA sets the CA of an acme.Options.
func CA(CA string) Option {
return func(o *Options) {
o.CA = CA
}
}
// ChallengeProvider sets the Challenge provider of an acme.Options
// if set, it enables the DNS challenge, otherwise tls-alpn-01 will be used.
func ChallengeProvider(p challenge.Provider) Option {
return func(o *Options) {
o.ChallengeProvider = p
}
}
// OnDemand enables on-demand certificate issuance. Not recommended for use
// with the DNS challenge, as the first connection may be very slow.
func OnDemand(b bool) Option {
return func(o *Options) {
o.OnDemand = b
}
}
// Cache provides a cache / storage interface to the underlying ACME library
// as there is no standard, this needs to be validated by the underlying
// implementation.
func Cache(c interface{}) Option {
return func(o *Options) {
o.Cache = c
}
}
// Logger sets the underline logger.
func Logger(l logger.Logger) Option {
return func(o *Options) {
o.Logger = l
}
}
// DefaultOptions uses the Let's Encrypt Production CA, with DNS Challenge disabled.
func DefaultOptions() Options {
return Options{
AcceptToS: true,
CA: LetsEncryptProductionCA,
OnDemand: true,
}
}
+57
View File
@@ -0,0 +1,57 @@
package cors
import (
"net/http"
)
type Config struct {
AllowOrigin string
AllowMethods string
AllowHeaders string
AllowCredentials bool
}
// CombinedCORSHandler wraps a server and provides CORS headers.
func CombinedCORSHandler(h http.Handler, config *Config) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if config != nil {
SetHeaders(w, r, config)
}
if r.Method == "OPTIONS" {
return
}
h.ServeHTTP(w, r)
})
}
// SetHeaders sets the CORS headers.
func SetHeaders(w http.ResponseWriter, _ *http.Request, config *Config) {
set := func(w http.ResponseWriter, k, v string) {
if v := w.Header().Get(k); len(v) > 0 {
return
}
w.Header().Set(k, v)
}
// For forward-compatible code, default values may not be provided in the future
if config.AllowCredentials {
set(w, "Access-Control-Allow-Credentials", "true")
} else {
set(w, "Access-Control-Allow-Credentials", "false")
}
if config.AllowOrigin == "" {
set(w, "Access-Control-Allow-Origin", "*")
} else {
set(w, "Access-Control-Allow-Origin", config.AllowOrigin)
}
if config.AllowMethods == "" {
set(w, "Access-Control-Allow-Methods", "POST, PATCH, GET, OPTIONS, PUT, DELETE")
} else {
set(w, "Access-Control-Allow-Methods", config.AllowMethods)
}
if config.AllowHeaders == "" {
set(w, "Access-Control-Allow-Headers", "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization")
} else {
set(w, "Access-Control-Allow-Headers", config.AllowHeaders)
}
}
+118
View File
@@ -0,0 +1,118 @@
// Package http provides a http server with features; acme, cors, etc
package http
import (
"crypto/tls"
"net"
"net/http"
"os"
"sync"
"github.com/gorilla/handlers"
"go-micro.dev/v4/api/server"
"go-micro.dev/v4/api/server/cors"
log "go-micro.dev/v4/logger"
)
type httpServer struct {
opts server.Options
mux *http.ServeMux
exit chan chan error
address string
mtx sync.RWMutex
}
func NewServer(address string, opts ...server.Option) server.Server {
options := server.NewOptions(opts...)
return &httpServer{
opts: options,
mux: http.NewServeMux(),
address: address,
exit: make(chan chan error),
}
}
func (s *httpServer) Address() string {
s.mtx.RLock()
defer s.mtx.RUnlock()
return s.address
}
func (s *httpServer) Init(opts ...server.Option) error {
for _, o := range opts {
o(&s.opts)
}
return nil
}
func (s *httpServer) Handle(path string, handler http.Handler) {
// TODO: move this stuff out to one place with ServeHTTP
// apply the wrappers, e.g. auth
for _, wrapper := range s.opts.Wrappers {
handler = wrapper(handler)
}
// wrap with cors
if s.opts.EnableCORS {
handler = cors.CombinedCORSHandler(handler, s.opts.CORSConfig)
}
// wrap with logger
handler = handlers.CombinedLoggingHandler(os.Stdout, handler)
s.mux.Handle(path, handler)
}
func (s *httpServer) Start() error {
logger := s.opts.Logger
var l net.Listener
var err error
if s.opts.EnableACME && s.opts.ACMEProvider != nil {
// should we check the address to make sure its using :443?
l, err = s.opts.ACMEProvider.Listen(s.opts.ACMEHosts...)
} else if s.opts.EnableTLS && s.opts.TLSConfig != nil {
l, err = tls.Listen("tcp", s.address, s.opts.TLSConfig)
} else {
// otherwise plain listen
l, err = net.Listen("tcp", s.address)
}
if err != nil {
return err
}
logger.Logf(log.InfoLevel, "HTTP API Listening on %s", l.Addr().String())
s.mtx.Lock()
s.address = l.Addr().String()
s.mtx.Unlock()
go func() {
if err := http.Serve(l, s.mux); err != nil {
// temporary fix
// logger.Log(log.FatalLevel, err)
logger.Log(log.ErrorLevel, err)
}
}()
go func() {
ch := <-s.exit
ch <- l.Close()
}()
return nil
}
func (s *httpServer) Stop() error {
ch := make(chan error)
s.exit <- ch
return <-ch
}
func (s *httpServer) String() string {
return "http"
}
+101
View File
@@ -0,0 +1,101 @@
package server
import (
"crypto/tls"
"net/http"
"go-micro.dev/v4/api/resolver"
"go-micro.dev/v4/api/server/acme"
"go-micro.dev/v4/api/server/cors"
"go-micro.dev/v4/logger"
)
type Option func(o *Options)
type Options struct {
ACMEProvider acme.Provider
Resolver resolver.Resolver
Logger logger.Logger
CORSConfig *cors.Config
TLSConfig *tls.Config
ACMEHosts []string
Wrappers []Wrapper
EnableACME bool
EnableCORS bool
EnableTLS bool
}
type Wrapper func(h http.Handler) http.Handler
func NewOptions(opts ...Option) Options {
options := Options{
Logger: logger.DefaultLogger,
}
for _, o := range opts {
o(&options)
}
return options
}
func WrapHandler(w Wrapper) Option {
return func(o *Options) {
o.Wrappers = append(o.Wrappers, w)
}
}
func EnableCORS(b bool) Option {
return func(o *Options) {
o.EnableCORS = b
}
}
func CORSConfig(c *cors.Config) Option {
return func(o *Options) {
o.CORSConfig = c
}
}
func EnableACME(b bool) Option {
return func(o *Options) {
o.EnableACME = b
}
}
func ACMEHosts(hosts ...string) Option {
return func(o *Options) {
o.ACMEHosts = hosts
}
}
func ACMEProvider(p acme.Provider) Option {
return func(o *Options) {
o.ACMEProvider = p
}
}
func EnableTLS(b bool) Option {
return func(o *Options) {
o.EnableTLS = b
}
}
func TLSConfig(t *tls.Config) Option {
return func(o *Options) {
o.TLSConfig = t
}
}
func Resolver(r resolver.Resolver) Option {
return func(o *Options) {
o.Resolver = r
}
}
// Logger sets the underline logging framework.
func Logger(l logger.Logger) Option {
return func(o *Options) {
o.Logger = l
}
}
+15
View File
@@ -0,0 +1,15 @@
// Package server provides an API gateway server which handles inbound requests
package server
import (
"net/http"
)
// Server serves api requests.
type Server interface {
Address() string
Init(opts ...Option) error
Handle(path string, handler http.Handler)
Start() error
Stop() error
}