├── .dockerignore ├── assets ├── bifrost.png ├── bifrostinline.png ├── diagram.drawio ├── requests_flow.svg └── configuration_flow.svg ├── abstraction ├── const.go └── endpoint.go ├── servicediscovery ├── registry │ ├── registry.go │ └── inmemory │ │ ├── registry.go │ │ └── concurrentmap.go ├── servicediscovery.go └── provider │ ├── testprovider.go │ └── kubernetes │ └── kubernetes.go ├── .gitignore ├── log ├── factory.go └── logger.go ├── httputils ├── compose.go ├── recovery.go └── clone.go ├── .github ├── release-drafter.yml └── workflows │ ├── release-drafter.yml │ └── release.yml ├── Dockerfile ├── Makefile ├── middleware ├── auth │ ├── sample_key.pub │ ├── sample_key │ ├── auth_test.go │ └── auth.go ├── middleware.go ├── options.go ├── cors │ ├── cors.go │ └── cors_test.go └── ratelimit │ └── limiter.go ├── router ├── routecontext.go ├── extensions.go ├── route.go └── dynamicrouter.go ├── README.md ├── handler ├── handler.go ├── reverseproxy │ ├── modifiers.go │ └── reverseproxy.go └── nats │ ├── options.go │ ├── nbbtransformer_test.go │ ├── nbbtransformer.go │ └── natspublisher.go ├── strutils └── utils.go ├── LICENSE ├── tracing ├── round_tripper.go ├── wrapper.go └── spanlogger.go ├── gateway ├── config.go ├── gateway_test.go └── gateway.go ├── config.json ├── go.mod ├── main.go └── main_test.go /.dockerignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .git 3 | -------------------------------------------------------------------------------- /assets/bifrost.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osstotalsoft/bifrost/HEAD/assets/bifrost.png -------------------------------------------------------------------------------- /assets/bifrostinline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/osstotalsoft/bifrost/HEAD/assets/bifrostinline.png -------------------------------------------------------------------------------- /abstraction/const.go: -------------------------------------------------------------------------------- 1 | package abstraction 2 | 3 | //ContextClaimsKey is the code used to register claims into context 4 | const ContextClaimsKey = "ContextClaimsKey" 5 | const HttpUserIdHeader = "user-id" 6 | -------------------------------------------------------------------------------- /servicediscovery/registry/registry.go: -------------------------------------------------------------------------------- 1 | package registry 2 | 3 | //type ServiceRegistryLoader func(serviceName string) (service servicediscovery.Service, ok bool) 4 | //type ServiceRegistrySaver func(service servicediscovery.Service) error 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # IDE settings 15 | .idea/** -------------------------------------------------------------------------------- /log/factory.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "context" 5 | "go.uber.org/zap" 6 | ) 7 | 8 | type Factory func(ctx context.Context) Logger 9 | 10 | func ZapLoggerFactory(logger *zap.Logger) Factory { 11 | return func(ctx context.Context) Logger { 12 | return zapLogger{logger} 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /httputils/compose.go: -------------------------------------------------------------------------------- 1 | package httputils 2 | 3 | import "net/http" 4 | 5 | //Compose Handlers 6 | func Compose(funcs ...func(handler http.Handler) http.Handler) func(handler http.Handler) http.Handler { 7 | return func(h http.Handler) http.Handler { 8 | for _, f := range funcs { 9 | h = f(h) 10 | } 11 | return h 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /servicediscovery/registry/inmemory/registry.go: -------------------------------------------------------------------------------- 1 | package inmemory 2 | 3 | import ( 4 | "github.com/osstotalsoft/bifrost/servicediscovery" 5 | ) 6 | 7 | func Store(store *inMemoryRegistryData) servicediscovery.ServiceFunc { 8 | return func(service servicediscovery.Service) { 9 | store.Store(service.Address, service) 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name-template: v$NEXT_PATCH_VERSION 🌈 2 | tag-template: v$NEXT_PATCH_VERSION 3 | categories: 4 | - title: 🚀 Features 5 | label: feature 6 | - title: 🐛 Bug Fixes 7 | label: fix 8 | - title: 🧰 Maintenance 9 | label: chore 10 | change-template: '- $TITLE @$AUTHOR (#$NUMBER)' 11 | template: | 12 | ## Changes 13 | 14 | $CHANGES -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.17 AS builder 2 | RUN go version 3 | WORKDIR /src 4 | COPY ./go.mod ./go.sum ./ 5 | RUN go mod download 6 | 7 | COPY ./ ./ 8 | RUN CGO_ENABLED=0 go build \ 9 | -installsuffix 'static' \ 10 | # -gcflags '-m -m' \ 11 | -o /app . 12 | 13 | FROM alpine AS final 14 | RUN apk add --no-cache bash openssh curl 15 | COPY --from=builder /app /app 16 | 17 | EXPOSE 8000 18 | ENTRYPOINT ["./app"] -------------------------------------------------------------------------------- /servicediscovery/servicediscovery.go: -------------------------------------------------------------------------------- 1 | package servicediscovery 2 | 3 | //Service is the main component of a service discovery 4 | type Service struct { 5 | UID string 6 | Name string 7 | Address string 8 | Resource string 9 | Secured bool 10 | OidcAudience string 11 | Version string 12 | Namespace string 13 | } 14 | 15 | //ServiceFunc is an alias 16 | type ServiceFunc func(service Service) 17 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DOCKER:=docker 2 | 3 | check-docker-env: 4 | ifeq ($(DOCKER_REGISTRY),) 5 | $(error DOCKER_REGISTRY environment variable must be set) 6 | endif 7 | ifeq ($(DOCKER_TAG),) 8 | $(error DOCKER_TAG environment variable must be set) 9 | endif 10 | 11 | docker-build: check-docker-env 12 | $(DOCKER) build . -t $(DOCKER_REGISTRY):$(DOCKER_TAG) 13 | 14 | docker-push: check-docker-env 15 | $(DOCKER) push $(DOCKER_REGISTRY):$(DOCKER_TAG) 16 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: Release Drafter 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | workflow_dispatch: 8 | 9 | jobs: 10 | update_release_draft: 11 | runs-on: ubuntu-latest 12 | steps: 13 | # Drafts your next Release notes as Pull Requests are merged into "master" 14 | - uses: release-drafter/release-drafter@v5 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /middleware/auth/sample_key.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4f5wg5l2hKsTeNem/V41 3 | fGnJm6gOdrj8ym3rFkEU/wT8RDtnSgFEZOQpHEgQ7JL38xUfU0Y3g6aYw9QT0hJ7 4 | mCpz9Er5qLaMXJwZxzHzAahlfA0icqabvJOMvQtzD6uQv6wPEyZtDTWiQi9AXwBp 5 | HssPnpYGIn20ZZuNlX2BrClciHhCPUIIZOQn/MmqTD31jSyjoQoV7MhhMTATKJx2 6 | XrHhR+1DcKJzQBSTAGnpYVaqpsARap+nwRipr3nUTuxyGohBTSmjJ2usSeQXHI3b 7 | ODIRe1AuTyHceAbewn8b462yEWKARdpd9AjQW5SIVPfdsz5B6GlYQ5LdYKtznTuy 8 | 7wIDAQAB 9 | -----END PUBLIC KEY----- -------------------------------------------------------------------------------- /middleware/middleware.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/osstotalsoft/bifrost/abstraction" 5 | "github.com/osstotalsoft/bifrost/log" 6 | "net/http" 7 | ) 8 | 9 | //Func is a signature that each middleware must implement 10 | type Func func(endpoint abstraction.Endpoint, loggerFactory log.Factory) func(http.Handler) http.Handler 11 | 12 | //Compose Funcs 13 | func Compose(funcs ...func(f Func) Func) func(f Func) Func { 14 | return func(m Func) Func { 15 | for _, f := range funcs { 16 | m = f(m) 17 | } 18 | return m 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /middleware/options.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type Options struct { 8 | startMiddlewareObserver func(r *http.Request) 9 | endMiddlewareObserver func(r *http.Request) 10 | } 11 | 12 | type Option func(*Options) 13 | 14 | func StartMiddlewareObserver(f func(r *http.Request)) Option { 15 | return func(options *Options) { 16 | options.startMiddlewareObserver = f 17 | } 18 | } 19 | 20 | func EndMiddlewareObserver(f func(r *http.Request)) Option { 21 | return func(options *Options) { 22 | options.endMiddlewareObserver = f 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /abstraction/endpoint.go: -------------------------------------------------------------------------------- 1 | package abstraction 2 | 3 | //Endpoint stores the gateway configuration for each routing and is passed around to all handlers and middleware 4 | type Endpoint struct { 5 | UpstreamPath string 6 | Secured bool 7 | OidcAudience string 8 | UpstreamPathPrefix string 9 | UpstreamURL string 10 | DownstreamPath string 11 | DownstreamPathPrefix string 12 | Methods []string 13 | HandlerType string 14 | HandlerConfig map[string]interface{} 15 | Filters map[string]interface{} 16 | } 17 | -------------------------------------------------------------------------------- /router/routecontext.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "context" 5 | "time" 6 | ) 7 | 8 | //ContextRouteKey is a key for storing route information into the request context 9 | const ContextRouteKey = "ContextRouteKey" 10 | 11 | //RouteContext is the object that gets stored into the request context 12 | type RouteContext struct { 13 | Path string 14 | PathPrefix string 15 | Timeout time.Duration 16 | Vars map[string]string 17 | } 18 | 19 | func GetRouteContextFromRequestContext(ctx context.Context) (RouteContext, bool) { 20 | a, b := ctx.Value(ContextRouteKey).(RouteContext) 21 | return a, b 22 | } 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![](assets/bifrostinline.png) 2 | Api gateway written in GO 3 | 4 | 5 | ## Architecture 6 | 7 | ### Configuration flow 8 | ![](assets/configuration_flow.svg) 9 | 10 | ### Request flow 11 | ![](assets/requests_flow.svg) 12 | 13 | [![GoDoc](https://godoc.org/github.com/osstotalsoft/bifrost?status.svg)](https://godoc.org/github.com/osstotalsoft/bifrost) 14 | [![Report Cart](https://goreportcard.com/badge/osstotalsoft/bifrost)](http://goreportcard.com/report/osstotalsoft/bifrost) 15 | [![Build status](https://dev.azure.com/totalsoft/Bifrost/_apis/build/status/Bifrost-Master)](https://dev.azure.com/totalsoft/Bifrost/_build/latest?definitionId=47) 16 | 17 | -------------------------------------------------------------------------------- /router/extensions.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "github.com/gorilla/mux" 5 | "net/http" 6 | ) 7 | 8 | //GorillaMuxRouteMatcher is used for route matching 9 | func GorillaMuxRouteMatcher(route Route) func(request *http.Request) RouteMatch { 10 | rr := new(mux.Route) 11 | 12 | if route.PathPrefix != "" { 13 | rr = rr.PathPrefix(route.PathPrefix) 14 | } 15 | if route.Path != "" { 16 | rr = rr.Path(route.Path) 17 | } 18 | if route.Methods != nil && len(route.Methods) > 0 { 19 | rr = rr.Methods(route.Methods...) 20 | } 21 | 22 | return func(request *http.Request) RouteMatch { 23 | var match mux.RouteMatch 24 | b := rr.Match(request, &match) 25 | return RouteMatch{b, match.Vars} 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /httputils/recovery.go: -------------------------------------------------------------------------------- 1 | package httputils 2 | 3 | import ( 4 | "github.com/osstotalsoft/bifrost/log" 5 | "go.uber.org/zap" 6 | "net/http" 7 | ) 8 | 9 | //RecoveryHandler handles pipeline panic 10 | func RecoveryHandler(loggerFactory log.Factory) func(inner http.Handler) http.Handler { 11 | return func(inner http.Handler) http.Handler { 12 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 13 | defer func() { 14 | if err := recover(); err != nil { 15 | w.WriteHeader(http.StatusInternalServerError) 16 | loggerFactory(req.Context()).Error("internal server error", zap.Any("error", err), zap.Stack("stack_trace")) 17 | } 18 | }() 19 | 20 | inner.ServeHTTP(w, req) 21 | }) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /httputils/clone.go: -------------------------------------------------------------------------------- 1 | package httputils 2 | 3 | import ( 4 | "net/http" 5 | "net/url" 6 | ) 7 | 8 | func CloneRequest(r *http.Request) *http.Request { 9 | if r == nil { 10 | panic("nil Request") 11 | } 12 | 13 | r2 := new(http.Request) 14 | *r2 = *r 15 | 16 | // Deep copy the URL because it isn't 17 | // a map and the URL is mutable by users 18 | // of WithContext. 19 | if r.URL != nil { 20 | r2URL := new(url.URL) 21 | *r2URL = *r.URL 22 | r2.URL = r2URL 23 | } 24 | 25 | r2.Header = CloneHeader(r.Header) 26 | 27 | return r2 28 | } 29 | 30 | func CloneHeader(h http.Header) http.Header { 31 | h2 := make(http.Header, len(h)) 32 | for k, vv := range h { 33 | vv2 := make([]string, len(vv)) 34 | copy(vv2, vv) 35 | h2[k] = vv2 36 | } 37 | return h2 38 | } 39 | -------------------------------------------------------------------------------- /handler/handler.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/osstotalsoft/bifrost/abstraction" 5 | "github.com/osstotalsoft/bifrost/log" 6 | "net/http" 7 | ) 8 | 9 | //ReverseProxyHandlerType is a handler type, used when registering a reverse proxy handler 10 | const ReverseProxyHandlerType = "reverseproxy" 11 | 12 | //EventPublisherHandlerType is a handler type, used when registering an event handler 13 | //ex: nats, kafka 14 | const EventPublisherHandlerType = "event" 15 | 16 | //Func is a signature that each handler must implement 17 | type Func func(endpoint abstraction.Endpoint, loggerFactory log.Factory) http.Handler 18 | 19 | //Compose Funcs 20 | func Compose(funcs ...func(f Func) Func) func(f Func) Func { 21 | return func(m Func) Func { 22 | for _, f := range funcs { 23 | m = f(m) 24 | } 25 | return m 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /strutils/utils.go: -------------------------------------------------------------------------------- 1 | package strutils 2 | 3 | import "strings" 4 | 5 | //SingleJoiningSlash joins two strings with slashes resulting a single string with one slash 6 | func SingleJoiningSlash(a, b string) string { 7 | aslash := strings.HasSuffix(a, "/") 8 | bslash := strings.HasPrefix(b, "/") 9 | switch { 10 | case aslash && bslash: 11 | return a + b[1:] 12 | case !aslash && !bslash && b != "": 13 | return a + "/" + b 14 | } 15 | return a + b 16 | } 17 | 18 | //Intersection intersects two string arrays 19 | func Intersection(s1, s2 []string) (inter []string) { 20 | hash := make(map[string]bool) 21 | for _, e := range s1 { 22 | hash[e] = true 23 | } 24 | for _, e := range s2 { 25 | // If elements present in the hashmap then append intersection list. 26 | if hash[e] { 27 | inter = append(inter, e) 28 | } 29 | } 30 | return 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 totalsoft 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /middleware/cors/cors.go: -------------------------------------------------------------------------------- 1 | package cors 2 | 3 | import ( 4 | "github.com/osstotalsoft/bifrost/abstraction" 5 | "github.com/osstotalsoft/bifrost/log" 6 | "github.com/osstotalsoft/bifrost/middleware" 7 | "github.com/rs/cors" 8 | "net/http" 9 | ) 10 | 11 | //CORSFilterCode is the code used to register this middleware 12 | const CORSFilterCode = "cors" 13 | 14 | //AuthorizationOptions are the options configured for all endpoints 15 | type Options struct { 16 | AllowedOrigins []string `mapstructure:"allowed_origins"` 17 | } 18 | 19 | // CORSFilter provides Cross-Origin Resource Sharing middleware. 20 | // using RS cors handlers 21 | func CORSFilter(options Options) middleware.Func { 22 | return func(endpoint abstraction.Endpoint, loggerFactory log.Factory) func(http.Handler) http.Handler { 23 | 24 | c := cors.New(cors.Options{ 25 | AllowedOrigins: options.AllowedOrigins, 26 | AllowedMethods: []string{ 27 | http.MethodHead, 28 | http.MethodGet, 29 | http.MethodPost, 30 | http.MethodPut, 31 | http.MethodPatch, 32 | http.MethodDelete, 33 | http.MethodOptions, 34 | }, 35 | AllowedHeaders: []string{"*"}, 36 | AllowCredentials: true, 37 | }) 38 | 39 | return c.Handler 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /servicediscovery/registry/inmemory/concurrentmap.go: -------------------------------------------------------------------------------- 1 | package inmemory 2 | 3 | import ( 4 | "github.com/osstotalsoft/bifrost/servicediscovery" 5 | "sync" 6 | ) 7 | 8 | type inMemoryRegistryData struct { 9 | m sync.RWMutex 10 | internal map[string]servicediscovery.Service 11 | } 12 | 13 | func NewInMemoryStore() *inMemoryRegistryData { 14 | return &inMemoryRegistryData{ 15 | internal: make(map[string]servicediscovery.Service), 16 | } 17 | } 18 | 19 | func (rm *inMemoryRegistryData) Load(key string) (value servicediscovery.Service, ok bool) { 20 | rm.m.RLock() 21 | result, ok := rm.internal[key] 22 | rm.m.RUnlock() 23 | return result, ok 24 | } 25 | 26 | func (rm *inMemoryRegistryData) GetAll() []servicediscovery.Service { 27 | rm.m.RLock() 28 | var result []servicediscovery.Service 29 | for _, value := range rm.internal { 30 | result = append(result, value) 31 | } 32 | rm.m.RUnlock() 33 | return result 34 | } 35 | 36 | func (rm *inMemoryRegistryData) Delete(key string) { 37 | rm.m.Lock() 38 | delete(rm.internal, key) 39 | rm.m.Unlock() 40 | } 41 | 42 | func (rm *inMemoryRegistryData) Store(key string, value servicediscovery.Service) { 43 | rm.m.Lock() 44 | rm.internal[key] = value 45 | rm.m.Unlock() 46 | } 47 | -------------------------------------------------------------------------------- /router/route.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "net/http" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | //RouteMatcherFunc type signature that any route matcher have to implement 10 | type RouteMatcherFunc func(route Route) func(request *http.Request) RouteMatch 11 | 12 | //Route stores the information about a certain route 13 | type Route struct { 14 | UID string 15 | Path string 16 | PathPrefix string 17 | Methods []string 18 | Timeout time.Duration 19 | matcher func(request *http.Request) RouteMatch 20 | handler http.Handler 21 | } 22 | 23 | //RouteMatch is the result of a route matching 24 | type RouteMatch struct { 25 | Matched bool 26 | Vars map[string]string 27 | } 28 | 29 | func (r Route) String() string { 30 | return r.PathPrefix + r.Path 31 | } 32 | 33 | //MatchRoute checks all the route if they match the incoming request 34 | func MatchRoute(routes *sync.Map, request *http.Request) (Route, RouteMatch) { 35 | 36 | var resRM RouteMatch 37 | var resR Route 38 | 39 | routes.Range(func(key, value interface{}) bool { 40 | r := value.(Route) 41 | rm := r.matcher(request) 42 | if rm.Matched { 43 | resRM = RouteMatch{rm.Matched, rm.Vars} 44 | resR = r 45 | return false 46 | } 47 | return true 48 | }) 49 | 50 | return resR, resRM 51 | } 52 | -------------------------------------------------------------------------------- /handler/reverseproxy/modifiers.go: -------------------------------------------------------------------------------- 1 | package reverseproxy 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "github.com/osstotalsoft/bifrost/abstraction" 7 | "net/http" 8 | ) 9 | 10 | //ClearCorsHeaders deletes cors headers from upstream service 11 | func ClearCorsHeaders(response *http.Response) error { 12 | //hack when upstream service has cors enabled; cors will be handled by the gateway 13 | response.Header.Del("Access-Control-Allow-Origin") 14 | response.Header.Del("Access-Control-Allow-Credentials") 15 | response.Header.Del("Access-Control-Allow-Methods") 16 | response.Header.Del("Access-Control-Allow-Headers") 17 | return nil 18 | } 19 | 20 | //AddUserIdToHeader puts userId claim to request header 21 | func AddUserIdToHeader(req *http.Request) error { 22 | claims, err := getClaims(req.Context()) 23 | if err == nil { 24 | if sub, ok := claims["sub"]; ok { 25 | req.Header.Add(abstraction.HttpUserIdHeader, sub.(string)) 26 | } 27 | } 28 | return nil 29 | } 30 | 31 | //getClaims get the claims map stored in the context 32 | func getClaims(context context.Context) (map[string]interface{}, error) { 33 | claims, ok := context.Value(abstraction.ContextClaimsKey).(map[string]interface{}) 34 | if !ok { 35 | return nil, errors.New("claims not present or not authenticated") 36 | } 37 | 38 | return claims, nil 39 | } 40 | -------------------------------------------------------------------------------- /tracing/round_tripper.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "github.com/opentracing/opentracing-go" 5 | "github.com/opentracing/opentracing-go/ext" 6 | "github.com/opentracing/opentracing-go/log" 7 | "net/http" 8 | ) 9 | 10 | type roundTripper struct { 11 | http.RoundTripper 12 | } 13 | 14 | //NewRoundTripperWithOpenTrancing creates a new roundTripper with OpenTracing 15 | func NewRoundTripperWithOpenTrancing() *roundTripper { 16 | return &roundTripper{RoundTripper: http.DefaultTransport} 17 | } 18 | 19 | //RoundTrip starts a opentracing span and then delegates the request to the actual roundtripper 20 | func (rt *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) { 21 | sp, _ := opentracing.StartSpanFromContext(req.Context(), "RoundTrip to "+req.URL.String()) 22 | defer sp.Finish() 23 | 24 | ext.SpanKindRPCClient.Set(sp) 25 | ext.Component.Set(sp, "RoundTripper") 26 | 27 | ext.HTTPMethod.Set(sp, req.Method) 28 | ext.HTTPUrl.Set(sp, req.URL.String()) 29 | 30 | carrier := opentracing.HTTPHeadersCarrier(req.Header) 31 | _ = sp.Tracer().Inject(sp.Context(), opentracing.HTTPHeaders, carrier) 32 | 33 | resp, err := rt.RoundTripper.RoundTrip(req) 34 | if resp != nil { 35 | ext.HTTPStatusCode.Set(sp, uint16(resp.StatusCode)) 36 | } else { 37 | sp.SetTag("error", true) 38 | sp.LogFields(log.Error(err)) 39 | } 40 | 41 | return resp, err 42 | } 43 | -------------------------------------------------------------------------------- /handler/nats/options.go: -------------------------------------------------------------------------------- 1 | package nats 2 | 3 | import ( 4 | "context" 5 | "github.com/osstotalsoft/bifrost/log" 6 | "go.uber.org/zap" 7 | ) 8 | 9 | type Option func(Config) Config 10 | 11 | //NoTransformation is a no op function 12 | func NoTransformation(messageContext messageContext, requestContext context.Context, payloadBytes []byte) (bytes []byte, e error) { 13 | return payloadBytes, nil 14 | } 15 | 16 | //EmptyResponse returns a empty byte[] 17 | func EmptyResponse(messageContext messageContext, requestContext context.Context) (bytes []byte, e error) { 18 | return nil, nil 19 | } 20 | 21 | //TransformMessage adds a TransformMessageFunc to config 22 | func TransformMessage(f TransformMessageFunc) Option { 23 | return func(config Config) Config { 24 | config.transformMessageFunc = f 25 | return config 26 | } 27 | } 28 | 29 | //BuildResponse adds a BuildResponseFunc to config 30 | func BuildResponse(f BuildResponseFunc) Option { 31 | return func(config Config) Config { 32 | config.buildResponseFunc = f 33 | return config 34 | } 35 | } 36 | 37 | //Logger adds a logger to config 38 | func Logger(logger log.Logger) Option { 39 | return func(config Config) Config { 40 | config.logger = logger.With(zap.String("handler", "nats")) 41 | return config 42 | } 43 | } 44 | 45 | func applyOptions(config Config, opts []Option) Config { 46 | for _, opt := range opts { 47 | config = opt(config) 48 | } 49 | 50 | return config 51 | } 52 | -------------------------------------------------------------------------------- /middleware/ratelimit/limiter.go: -------------------------------------------------------------------------------- 1 | package ratelimit 2 | 3 | import ( 4 | "fmt" 5 | "github.com/osstotalsoft/bifrost/abstraction" 6 | "github.com/osstotalsoft/bifrost/log" 7 | "github.com/osstotalsoft/bifrost/middleware" 8 | "golang.org/x/time/rate" 9 | "net/http" 10 | ) 11 | 12 | const RateLimitingFilterCode = "ratelimit" 13 | 14 | //DefaultGlobalRequestLimit defines max nr of request / route / second 15 | const DefaultGlobalRequestLimit = 5000 16 | const MaxRequestLimit = 10000 17 | 18 | //RateLimiting is a middleware which can limit the number of request / route / second 19 | //and then return StatusTooManyRequests response if the limit is reached 20 | func RateLimiting(limit int) middleware.Func { 21 | return func(endpoint abstraction.Endpoint, loggerFactory log.Factory) func(http.Handler) http.Handler { 22 | limiter := rate.NewLimiter(rate.Limit(limit), limit) 23 | 24 | return func(next http.Handler) http.Handler { 25 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 26 | setResponseHeaders(limiter.Limit(), w, r) 27 | 28 | if limiter.Allow() == false { 29 | http.Error(w, http.StatusText(http.StatusTooManyRequests), http.StatusTooManyRequests) 30 | return 31 | } 32 | 33 | next.ServeHTTP(w, r) 34 | }) 35 | } 36 | } 37 | } 38 | 39 | func setResponseHeaders(lmt rate.Limit, w http.ResponseWriter, r *http.Request) { 40 | w.Header().Add("X-Rate-Limit-Limit", fmt.Sprintf("%.2f", lmt)) 41 | w.Header().Add("X-Rate-Limit-Duration", "1") 42 | w.Header().Add("X-Rate-Limit-Request-Forwarded-For", r.Header.Get("X-Forwarded-For")) 43 | w.Header().Add("X-Rate-Limit-Request-Remote-Addr", r.RemoteAddr) 44 | } 45 | -------------------------------------------------------------------------------- /gateway/config.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | //Config is an object loaded from config.json 4 | type Config struct { 5 | Endpoints []EndpointConfig `mapstructure:"endpoints"` 6 | Port int `mapstructure:"port"` 7 | Version string `mapstructure:"version"` 8 | Name string `mapstructure:"name"` 9 | UpstreamPathPrefix string `mapstructure:"upstream_path_prefix"` 10 | DownstreamPathPrefix string `mapstructure:"downstream_path_prefix"` 11 | LogLevel string `mapstructure:"log_level"` 12 | InCluster bool `mapstructure:"in_cluster"` 13 | OverrideServiceAddress string `mapstructure:"override_service_address"` 14 | ServiceNamespacePrefixFilter string `mapstructure:"service_namespace_prefix_filter"` 15 | } 16 | 17 | //EndpointConfig is a configuration detail from config.json 18 | type EndpointConfig struct { 19 | UpstreamPath string `mapstructure:"upstream_path"` 20 | UpstreamPathPrefix string `mapstructure:"upstream_path_prefix"` 21 | DownstreamPath string `mapstructure:"downstream_path"` 22 | DownstreamPathPrefix string `mapstructure:"downstream_path_prefix"` 23 | ServiceName string `mapstructure:"service_name"` 24 | Methods []string `mapstructure:"methods"` 25 | HandlerType string `mapstructure:"handler_type"` 26 | HandlerConfig map[string]interface{} `mapstructure:"handler_config"` 27 | Filters map[string]interface{} `mapstructure:"filters"` 28 | } 29 | -------------------------------------------------------------------------------- /middleware/auth/sample_key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEA4f5wg5l2hKsTeNem/V41fGnJm6gOdrj8ym3rFkEU/wT8RDtn 3 | SgFEZOQpHEgQ7JL38xUfU0Y3g6aYw9QT0hJ7mCpz9Er5qLaMXJwZxzHzAahlfA0i 4 | cqabvJOMvQtzD6uQv6wPEyZtDTWiQi9AXwBpHssPnpYGIn20ZZuNlX2BrClciHhC 5 | PUIIZOQn/MmqTD31jSyjoQoV7MhhMTATKJx2XrHhR+1DcKJzQBSTAGnpYVaqpsAR 6 | ap+nwRipr3nUTuxyGohBTSmjJ2usSeQXHI3bODIRe1AuTyHceAbewn8b462yEWKA 7 | Rdpd9AjQW5SIVPfdsz5B6GlYQ5LdYKtznTuy7wIDAQABAoIBAQCwia1k7+2oZ2d3 8 | n6agCAbqIE1QXfCmh41ZqJHbOY3oRQG3X1wpcGH4Gk+O+zDVTV2JszdcOt7E5dAy 9 | MaomETAhRxB7hlIOnEN7WKm+dGNrKRvV0wDU5ReFMRHg31/Lnu8c+5BvGjZX+ky9 10 | POIhFFYJqwCRlopGSUIxmVj5rSgtzk3iWOQXr+ah1bjEXvlxDOWkHN6YfpV5ThdE 11 | KdBIPGEVqa63r9n2h+qazKrtiRqJqGnOrHzOECYbRFYhexsNFz7YT02xdfSHn7gM 12 | IvabDDP/Qp0PjE1jdouiMaFHYnLBbgvlnZW9yuVf/rpXTUq/njxIXMmvmEyyvSDn 13 | FcFikB8pAoGBAPF77hK4m3/rdGT7X8a/gwvZ2R121aBcdPwEaUhvj/36dx596zvY 14 | mEOjrWfZhF083/nYWE2kVquj2wjs+otCLfifEEgXcVPTnEOPO9Zg3uNSL0nNQghj 15 | FuD3iGLTUBCtM66oTe0jLSslHe8gLGEQqyMzHOzYxNqibxcOZIe8Qt0NAoGBAO+U 16 | I5+XWjWEgDmvyC3TrOSf/KCGjtu0TSv30ipv27bDLMrpvPmD/5lpptTFwcxvVhCs 17 | 2b+chCjlghFSWFbBULBrfci2FtliClOVMYrlNBdUSJhf3aYSG2Doe6Bgt1n2CpNn 18 | /iu37Y3NfemZBJA7hNl4dYe+f+uzM87cdQ214+jrAoGAXA0XxX8ll2+ToOLJsaNT 19 | OvNB9h9Uc5qK5X5w+7G7O998BN2PC/MWp8H+2fVqpXgNENpNXttkRm1hk1dych86 20 | EunfdPuqsX+as44oCyJGFHVBnWpm33eWQw9YqANRI+pCJzP08I5WK3osnPiwshd+ 21 | hR54yjgfYhBFNI7B95PmEQkCgYBzFSz7h1+s34Ycr8SvxsOBWxymG5zaCsUbPsL0 22 | 4aCgLScCHb9J+E86aVbbVFdglYa5Id7DPTL61ixhl7WZjujspeXZGSbmq0Kcnckb 23 | mDgqkLECiOJW2NHP/j0McAkDLL4tysF8TLDO8gvuvzNC+WQ6drO2ThrypLVZQ+ry 24 | eBIPmwKBgEZxhqa0gVvHQG/7Od69KWj4eJP28kq13RhKay8JOoN0vPmspXJo1HY3 25 | CKuHRG+AP579dncdUnOMvfXOtkdM4vk0+hWASBQzM9xzVcztCa+koAugjVaLS9A+ 26 | 9uQoqEeVNTckxx0S2bYevRy7hGQmUJTyQm3j1zEUR5jpdbL83Fbq 27 | -----END RSA PRIVATE KEY----- -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | # This workflow uses actions that are not certified by GitHub. 4 | # They are provided by a third-party and are governed by 5 | # separate terms of service, privacy policy, and support 6 | # documentation. 7 | 8 | on: 9 | release: 10 | types: [published] 11 | workflow_dispatch: 12 | 13 | env: 14 | # Use docker.io for Docker Hub if empty 15 | REGISTRY: ghcr.io 16 | # github.repository as / 17 | IMAGE_NAME: ${{ github.repository }} 18 | 19 | 20 | jobs: 21 | build: 22 | runs-on: ubuntu-latest 23 | permissions: 24 | contents: read 25 | packages: write 26 | env: 27 | ARTIFACT_DIR: ./release 28 | HELM_PACKAGE_DIR: helm 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@v2 32 | 33 | - name: Set up Go 34 | uses: actions/setup-go@v2 35 | with: 36 | go-version: 1.17 37 | 38 | - name: Set release version 39 | run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/v}" >> $GITHUB_ENV 40 | 41 | # Login against a Docker registry except on PR 42 | # https://github.com/docker/login-action 43 | - name: Log into registry ${{ env.REGISTRY }} 44 | uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c 45 | with: 46 | registry: ${{ env.REGISTRY }} 47 | username: ${{ github.actor }} 48 | password: ${{ secrets.GITHUB_TOKEN }} 49 | 50 | - name: Build images 51 | env: 52 | DOCKER_REGISTRY: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 53 | DOCKER_TAG: ${{ env.RELEASE_VERSION }} 54 | run: make docker-build 55 | 56 | - name: Push images 57 | env: 58 | DOCKER_REGISTRY: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 59 | DOCKER_TAG: ${{ env.RELEASE_VERSION }} 60 | run: make docker-push -------------------------------------------------------------------------------- /log/logger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | "go.uber.org/zap/zapcore" 6 | ) 7 | 8 | // Logger is a simplified abstraction of the zap.Logger 9 | type Logger interface { 10 | Info(msg string, fields ...zapcore.Field) 11 | Error(msg string, fields ...zapcore.Field) 12 | Debug(msg string, fields ...zapcore.Field) 13 | Fatal(msg string, fields ...zapcore.Field) 14 | Warn(msg string, fields ...zapcore.Field) 15 | Panic(msg string, fields ...zapcore.Field) 16 | With(fields ...zapcore.Field) Logger 17 | } 18 | 19 | // zapLogger delegates all calls to the underlying zap.Logger 20 | type zapLogger struct { 21 | logger *zap.Logger 22 | } 23 | 24 | // Info logs an info msg with fields 25 | func (l zapLogger) Info(msg string, fields ...zapcore.Field) { 26 | l.logger.Info(msg, fields...) 27 | } 28 | 29 | // Error logs an error msg with fields 30 | func (l zapLogger) Error(msg string, fields ...zapcore.Field) { 31 | l.logger.Error(msg, fields...) 32 | } 33 | 34 | // Fatal logs a fatal error msg with fields 35 | func (l zapLogger) Fatal(msg string, fields ...zapcore.Field) { 36 | l.logger.Fatal(msg, fields...) 37 | } 38 | 39 | // Debug logs a fatal error msg with fields 40 | func (l zapLogger) Debug(msg string, fields ...zapcore.Field) { 41 | l.logger.Debug(msg, fields...) 42 | } 43 | 44 | // Warn logs a fatal error msg with fields 45 | func (l zapLogger) Warn(msg string, fields ...zapcore.Field) { 46 | l.logger.Warn(msg, fields...) 47 | } 48 | 49 | // Panic logs a fatal error msg with fields 50 | func (l zapLogger) Panic(msg string, fields ...zapcore.Field) { 51 | l.logger.Panic(msg, fields...) 52 | } 53 | 54 | // With creates a child logger, and optionally adds some context fields to that logger. 55 | func (l zapLogger) With(fields ...zapcore.Field) Logger { 56 | return zapLogger{logger: l.logger.With(fields...)} 57 | } 58 | 59 | //NewNop return a no op logger 60 | func NewNop() Logger { 61 | return zapLogger{zap.NewNop()} 62 | } 63 | -------------------------------------------------------------------------------- /tracing/wrapper.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "github.com/opentracing-contrib/go-stdlib/nethttp" 5 | "github.com/opentracing/opentracing-go" 6 | "github.com/osstotalsoft/bifrost/abstraction" 7 | "github.com/osstotalsoft/bifrost/handler" 8 | "github.com/osstotalsoft/bifrost/log" 9 | "github.com/osstotalsoft/bifrost/middleware" 10 | "net/http" 11 | ) 12 | 13 | //SpanWrapper is a http.Handler with opentracing 14 | func SpanWrapper(inner http.Handler) http.Handler { 15 | 16 | tracer := opentracing.GlobalTracer() 17 | 18 | //setup opentracing for main handler 19 | return nethttp.Middleware(tracer, inner, nethttp.OperationNameFunc(func(r *http.Request) string { 20 | return "HTTP " + r.Method + ":" + r.URL.Path 21 | }), nethttp.MWSpanObserver(func(span opentracing.Span, r *http.Request) { 22 | span.SetTag("http.uri", r.URL.EscapedPath()) 23 | })) 24 | } 25 | 26 | //MiddlewareSpanWrapper is a middleware.Func with opentracing 27 | func MiddlewareSpanWrapper(operation string) func(inner middleware.Func) middleware.Func { 28 | return func(inner middleware.Func) middleware.Func { 29 | return func(endpoint abstraction.Endpoint, loggerFactory log.Factory) func(http.Handler) http.Handler { 30 | return func(next http.Handler) http.Handler { 31 | return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { 32 | span, ctx := opentracing.StartSpanFromContext(request.Context(), operation) 33 | defer span.Finish() 34 | inner(endpoint, loggerFactory)(next).ServeHTTP(writer, request.WithContext(ctx)) 35 | }) 36 | } 37 | } 38 | } 39 | } 40 | 41 | //HandlerSpanWrapper is a handler.Func with opentracing 42 | func HandlerSpanWrapper(operation string) func(inner handler.Func) handler.Func { 43 | return func(inner handler.Func) handler.Func { 44 | return func(endpoint abstraction.Endpoint, loggerFactory log.Factory) http.Handler { 45 | return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { 46 | span, ctx := opentracing.StartSpanFromContext(request.Context(), operation) 47 | defer span.Finish() 48 | inner(endpoint, loggerFactory).ServeHTTP(writer, request.WithContext(ctx)) 49 | }) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /servicediscovery/provider/testprovider.go: -------------------------------------------------------------------------------- 1 | package provider 2 | 3 | import ( 4 | "github.com/osstotalsoft/bifrost/servicediscovery" 5 | "log" 6 | ) 7 | 8 | //TestProvider is a service discovery provider used for testing 9 | type TestProvider struct { 10 | onRegisterHandlers []servicediscovery.ServiceFunc 11 | onUnRegisterHandlers []servicediscovery.ServiceFunc 12 | } 13 | 14 | //NewTestProvider create a test provider 15 | func NewTestProvider() *TestProvider { 16 | 17 | return &TestProvider{ 18 | onRegisterHandlers: []servicediscovery.ServiceFunc{}, 19 | onUnRegisterHandlers: []servicediscovery.ServiceFunc{}, 20 | } 21 | } 22 | 23 | func Start(provider *TestProvider) *TestProvider { 24 | 25 | callAllSubscribers(provider.onRegisterHandlers, servicediscovery.Service{ 26 | Address: "http://kube-worker1:32344", 27 | Resource: "api1", 28 | Namespace: "gateway", 29 | }) 30 | 31 | callAllSubscribers(provider.onRegisterHandlers, servicediscovery.Service{ 32 | Address: "http://kube-worker1:32684", 33 | Resource: "api2", 34 | Namespace: "gateway", 35 | }) 36 | 37 | callAllSubscribers(provider.onRegisterHandlers, servicediscovery.Service{ 38 | Address: "http://downstream-api-1.gateway/", 39 | Resource: "kube-api1", 40 | Namespace: "gateway", 41 | }) 42 | 43 | callAllSubscribers(provider.onRegisterHandlers, servicediscovery.Service{ 44 | Address: "http://downstream-api-2.gateway/", 45 | Resource: "kube-api2", 46 | Namespace: "gateway", 47 | }) 48 | 49 | callAllSubscribers(provider.onRegisterHandlers, servicediscovery.Service{ 50 | Address: "http://localhost:64307", 51 | Resource: "dealers", 52 | Namespace: "lsng", 53 | }) 54 | 55 | callAllSubscribers(provider.onRegisterHandlers, servicediscovery.Service{ 56 | Address: "http://localhost:64307/", 57 | Resource: "countries", 58 | Namespace: "lsng", 59 | }) 60 | 61 | callAllSubscribers(provider.onRegisterHandlers, servicediscovery.Service{ 62 | Address: "http://localhost:64307", 63 | Resource: "lsng-api", 64 | Namespace: "lsng", 65 | }) 66 | 67 | return provider 68 | } 69 | 70 | func callAllSubscribers(handlers []servicediscovery.ServiceFunc, service servicediscovery.Service) { 71 | 72 | log.Printf("Added new service: %v", service) 73 | 74 | for _, fn := range handlers { 75 | fn(service) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "1", 3 | "name": "Bifrost api gateway", 4 | "port": 8000, 5 | "in_cluster": false, 6 | "override_service_address": "http://kube-worker1:32344/", 7 | "log_level": "debug", 8 | "service_namespace_prefix_filter": "", 9 | "metrics": { 10 | "enabled": true, 11 | "collection_time": "60s", 12 | "proxy_disabled": false, 13 | "router_disabled": false, 14 | "backend_disabled": false, 15 | "endpoint_disabled": false, 16 | "listen_address": "8090" 17 | }, 18 | "downstream_path_prefix": "", 19 | "upstream_path_prefix": "/api", 20 | "endpoints": [ 21 | { 22 | "service_name": "downstream-api-1", 23 | "downstream_path_prefix": "/messaging/offers/computeOfferStates", 24 | "handler_type": "event", 25 | "handler_config": { 26 | "topic": "ch.commands.Charisma.Leasing.PublishedLanguage.Commands.LeasingOffer.ComputeOffertNextStatesList" 27 | }, 28 | "methods": [ 29 | "POST" 30 | ] 31 | }, 32 | { 33 | "service_name": "downstream-api-2", 34 | "upstream_path_prefix": "/downstream-api-2", 35 | "methods": [ 36 | "POST", 37 | "GET" 38 | ], 39 | "filters": { 40 | "auth": { 41 | "disabled": false, 42 | "allowed_scopes": [ 43 | "LSNG.Api.read_only", 44 | "Notifier.Api.write" 45 | ], 46 | "claims_requirement": { 47 | "client_id": "CharismaFinancialServices" 48 | } 49 | }, 50 | "rate_limit": { 51 | "limit": 500 52 | } 53 | } 54 | }, 55 | { 56 | "service_name": "downstream-api-1", 57 | "upstream_path_prefix": "/api", 58 | "filters": { 59 | "auth": { 60 | "disabled": true, 61 | "allowed_scopes": [ 62 | "LSNG.Api.read_only" 63 | ] 64 | } 65 | } 66 | }, 67 | { 68 | "service_name": "hubs", 69 | "upstream_path_prefix": "/hubs" 70 | }, 71 | { 72 | "service_name": "lsng-api", 73 | "filters": { 74 | "auth": { 75 | "allowed_scopes": [ 76 | "LSNG.Api.read_only" 77 | ] 78 | } 79 | } 80 | } 81 | ], 82 | "handlers": { 83 | "event": { 84 | "nats": { 85 | "nats_url": "nats://kube-worker1:31291", 86 | "cluster": "faas-cluster", 87 | "client_id": "GoGatewayClientId2", 88 | "q_group": "GoGateway", 89 | "durable_name": "durable", 90 | "topic_prefix": "LSNG_LIVIU_", 91 | "source": "GoGateway" 92 | } 93 | } 94 | }, 95 | "filters": { 96 | "auth": { 97 | "authority": "https://leasing-sso.appservice.online" 98 | }, 99 | "cors": { 100 | "allowed_origins": [ 101 | "http://localhost:3000", 102 | "https://leasing-app.appservice.online", 103 | "https://lsng.appservice.online" 104 | ] 105 | }, 106 | "rate_limit": { 107 | "enabled": false, 108 | "limit": 5000 109 | } 110 | }, 111 | "opentracing": { 112 | "enabled": true, 113 | "agent": "kube-worker1:31457" 114 | } 115 | } -------------------------------------------------------------------------------- /router/dynamicrouter.go: -------------------------------------------------------------------------------- 1 | package router 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/osstotalsoft/bifrost/log" 8 | "github.com/satori/go.uuid" 9 | "go.uber.org/zap" 10 | "net/http" 11 | "sync" 12 | ) 13 | 14 | type dynamicRouter struct { 15 | routes *sync.Map 16 | routeMatcher RouteMatcherFunc 17 | logger log.Logger 18 | } 19 | 20 | //NewDynamicRouter creates a new dynamic router 21 | //Its dynamic because it can add/remove routes at runtime 22 | //this router does not do any route matching, it relies on third parties for that 23 | func NewDynamicRouter(routeMatcher RouteMatcherFunc, loggerFactory log.Factory) *dynamicRouter { 24 | return &dynamicRouter{ 25 | new(sync.Map), 26 | routeMatcher, 27 | loggerFactory(nil), 28 | } 29 | } 30 | 31 | //GetHandler returns the router http.Handler 32 | func GetHandler(router *dynamicRouter) http.Handler { 33 | return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { 34 | route, routeMatch := MatchRoute(router.routes, request) 35 | if !routeMatch.Matched { 36 | http.NotFound(writer, request) 37 | return 38 | } 39 | 40 | ctx := context.WithValue(request.Context(), ContextRouteKey, RouteContext{ 41 | route.Path, 42 | route.PathPrefix, 43 | route.Timeout, 44 | routeMatch.Vars, 45 | }) 46 | 47 | route.handler.ServeHTTP(writer, request.WithContext(ctx)) 48 | }) 49 | } 50 | 51 | //AddRoute adds a new route 52 | func AddRoute(router *dynamicRouter) func(path, pathPrefix string, methods []string, handler http.Handler) (string, error) { 53 | return func(path, pathPrefix string, methods []string, handler http.Handler) (string, error) { 54 | route := Route{ 55 | Path: path, 56 | PathPrefix: pathPrefix, 57 | Methods: methods, 58 | handler: handler, 59 | UID: uuid.NewV4().String(), 60 | } 61 | 62 | route.matcher = router.routeMatcher(route) 63 | err := validateRoute(router, route) 64 | if err != nil { 65 | router.logger.Error("invalid route", zap.Error(err)) 66 | return "", err 67 | } 68 | 69 | router.routes.Store(route.UID, route) 70 | router.logger.Info(fmt.Sprintf("DynamicRouter: Added new route: id: %s; pathPrefix: %s; path %s", route.UID, route.PathPrefix, route.Path)) 71 | return route.UID, nil 72 | } 73 | } 74 | 75 | func validateRoute(router *dynamicRouter, route Route) error { 76 | err := error(nil) 77 | 78 | //check for multiple registrations 79 | router.routes.Range(func(key, value interface{}) bool { 80 | r := value.(Route) 81 | if r.String() == route.String() { 82 | err = errors.New("DynamicRouter: multiple registrations for : " + route.String()) 83 | return false 84 | } 85 | return true 86 | }) 87 | 88 | return err 89 | } 90 | 91 | //RemoveRoute removes a route 92 | func RemoveRoute(router *dynamicRouter) func(routeId string) { 93 | return func(routeId string) { 94 | route, ok := router.routes.Load(routeId) 95 | if !ok { 96 | router.logger.Error("DynamicRouter: Route does not exist " + routeId) 97 | } 98 | 99 | router.routes.Delete(routeId) 100 | router.logger.Info(fmt.Sprintf("DynamicRouter: Deleted route id: %s; pathPrefix: %s; path %s", route.(Route).UID, route.(Route).PathPrefix, route.(Route).Path)) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/osstotalsoft/bifrost 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/golang-jwt/jwt/v4 v4.4.2 7 | github.com/gorilla/mux v1.8.0 8 | github.com/mitchellh/mapstructure v1.5.0 9 | github.com/nats-io/stan.go v0.10.3 10 | github.com/opentracing-contrib/go-stdlib v1.0.0 11 | github.com/opentracing/opentracing-go v1.2.0 12 | github.com/osstotalsoft/oidc-jwt-go v0.0.0-20220214041528-1f0373671812 13 | github.com/rs/cors v1.8.2 14 | github.com/satori/go.uuid v1.2.0 15 | github.com/spf13/viper v1.12.0 16 | github.com/uber/jaeger-client-go v2.30.0+incompatible 17 | go.uber.org/zap v1.23.0 18 | golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 19 | k8s.io/api v0.23.8 20 | k8s.io/apimachinery v0.23.8 21 | k8s.io/client-go v0.23.8 22 | ) 23 | 24 | require ( 25 | github.com/HdrHistogram/hdrhistogram-go v1.1.2 // indirect 26 | github.com/davecgh/go-spew v1.1.1 // indirect 27 | github.com/fsnotify/fsnotify v1.5.4 // indirect 28 | github.com/go-logr/logr v1.2.0 // indirect 29 | github.com/gogo/protobuf v1.3.2 // indirect 30 | github.com/golang/protobuf v1.5.2 // indirect 31 | github.com/google/go-cmp v0.5.8 // indirect 32 | github.com/google/gofuzz v1.1.0 // indirect 33 | github.com/googleapis/gnostic v0.5.5 // indirect 34 | github.com/hashicorp/hcl v1.0.0 // indirect 35 | github.com/imdario/mergo v0.3.5 // indirect 36 | github.com/json-iterator/go v1.1.12 // indirect 37 | github.com/magiconair/properties v1.8.6 // indirect 38 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 39 | github.com/modern-go/reflect2 v1.0.2 // indirect 40 | github.com/nats-io/nats-server/v2 v2.8.4 // indirect 41 | github.com/nats-io/nats-streaming-server v0.24.6 // indirect 42 | github.com/nats-io/nats.go v1.16.0 // indirect 43 | github.com/nats-io/nkeys v0.3.0 // indirect 44 | github.com/nats-io/nuid v1.0.1 // indirect 45 | github.com/pelletier/go-toml v1.9.5 // indirect 46 | github.com/pelletier/go-toml/v2 v2.0.1 // indirect 47 | github.com/pkg/errors v0.9.1 // indirect 48 | github.com/spf13/afero v1.8.2 // indirect 49 | github.com/spf13/cast v1.5.0 // indirect 50 | github.com/spf13/jwalterweatherman v1.1.0 // indirect 51 | github.com/spf13/pflag v1.0.5 // indirect 52 | github.com/subosito/gotenv v1.3.0 // indirect 53 | github.com/uber/jaeger-lib v2.4.1+incompatible // indirect 54 | go.uber.org/atomic v1.7.0 // indirect 55 | go.uber.org/multierr v1.6.0 // indirect 56 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect 57 | golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2 // indirect 58 | golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 // indirect 59 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a // indirect 60 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect 61 | golang.org/x/text v0.3.7 // indirect 62 | google.golang.org/appengine v1.6.7 // indirect 63 | google.golang.org/protobuf v1.28.0 // indirect 64 | gopkg.in/inf.v0 v0.9.1 // indirect 65 | gopkg.in/ini.v1 v1.66.4 // indirect 66 | gopkg.in/yaml.v2 v2.4.0 // indirect 67 | gopkg.in/yaml.v3 v3.0.1 // indirect 68 | k8s.io/klog/v2 v2.30.0 // indirect 69 | k8s.io/kube-openapi v0.0.0-20211115234752-e816edb12b65 // indirect 70 | k8s.io/utils v0.0.0-20211116205334-6203023598ed // indirect 71 | sigs.k8s.io/json v0.0.0-20211020170558-c049b76a60c6 // indirect 72 | sigs.k8s.io/structured-merge-diff/v4 v4.2.1 // indirect 73 | sigs.k8s.io/yaml v1.2.0 // indirect 74 | ) 75 | -------------------------------------------------------------------------------- /handler/reverseproxy/reverseproxy.go: -------------------------------------------------------------------------------- 1 | package reverseproxy 2 | 3 | import ( 4 | "fmt" 5 | "github.com/osstotalsoft/bifrost/abstraction" 6 | "github.com/osstotalsoft/bifrost/handler" 7 | "github.com/osstotalsoft/bifrost/log" 8 | "github.com/osstotalsoft/bifrost/router" 9 | "github.com/osstotalsoft/bifrost/strutils" 10 | "go.uber.org/zap" 11 | "net/http" 12 | "net/http/httputil" 13 | "net/url" 14 | "strings" 15 | ) 16 | 17 | type RequestModifier func(r *http.Request) error 18 | type ResponseModifier func(r *http.Response) error 19 | 20 | //NewReverseProxy create a new reverproxy http.Handler for each endpoint 21 | func NewReverseProxy(transport http.RoundTripper, requestModifier RequestModifier, responseModifier ResponseModifier) handler.Func { 22 | return func(endPoint abstraction.Endpoint, loggerFactory log.Factory) http.Handler { 23 | //https://github.com/golang/go/issues/16012 24 | //http.DefaultTransport.(*http.Transport).MaxIdleConnsPerHost = 100 25 | 26 | return &httputil.ReverseProxy{ 27 | Director: getDirector(endPoint.UpstreamURL, endPoint.UpstreamPath, endPoint.UpstreamPathPrefix, loggerFactory, requestModifier), 28 | ModifyResponse: responseModifier, 29 | Transport: transport, 30 | } 31 | } 32 | } 33 | 34 | func getDirector(targetUrl, targetUrlPath, targetUrlPrefix string, loggerFactory log.Factory, requestModifier RequestModifier) func(req *http.Request) { 35 | return func(req *http.Request) { 36 | logger := loggerFactory(req.Context()) 37 | routeContext, ok := router.GetRouteContextFromRequestContext(req.Context()) 38 | if !ok { 39 | logger.Panic("routeContext not found") 40 | } 41 | 42 | if requestModifier != nil { 43 | err := requestModifier(req) 44 | if err != nil { 45 | logger.Panic("Error when calling requestModifier", zap.Error(err)) 46 | return 47 | } 48 | } 49 | 50 | initial := req.URL.String() 51 | target, err := url.Parse(targetUrl) 52 | if err != nil { 53 | logger.Panic("Error when converting to url "+targetUrl, zap.String("target_url", targetUrl)) 54 | return 55 | } 56 | targetQuery := target.RawQuery 57 | req.URL.Scheme = target.Scheme 58 | req.URL.Host = target.Host 59 | req.Host = target.Host 60 | if targetUrlPath == "" { 61 | path := req.URL.RawPath //do not use escapedPath 62 | if path == "" { //if no escaping 63 | path = req.URL.Path 64 | } 65 | req.URL.Path = strutils.SingleJoiningSlash(target.Path, strings.TrimPrefix(path, routeContext.PathPrefix)) 66 | 67 | if targetQuery == "" || req.URL.RawQuery == "" { 68 | req.URL.RawQuery = targetQuery + req.URL.RawQuery 69 | } else { 70 | req.URL.RawQuery = targetQuery + "&" + req.URL.RawQuery 71 | } 72 | } else { 73 | req.URL.Path = target.Path 74 | req.URL.RawQuery = targetQuery 75 | } 76 | 77 | req.URL.Path = replaceVarsInTarget(req.URL.Path, routeContext.Vars) 78 | req.URL.RawQuery = replaceVarsInTarget(req.URL.RawQuery, routeContext.Vars) 79 | 80 | if _, ok := req.Header["User-Agent"]; !ok { 81 | // explicitly disable User-Agent so it's not set to default value 82 | req.Header.Set("User-Agent", "") 83 | } 84 | 85 | logger.Debug(fmt.Sprintf("Forwarding request from %v to %v", initial, req.URL.String())) 86 | } 87 | } 88 | 89 | func replaceVarsInTarget(targetUrl string, vars map[string]string) string { 90 | for key, val := range vars { 91 | targetUrl = strings.Replace(targetUrl, "{"+key+"}", val, 1) 92 | } 93 | 94 | return targetUrl 95 | } 96 | -------------------------------------------------------------------------------- /middleware/auth/auth_test.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "github.com/golang-jwt/jwt/v4" 5 | "github.com/golang-jwt/jwt/v4/test" 6 | "github.com/osstotalsoft/bifrost/abstraction" 7 | "github.com/osstotalsoft/bifrost/log" 8 | "github.com/osstotalsoft/oidc-jwt-go" 9 | "go.uber.org/zap" 10 | "io" 11 | "net/http" 12 | "net/http/httptest" 13 | "testing" 14 | ) 15 | 16 | var testEndPoint = abstraction.Endpoint{ 17 | Secured: true, 18 | Filters: map[string]interface{}{ 19 | "auth": AuthorizationEndpointOptions{ 20 | ClaimsRequirement: map[string]string{ 21 | "client_id": "CharismaFinancialServices", 22 | }, 23 | Audience: "LSNG.Api", 24 | AllowedScopes: []string{"LSNG.Api.read_only", "Notifier.Api.write"}, 25 | }, 26 | }, 27 | } 28 | 29 | var intentityConfig = AuthorizationOptions{ 30 | Authority: "http://kube-worker1:30692", 31 | } 32 | 33 | var claims = jwt.MapClaims{ 34 | "iss": "http://kube-worker1:30692", 35 | "aud": []string{ 36 | "http://kube-worker1:30692/resources", 37 | "LSNG.Api", 38 | "Notifier.Api", 39 | }, 40 | "client_id": "CharismaFinancialServices", 41 | "sub": "c8124881-ad67-443e-9473-08d5777d1ba8", 42 | "idp": "local", 43 | "partner_id": "-100", 44 | "charisma_user_id": "1", 45 | "scope": []string{ 46 | "openid", 47 | "profile", 48 | "roles", 49 | "LSNG.Api.read_only", 50 | "charisma_data", 51 | "Notifier.Api.write", 52 | }, 53 | "amr": []string{ 54 | "pwd", 55 | }, 56 | } 57 | 58 | func TestAuthorizationFilter(t *testing.T) { 59 | privateKey := test.LoadRSAPrivateKeyFromDisk("sample_key") 60 | publicKey := test.LoadRSAPublicKeyFromDisk("sample_key.pub") 61 | intentityConfig.SecretProvider = oidc.NewKeyProvider(publicKey) 62 | 63 | token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) 64 | tokenString, _ := token.SignedString(privateKey) 65 | 66 | logger, _ := zap.NewDevelopment() 67 | filter := AuthorizationFilter(intentityConfig)(testEndPoint, log.ZapLoggerFactory(logger)) 68 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 69 | _, _ = io.WriteString(w, "OK") 70 | }) 71 | req := httptest.NewRequest("GET", "/whatever", nil) 72 | req.Header.Add("Authorization", "Bearer "+tokenString) 73 | w := httptest.NewRecorder() 74 | filter(handler).ServeHTTP(w, req) 75 | result := w.Result() 76 | 77 | if result.StatusCode != http.StatusOK { 78 | t.Error("request failed status: ", result.StatusCode) 79 | } 80 | } 81 | 82 | func BenchmarkAuthorizationFilter(b *testing.B) { 83 | privateKey := test.LoadRSAPrivateKeyFromDisk("sample_key") 84 | publicKey := test.LoadRSAPublicKeyFromDisk("sample_key.pub") 85 | intentityConfig.SecretProvider = oidc.NewKeyProvider(publicKey) 86 | 87 | token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims) 88 | tokenString, _ := token.SignedString(privateKey) 89 | 90 | logger, _ := zap.NewDevelopment() 91 | filter := AuthorizationFilter(intentityConfig)(testEndPoint, log.ZapLoggerFactory(logger)) 92 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 93 | _, _ = io.WriteString(w, "OK") 94 | }) 95 | req := httptest.NewRequest("GET", "/whatever", nil) 96 | req.Header.Add("Authorization", "Bearer "+tokenString) 97 | w := httptest.NewRecorder() 98 | 99 | b.ReportAllocs() 100 | b.ResetTimer() 101 | 102 | for i := 0; i < b.N; i++ { 103 | filter(handler).ServeHTTP(w, req) 104 | w.Result() 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /handler/nats/nbbtransformer_test.go: -------------------------------------------------------------------------------- 1 | package nats 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "github.com/osstotalsoft/bifrost/abstraction" 7 | "github.com/satori/go.uuid" 8 | "testing" 9 | "time" 10 | ) 11 | 12 | func TestTransformMessage(t *testing.T) { 13 | 14 | // Arrange 15 | var payload = map[string]interface{}{ 16 | "myField": "myValue", 17 | } 18 | var payloadBytes, _ = json.Marshal(payload) 19 | var messageContext = messageContext{Source: "src", Headers: map[string]interface{}{}} 20 | 21 | var claimsMap = map[string]interface{}{ 22 | UserIdClaimKey: "user1", 23 | CharismaIdClaimKey: 999, 24 | } 25 | var requestContext = context.WithValue(context.Background(), abstraction.ContextClaimsKey, claimsMap) 26 | var response Message 27 | 28 | // Act 29 | responseBytes, _ := NBBTransformMessage(messageContext, requestContext, payloadBytes) 30 | 31 | // Assert 32 | if err := json.Unmarshal(responseBytes, &response); err != nil { 33 | t.Fatal(err.Error()) 34 | } 35 | 36 | if response.Headers == nil { 37 | t.Fatal("headers not present in the message") 38 | } else { 39 | if userId, ok := response.Headers[UserIdKey]; !ok || userId != "user1" { 40 | t.Fatal(UserIdKey + " header not present in the message") 41 | } 42 | if charismaUserId, ok := response.Headers[CharismaUserIdKey].(float64); !ok || charismaUserId != 999 { 43 | t.Fatal(CharismaUserIdKey + " header not present in the message") 44 | } 45 | if _, ok := response.Headers[CorrelationIdKey]; !ok { 46 | t.Fatal(CorrelationIdKey + " header not present in the message") 47 | } 48 | if _, ok := response.Headers[MessageIdKey]; !ok { 49 | t.Fatal(MessageIdKey + " header not present in the message") 50 | } 51 | if _, ok := response.Headers[PublishTimeKey]; !ok { 52 | t.Fatal(PublishTimeKey + " header not present in the message") 53 | } 54 | if source, ok := response.Headers[SourceKey]; !ok || source != "src" { 55 | t.Fatal(SourceKey + " header not present in the message") 56 | } 57 | } 58 | 59 | if response.Payload == nil { 60 | t.Fatal("payload not present in the message") 61 | } else { 62 | if _, ok := response.Payload[CommandIdKey]; !ok { 63 | t.Fatal(CommandIdKey + " not present in the payload") 64 | } 65 | if metadata, ok := response.Payload[MetadataKey].(map[string]interface{}); !ok { 66 | t.Error(MetadataKey + " header not present in the payload") 67 | if _, ok := metadata[CreationDateKey].(time.Time); !ok { 68 | t.Fatal(CreationDateKey + " metadata not present in the message") 69 | } 70 | } 71 | } 72 | 73 | if _, ok := messageContext.Headers[CommandIdKey]; !ok { 74 | t.Fatal(CommandIdKey + " not present in the message context") 75 | } 76 | 77 | if _, ok := messageContext.Headers[CorrelationIdKey]; !ok { 78 | t.Fatal(CorrelationIdKey + " not present in the message context") 79 | } 80 | } 81 | 82 | func TestBuildResponse(t *testing.T) { 83 | 84 | // Arrange 85 | var correlationId = uuid.NewV4() 86 | var commandId = uuid.NewV4() 87 | 88 | var messageContext = messageContext{Headers: map[string]interface{}{ 89 | CorrelationIdKey: correlationId, 90 | CommandIdKey: commandId, 91 | }} 92 | var requestContext = context.WithValue(context.Background(), abstraction.ContextClaimsKey, nil) 93 | var expectedResponse, _ = json.Marshal(CommandResult{CommandId: commandId, CorrelationId: correlationId}) 94 | 95 | // Act 96 | resp, _ := NBBBuildResponse(messageContext, requestContext) 97 | 98 | // Assert 99 | if string(resp) != string(expectedResponse) { 100 | t.Fatal("Response does not match expected value") 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /middleware/cors/cors_test.go: -------------------------------------------------------------------------------- 1 | package cors 2 | 3 | import ( 4 | "github.com/osstotalsoft/bifrost/abstraction" 5 | "github.com/osstotalsoft/bifrost/log" 6 | "go.uber.org/zap" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | ) 11 | 12 | const ( 13 | corsOptionMethod string = "OPTIONS" 14 | corsAllowOriginHeader string = "Access-Control-Allow-Origin" 15 | corsExposeHeadersHeader string = "Access-Control-Expose-Headers" 16 | corsMaxAgeHeader string = "Access-Control-Max-Age" 17 | corsAllowMethodsHeader string = "Access-Control-Allow-Methods" 18 | corsAllowHeadersHeader string = "Access-Control-Allow-Headers" 19 | corsAllowCredentialsHeader string = "Access-Control-Allow-Credentials" 20 | corsRequestMethodHeader string = "Access-Control-Request-Method" 21 | corsRequestHeadersHeader string = "Access-Control-Request-Headers" 22 | corsOriginHeader string = "Origin" 23 | corsVaryHeader string = "Vary" 24 | corsOriginMatchAll string = "*" 25 | ) 26 | 27 | var endpoint = abstraction.Endpoint{} 28 | 29 | func TestCORSFilter(t *testing.T) { 30 | r := httptest.NewRequest("OPTIONS", "http://www.example.com/", nil) 31 | r.Header.Set(corsOriginHeader, r.URL.String()) 32 | r.Header.Set(corsRequestMethodHeader, "GET") 33 | r.Header.Set(corsRequestHeadersHeader, "Authorization") 34 | 35 | rr := httptest.NewRecorder() 36 | 37 | testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) 38 | logger, _ := zap.NewDevelopment() 39 | options := Options{AllowedOrigins: []string{"http://www.example.com/"}} 40 | CORSFilter(options)(endpoint, log.ZapLoggerFactory(logger))(testHandler).ServeHTTP(rr, r) 41 | 42 | if status := rr.Code; status != http.StatusNoContent { 43 | t.Fatalf("bad status: got %v want %v", status, http.StatusNoContent) 44 | } 45 | 46 | header := rr.Header().Get(corsAllowHeadersHeader) 47 | if header != "Authorization" { 48 | t.Fatalf("bad header: expected Authorization header, got empty header.") 49 | } 50 | 51 | header = rr.Header().Get(corsAllowOriginHeader) 52 | if header != "http://www.example.com/" { 53 | t.Fatalf("bad header: expected Access-Control-Allow-Origin:http://www.example.com/ header, got %s", header) 54 | } 55 | } 56 | 57 | func BenchmarkCORSPreflight(b *testing.B) { 58 | r := httptest.NewRequest("OPTIONS", "http://www.example.com/", nil) 59 | r.Header.Set("Origin", r.URL.String()) 60 | r.Header.Set(corsRequestMethodHeader, "GET") 61 | r.Header.Set(corsRequestHeadersHeader, "Authorization") 62 | 63 | rr := httptest.NewRecorder() 64 | 65 | testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) 66 | logger, _ := zap.NewDevelopment() 67 | options := Options{AllowedOrigins: []string{"http://www.example.com/"}} 68 | h := CORSFilter(options)(endpoint, log.ZapLoggerFactory(logger))(testHandler) 69 | 70 | b.ReportAllocs() 71 | b.ResetTimer() 72 | 73 | for i := 0; i < b.N; i++ { 74 | h.ServeHTTP(rr, r) 75 | if status := rr.Code; status != http.StatusNoContent { 76 | b.Errorf("bad status: got %v want %v", status, http.StatusOK) 77 | } 78 | } 79 | } 80 | 81 | func BenchmarkCORSActualRequest(b *testing.B) { 82 | r := httptest.NewRequest("GET", "http://www.example.com/", nil) 83 | r.Header.Set("Origin", r.URL.String()) 84 | 85 | rr := httptest.NewRecorder() 86 | 87 | testHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) 88 | logger, _ := zap.NewDevelopment() 89 | options := Options{AllowedOrigins: []string{"http://www.example.com/"}} 90 | h := CORSFilter(options)(endpoint, log.ZapLoggerFactory(logger))(testHandler) 91 | 92 | b.ReportAllocs() 93 | b.ResetTimer() 94 | 95 | for i := 0; i < b.N; i++ { 96 | h.ServeHTTP(rr, r) 97 | if status := rr.Code; status != http.StatusOK { 98 | b.Errorf("bad status: got %v want %v", status, http.StatusOK) 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /handler/nats/nbbtransformer.go: -------------------------------------------------------------------------------- 1 | package nats 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "github.com/osstotalsoft/bifrost/abstraction" 8 | "github.com/satori/go.uuid" 9 | "time" 10 | ) 11 | 12 | const ( 13 | CorrelationIdKey = "nbb-correlationId" 14 | MessageIdKey = "nbb-messageId" 15 | PublishTimeKey = "nbb-publishTime" 16 | SourceKey = "nbb-source" 17 | CommandIdKey = "CommandId" 18 | UserIdKey = "UserId" 19 | CharismaUserIdKey = "CharismaUserId" 20 | MetadataKey = "Metadata" 21 | CreationDateKey = "CreationDate" 22 | UserIdClaimKey = "sub" 23 | CharismaIdClaimKey = "charisma_user_id" 24 | ) 25 | 26 | //Message is the structure of the message envelope to be published 27 | type Message struct { 28 | Headers map[string]interface{} 29 | Payload map[string]interface{} 30 | } 31 | 32 | //CommandResult is the structure to be returned in the HTTP response 33 | type CommandResult struct { 34 | CommandId uuid.UUID 35 | CorrelationId uuid.UUID 36 | } 37 | 38 | //TransformMessage transforms a message received in the HTTP request to a format required by the NBB infrastructure. 39 | // It envelopes the message adding the required metadata such as UserId, CorrelationId, MessageId, PublishTime, Source, etc. 40 | func NBBTransformMessage(messageContext messageContext, requestContext context.Context, payloadBytes []byte) ([]byte, error) { 41 | claims, err := getClaims(requestContext) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | userId, ok := claims[UserIdClaimKey] 47 | if !ok { 48 | return nil, errors.New(UserIdClaimKey + " claim not found") 49 | } 50 | 51 | charismaUserId, ok := claims[CharismaIdClaimKey] 52 | if !ok { 53 | return nil, errors.New(CharismaIdClaimKey + " claim not found") 54 | } 55 | 56 | correlationId := uuid.NewV4() 57 | commandId := uuid.NewV4() 58 | now := time.Now() 59 | 60 | headers := map[string]interface{}{ 61 | UserIdKey: userId, 62 | CharismaUserIdKey: charismaUserId, 63 | CorrelationIdKey: correlationId, 64 | MessageIdKey: uuid.NewV4(), 65 | SourceKey: messageContext.Source, 66 | PublishTimeKey: now, 67 | } 68 | payloadChanges := map[string]interface{}{ 69 | CommandIdKey: commandId, 70 | MetadataKey: map[string]interface{}{CreationDateKey: now}, 71 | } 72 | 73 | messageContext.Headers[CorrelationIdKey] = correlationId 74 | messageContext.Headers[CommandIdKey] = commandId 75 | 76 | return envelopeMessage(payloadBytes, headers, payloadChanges), nil 77 | } 78 | 79 | //BuildResponse builds the response that is returned by the Gateway after publishing a message 80 | func NBBBuildResponse(messageContext messageContext, requestContext context.Context) ([]byte, error) { 81 | 82 | correlationId, ok := messageContext.Headers[CorrelationIdKey].(uuid.UUID) 83 | if !ok { 84 | return nil, errors.New("correlation id not found in message context") 85 | } 86 | 87 | commandId, ok := messageContext.Headers[CommandIdKey].(uuid.UUID) 88 | if !ok { 89 | return nil, errors.New("command id not found in message context") 90 | } 91 | 92 | responseBytes, err := json.Marshal(CommandResult{CommandId: commandId, CorrelationId: correlationId}) 93 | 94 | return responseBytes, err 95 | } 96 | 97 | //getClaims get the claims map stored in the context 98 | func getClaims(context context.Context) (map[string]interface{}, error) { 99 | claims, ok := context.Value(abstraction.ContextClaimsKey).(map[string]interface{}) 100 | if !ok { 101 | return nil, errors.New("claims not present or not authenticated") 102 | } 103 | 104 | return claims, nil 105 | } 106 | 107 | //envelopeMessage envelopes a message payload with the headers specified and applies changes/additions to the payload 108 | func envelopeMessage(payloadBytes []byte, headers, payloadChanges map[string]interface{}) []byte { 109 | 110 | var payload map[string]interface{} 111 | 112 | _ = json.Unmarshal(payloadBytes, &payload) 113 | message := Message{ 114 | Headers: headers, 115 | Payload: payload, 116 | } 117 | 118 | for k, v := range payloadChanges { 119 | payload[k] = v 120 | } 121 | 122 | envelopeBytes, _ := json.Marshal(message) 123 | 124 | return envelopeBytes 125 | } 126 | -------------------------------------------------------------------------------- /gateway/gateway_test.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | "github.com/osstotalsoft/bifrost/abstraction" 5 | "github.com/osstotalsoft/bifrost/log" 6 | "github.com/osstotalsoft/bifrost/servicediscovery" 7 | "go.uber.org/zap" 8 | "net/http" 9 | "testing" 10 | ) 11 | 12 | type gateTest struct { 13 | title string 14 | service servicediscovery.Service 15 | expectedPath string 16 | expectedPathPrefix string 17 | expectedDestination string 18 | } 19 | 20 | var ( 21 | testConfig1 = Config{ 22 | DownstreamPathPrefix: "", 23 | UpstreamPathPrefix: "/api", 24 | Endpoints: []EndpointConfig{ 25 | { 26 | UpstreamPathPrefix: "/api/v1", 27 | DownstreamPathPrefix: "/users", 28 | DownstreamPath: "", 29 | UpstreamPath: "", 30 | ServiceName: "users", 31 | Methods: nil, 32 | }, 33 | { 34 | UpstreamPathPrefix: "/api/v2", 35 | DownstreamPathPrefix: "/dealers2", 36 | DownstreamPath: "", 37 | UpstreamPath: "", 38 | ServiceName: "dealers", 39 | Methods: nil, 40 | }, 41 | { 42 | UpstreamPathPrefix: "/api/offers", 43 | DownstreamPathPrefix: "", 44 | DownstreamPath: "", 45 | UpstreamPath: "", 46 | ServiceName: "offers", 47 | Methods: nil, 48 | }, 49 | { 50 | UpstreamPathPrefix: "/api/offers", 51 | DownstreamPathPrefix: "/offers2", 52 | DownstreamPath: "/add_offer/{id}", 53 | UpstreamPath: "/add/{id}", 54 | ServiceName: "offers3", 55 | Methods: []string{"POST"}, 56 | }, 57 | { 58 | UpstreamPathPrefix: "/", 59 | DownstreamPathPrefix: "/offers4", 60 | DownstreamPath: "", 61 | UpstreamPath: "", 62 | ServiceName: "offers4", 63 | }, 64 | }, 65 | } 66 | 67 | testCases1 = []gateTest{ 68 | { 69 | title: "serviceWithPrefixNoPath", 70 | service: servicediscovery.Service{ 71 | Resource: "users", 72 | Namespace: "app", 73 | Address: "http://users.app:80/", 74 | Secured: false}, 75 | expectedPath: "", 76 | expectedPathPrefix: "/users", 77 | expectedDestination: "http://users.app:80/api/v1", 78 | }, 79 | { 80 | title: "serviceWithDefaults", 81 | service: servicediscovery.Service{ 82 | Resource: "partners", 83 | Namespace: "app", 84 | Address: "http://partners.app:80/", 85 | Secured: false}, 86 | expectedPath: "", 87 | expectedPathPrefix: "/partners", 88 | expectedDestination: "http://partners.app:80/api", 89 | }, 90 | { 91 | title: "serviceWithPrefix2", 92 | service: servicediscovery.Service{ 93 | Resource: "dealers", 94 | Namespace: "app", 95 | Address: "http://dealers.app:80/", 96 | Secured: false}, 97 | expectedPath: "", 98 | expectedPathPrefix: "/dealers2", 99 | expectedDestination: "http://dealers.app:80/api/v2", 100 | }, 101 | { 102 | title: "serviceWithPrefix3", 103 | service: servicediscovery.Service{ 104 | Resource: "offers", 105 | Namespace: "app", 106 | Address: "http://offers.app:80/", 107 | Secured: false}, 108 | expectedPath: "", 109 | expectedPathPrefix: "/offers", 110 | expectedDestination: "http://offers.app:80/api/offers", 111 | }, 112 | { 113 | title: "serviceWithPrefix4", 114 | service: servicediscovery.Service{ 115 | Resource: "offers3", 116 | Namespace: "app", 117 | Address: "http://offers3.app:80/", 118 | Secured: false}, 119 | expectedPath: "/add_offer/{id}", 120 | expectedPathPrefix: "/offers2", 121 | expectedDestination: "http://offers3.app:80/api/offers/add/{id}", 122 | }, 123 | { 124 | title: "serviceWithPrefix5", 125 | service: servicediscovery.Service{ 126 | Resource: "offers4", 127 | Namespace: "app", 128 | Address: "http://offers4.app:80/", 129 | Secured: false}, 130 | expectedPath: "", 131 | expectedPathPrefix: "/offers4", 132 | expectedDestination: "http://offers4.app:80/", 133 | }, 134 | } 135 | ) 136 | 137 | func TestAddService(t *testing.T) { 138 | logger, _ := zap.NewDevelopment() 139 | factory := log.ZapLoggerFactory(logger) 140 | gate := NewGateway(&testConfig1, factory) 141 | RegisterHandler(gate)(DefaultHandlerType, func(endpoint abstraction.Endpoint, loggerFactory log.Factory) http.Handler { 142 | return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 143 | }) 144 | }) 145 | 146 | t.Run("group", func(t *testing.T) { 147 | for _, tc := range testCases1 { 148 | tc := tc 149 | t.Run(tc.title, func(t *testing.T) { 150 | t.Parallel() 151 | 152 | endp := internalAddService(gate, tc.service, func(path string, pathPrefix string, methods []string, handler http.Handler) (id string, e error) { 153 | return "1", nil 154 | }) 155 | 156 | if endp[0].DownstreamPath != tc.expectedPath { 157 | t.Fatalf("expectedPath %v, but got %v", tc.expectedPath, endp[0].DownstreamPath) 158 | } 159 | if endp[0].DownstreamPathPrefix != tc.expectedPathPrefix { 160 | t.Fatalf("expectedPathPrefix %v, but got %v", tc.expectedPathPrefix, endp[0].DownstreamPathPrefix) 161 | } 162 | if endp[0].UpstreamURL != tc.expectedDestination { 163 | t.Fatalf("expectedDestination %v, but got %v", tc.expectedDestination, endp[0].UpstreamURL) 164 | } 165 | }) 166 | } 167 | }) 168 | } 169 | -------------------------------------------------------------------------------- /middleware/auth/auth.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "context" 5 | "github.com/golang-jwt/jwt/v4" 6 | jwtRequest "github.com/golang-jwt/jwt/v4/request" 7 | "github.com/mitchellh/mapstructure" 8 | "github.com/osstotalsoft/bifrost/abstraction" 9 | "github.com/osstotalsoft/bifrost/log" 10 | "github.com/osstotalsoft/bifrost/middleware" 11 | "github.com/osstotalsoft/oidc-jwt-go" 12 | "github.com/osstotalsoft/oidc-jwt-go/discovery" 13 | "go.uber.org/zap" 14 | "net/http" 15 | "strings" 16 | ) 17 | 18 | //AuthorizationFilterCode is the code used to register this middleware 19 | const AuthorizationFilterCode = "auth" 20 | 21 | //AuthorizationOptions are the options configured for all endpoints 22 | type AuthorizationOptions struct { 23 | Authority string `mapstructure:"authority"` 24 | SecretProvider oidc.SecretProvider 25 | } 26 | 27 | //AuthorizationEndpointOptions are the options configured for each endpoint 28 | type AuthorizationEndpointOptions struct { 29 | Audience string `mapstructure:"audience"` 30 | Disabled bool `mapstructure:"disabled"` 31 | ClaimsRequirement map[string]string `mapstructure:"claims_requirement"` 32 | AllowedScopes []string `mapstructure:"allowed_scopes"` 33 | } 34 | 35 | //AuthorizationFilter is a middleware that handles authorization using 36 | //an OpendID Connect server 37 | func AuthorizationFilter(opts AuthorizationOptions) middleware.Func { 38 | if opts.SecretProvider == nil { 39 | opts.SecretProvider = oidc.NewOidcSecretProvider(discovery.NewClient(discovery.Options{opts.Authority})) 40 | } 41 | 42 | return func(endpoint abstraction.Endpoint, loggerFactory log.Factory) func(http.Handler) http.Handler { 43 | cfg := AuthorizationEndpointOptions{} 44 | if fl, ok := endpoint.Filters[AuthorizationFilterCode]; ok { 45 | err := mapstructure.Decode(fl, &cfg) 46 | if err != nil { 47 | loggerFactory(nil).Error("AuthorizationFilter: Cannot find or decode AuthorizationEndpointOptions for authorization filter", zap.Error(err)) 48 | } 49 | } 50 | audience := endpoint.OidcAudience 51 | if cfg.Audience != "" { 52 | audience = cfg.Audience 53 | } 54 | validator := oidc.NewJWTValidator(jwtRequest.OAuth2Extractor, opts.SecretProvider, audience, opts.Authority) 55 | 56 | return func(next http.Handler) http.Handler { 57 | return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { 58 | logger := loggerFactory(request.Context()) 59 | if !endpoint.Secured || cfg.Disabled { 60 | logger.Debug("AuthorizationFilter skipped") 61 | next.ServeHTTP(writer, request) 62 | return 63 | } 64 | 65 | token, err := validator(request) 66 | if err != nil { 67 | logger.Error("AuthorizationFilter: Token is not valid", zap.Error(err)) 68 | UnauthorizedWithHeader(writer, err.Error()) 69 | return 70 | } 71 | 72 | if len(cfg.AllowedScopes) > 0 || len(cfg.ClaimsRequirement) > 0 { 73 | if len(cfg.AllowedScopes) > 0 { 74 | hasScope := checkScopes(cfg.AllowedScopes, token.Claims.(jwt.MapClaims)["scope"].([]interface{})) 75 | if !hasScope { 76 | logger.Error("AuthorizationFilter: insufficient scope", zap.String("error", "insufficient scope")) 77 | InsufficientScope(writer, "insufficient scope", cfg.AllowedScopes) 78 | return 79 | } 80 | } 81 | 82 | if len(cfg.ClaimsRequirement) > 0 { 83 | hasScope := checkClaimsRequirements(cfg.ClaimsRequirement, token.Claims.(jwt.MapClaims)) 84 | if !hasScope { 85 | logger.Error("AuthorizationFilter: invalid claim", zap.String("error", "invalid claim")) 86 | Forbidden(writer, "invalid claim") 87 | return 88 | } 89 | } 90 | } 91 | 92 | ctx := context.WithValue(request.Context(), abstraction.ContextClaimsKey, token.Claims) 93 | request = request.WithContext(ctx) 94 | next.ServeHTTP(writer, request) 95 | 96 | }) 97 | } 98 | } 99 | } 100 | 101 | //UnauthorizedWithHeader adds to the response a WWW-Authenticate header and returns a StatusUnauthorized error 102 | func UnauthorizedWithHeader(writer http.ResponseWriter, err string) { 103 | writer.Header().Set("WWW-Authenticate", "Bearer error=\"invalid_token\", error_description=\""+err+"\"") 104 | http.Error(writer, "", http.StatusUnauthorized) 105 | } 106 | 107 | //InsufficientScope adds to the response a WWW-Authenticate header and returns a StatusForbidden error 108 | func InsufficientScope(writer http.ResponseWriter, err string, scopes []string) { 109 | val := "Bearer error=\"insufficient_scope\", error_description=\"" + err + "\" scope=\"" + strings.Join(scopes, ",") + "\"" 110 | writer.Header().Set("WWW-Authenticate", val) 111 | Forbidden(writer, "") 112 | } 113 | 114 | //Forbidden returns a StatusForbidden error 115 | func Forbidden(writer http.ResponseWriter, err string) { 116 | http.Error(writer, err, http.StatusForbidden) 117 | } 118 | 119 | func checkScopes(requiredScopes []string, userScopes []interface{}) bool { 120 | for _, el := range userScopes { 121 | for _, el1 := range requiredScopes { 122 | if el == el1 { 123 | return true 124 | } 125 | } 126 | } 127 | return false 128 | } 129 | 130 | func checkClaimsRequirements(requiredClaims map[string]string, claims jwt.MapClaims) bool { 131 | for key, val := range requiredClaims { 132 | v, ok := claims[key] 133 | if ok { 134 | if v != val { 135 | return false 136 | } 137 | } else { 138 | return false 139 | } 140 | } 141 | 142 | return true 143 | } 144 | -------------------------------------------------------------------------------- /handler/nats/natspublisher.go: -------------------------------------------------------------------------------- 1 | package nats 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/mitchellh/mapstructure" 7 | "github.com/nats-io/stan.go" 8 | "github.com/osstotalsoft/bifrost/abstraction" 9 | "github.com/osstotalsoft/bifrost/handler" 10 | "github.com/osstotalsoft/bifrost/log" 11 | "github.com/satori/go.uuid" 12 | "go.uber.org/zap" 13 | "io/ioutil" 14 | "net/http" 15 | ) 16 | 17 | //Config is the global NATS configuration 18 | type Config struct { 19 | NatsUrl string `mapstructure:"nats_url"` 20 | Cluster string `mapstructure:"cluster"` 21 | ClientId string `mapstructure:"client_id"` 22 | QGroup string `mapstructure:"q_group"` 23 | DurableName string `mapstructure:"durable_name"` 24 | TopicPrefix string `mapstructure:"topic_prefix"` 25 | Source string `mapstructure:"source"` 26 | transformMessageFunc TransformMessageFunc 27 | buildResponseFunc BuildResponseFunc 28 | logger log.Logger 29 | } 30 | 31 | //EndpointConfig is the NATS specific configuration of the endpoint 32 | type EndpointConfig struct { 33 | Topic string `mapstructure:"topic"` 34 | } 35 | 36 | //CloseConnectionFunc is to be called to close the NATS connection 37 | type CloseConnectionFunc func() error 38 | 39 | //TransformMessageFunc transforms a message received in the HTTP request to a format required by the NBB infrastructure. 40 | //It envelopes the message adding the required metadata such as UserId, CorrelationId, MessageId, PublishTime, Source, etc. 41 | type TransformMessageFunc func(messageContext messageContext, requestContext context.Context, payloadBytes []byte) ([]byte, error) 42 | 43 | //BuildResponseFunc builds the response that is returned by the Gateway after publishing a message 44 | // The returned data will be written to the HTTP response 45 | type BuildResponseFunc func(messageContext messageContext, requestContext context.Context) ([]byte, error) 46 | 47 | type messageContext struct { 48 | Source string 49 | Logger log.Logger 50 | Topic string 51 | RawPayload []byte 52 | Headers map[string]interface{} 53 | } 54 | 55 | //NewNatsPublisher creates an instance of the NATS publisher handler. 56 | // It transforms the received HTTP request using the transformMessageFunc into a message, publishes the message to NATS and 57 | // returns the http response built using buildResponseFunc 58 | func NewNatsPublisher(config Config, options ...Option) (handler.Func, CloseConnectionFunc, error) { 59 | 60 | config.transformMessageFunc = NoTransformation 61 | config.buildResponseFunc = EmptyResponse 62 | config.logger = log.NewNop() 63 | 64 | config = applyOptions(config, options) 65 | 66 | natsConnection, closeConnectionFunc, err := connect(config.NatsUrl, config.ClientId, config.Cluster, config.logger) 67 | if err != nil { 68 | //logger.Error("cannot connect", zap.Error(err)) 69 | return nil, closeConnectionFunc, err 70 | } 71 | 72 | handlerFunc := func(endpoint abstraction.Endpoint, loggerFactory log.Factory) http.Handler { 73 | var cfg EndpointConfig 74 | _ = mapstructure.Decode(endpoint.HandlerConfig, &cfg) 75 | 76 | return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { 77 | var messageContext = messageContext{Headers: map[string]interface{}{}} 78 | messageContext.Source = config.Source 79 | messageContext.Topic = config.TopicPrefix + cfg.Topic 80 | messageContext.Logger = loggerFactory(request.Context()) 81 | 82 | messageBytes, err := ioutil.ReadAll(request.Body) 83 | if err != nil { 84 | badRequest(messageContext.Logger, err, "cannot read body", writer) 85 | return 86 | } 87 | 88 | messageBytes, err = config.transformMessageFunc(messageContext, request.Context(), messageBytes) 89 | if err != nil { 90 | internalServerError(messageContext.Logger, err, "cannot transform", writer) 91 | return 92 | } 93 | 94 | if err := natsConnection.Publish(messageContext.Topic, messageBytes); err != nil { 95 | internalServerError(messageContext.Logger, err, "cannot publish", writer) 96 | return 97 | } 98 | 99 | messageContext.Logger.Debug( 100 | fmt.Sprintf("Forwarding request from %v to %v", request.URL.String(), messageContext.Topic), 101 | zap.String("request_url", request.URL.String()), 102 | zap.String("topic", messageContext.Topic)) 103 | 104 | responseBytes, err := config.buildResponseFunc(messageContext, request.Context()) 105 | if err != nil { 106 | internalServerError(messageContext.Logger, err, "build response error", writer) 107 | return 108 | } 109 | 110 | if responseBytes != nil { 111 | _, _ = writer.Write(responseBytes) 112 | } 113 | }) 114 | } 115 | return handlerFunc, closeConnectionFunc, nil 116 | } 117 | 118 | func internalServerError(logger log.Logger, err error, msg string, writer http.ResponseWriter) { 119 | logger.Error(msg, zap.Error(err)) 120 | http.Error(writer, err.Error(), http.StatusInternalServerError) 121 | } 122 | 123 | func badRequest(logger log.Logger, err error, msg string, writer http.ResponseWriter) { 124 | logger.Error(msg, zap.Error(err)) 125 | http.Error(writer, err.Error(), http.StatusBadRequest) 126 | } 127 | 128 | //connect opens a streaming NATS connection 129 | func connect(natsUrl, clientId, clusterId string, logger log.Logger) (stan.Conn, CloseConnectionFunc, error) { 130 | nc, err := stan.Connect(clusterId, clientId+uuid.NewV4().String(), stan.NatsURL(natsUrl)) 131 | if err != nil { 132 | //logger.Error("cannot connect to nats server", zap.Error(err)) 133 | return nc, nil, err 134 | } 135 | 136 | return nc, func() error { 137 | logger.Info("closing nats connection", zap.Error(err)) 138 | 139 | err := nc.Close() 140 | if err != nil { 141 | logger.Error("cannot close nats connection", zap.Error(err)) 142 | } 143 | return err 144 | }, err 145 | } 146 | -------------------------------------------------------------------------------- /tracing/spanlogger.go: -------------------------------------------------------------------------------- 1 | package tracing 2 | 3 | import ( 4 | "context" 5 | "github.com/opentracing/opentracing-go" 6 | tag "github.com/opentracing/opentracing-go/ext" 7 | otlog "github.com/opentracing/opentracing-go/log" 8 | "github.com/osstotalsoft/bifrost/log" 9 | "go.uber.org/zap" 10 | "go.uber.org/zap/zapcore" 11 | "time" 12 | ) 13 | 14 | //SpanLoggerFactory is a wrapper over zap.Logger with OpenTracing 15 | func SpanLoggerFactory(logger *zap.Logger) log.Factory { 16 | return func(ctx context.Context) log.Logger { 17 | if ctx != nil { 18 | if span := opentracing.SpanFromContext(ctx); span != nil { 19 | // TODO for Jaeger span extract trace/span IDs as fields 20 | return spanLogger{logger, span} 21 | } 22 | } 23 | return log.ZapLoggerFactory(logger)(nil) 24 | } 25 | } 26 | 27 | type spanLogger struct { 28 | logger *zap.Logger 29 | span opentracing.Span 30 | } 31 | 32 | func (sl spanLogger) Info(msg string, fields ...zapcore.Field) { 33 | sl.logToSpan("info", msg, fields...) 34 | sl.logger.Info(msg, fields...) 35 | } 36 | 37 | func (sl spanLogger) Error(msg string, fields ...zapcore.Field) { 38 | sl.logToSpan("error", msg, fields...) 39 | //tag.Error.Set(sl.span, true) 40 | sl.logger.Error(msg, fields...) 41 | } 42 | 43 | func (sl spanLogger) Fatal(msg string, fields ...zapcore.Field) { 44 | sl.logToSpan("fatal", msg, fields...) 45 | tag.Error.Set(sl.span, true) 46 | sl.logger.Fatal(msg, fields...) 47 | } 48 | 49 | // Debug logs a fatal error msg with fields 50 | func (sl spanLogger) Debug(msg string, fields ...zapcore.Field) { 51 | sl.logToSpan("debug", msg, fields...) 52 | sl.logger.Debug(msg, fields...) 53 | } 54 | 55 | // Warn logs a fatal error msg with fields 56 | func (sl spanLogger) Warn(msg string, fields ...zapcore.Field) { 57 | sl.logToSpan("warn", msg, fields...) 58 | sl.logger.Warn(msg, fields...) 59 | } 60 | 61 | // Panic logs a fatal error msg with fields 62 | func (sl spanLogger) Panic(msg string, fields ...zapcore.Field) { 63 | sl.logToSpan("panic", msg, fields...) 64 | tag.Error.Set(sl.span, true) 65 | sl.logger.Panic(msg, fields...) 66 | } 67 | 68 | // With creates a child logger, and optionally adds some context fields to that logger. 69 | func (sl spanLogger) With(fields ...zapcore.Field) log.Logger { 70 | return spanLogger{logger: sl.logger.With(fields...)} 71 | } 72 | 73 | func (sl spanLogger) logToSpan(level string, msg string, fields ...zapcore.Field) { 74 | // TODO rather than always converting the fields, we could wrap them into a lazy logger 75 | fa := fieldAdapter(make([]otlog.Field, 0, 2+len(fields))) 76 | fa = append(fa, otlog.String("event", msg)) 77 | fa = append(fa, otlog.String("level", level)) 78 | for _, field := range fields { 79 | field.AddTo(&fa) 80 | } 81 | sl.span.LogFields(fa...) 82 | } 83 | 84 | type fieldAdapter []otlog.Field 85 | 86 | func (fa *fieldAdapter) AddBool(key string, value bool) { 87 | *fa = append(*fa, otlog.Bool(key, value)) 88 | } 89 | 90 | func (fa *fieldAdapter) AddFloat64(key string, value float64) { 91 | *fa = append(*fa, otlog.Float64(key, value)) 92 | } 93 | 94 | func (fa *fieldAdapter) AddFloat32(key string, value float32) { 95 | *fa = append(*fa, otlog.Float64(key, float64(value))) 96 | } 97 | 98 | func (fa *fieldAdapter) AddInt(key string, value int) { 99 | *fa = append(*fa, otlog.Int(key, value)) 100 | } 101 | 102 | func (fa *fieldAdapter) AddInt64(key string, value int64) { 103 | *fa = append(*fa, otlog.Int64(key, value)) 104 | } 105 | 106 | func (fa *fieldAdapter) AddInt32(key string, value int32) { 107 | *fa = append(*fa, otlog.Int64(key, int64(value))) 108 | } 109 | 110 | func (fa *fieldAdapter) AddInt16(key string, value int16) { 111 | *fa = append(*fa, otlog.Int64(key, int64(value))) 112 | } 113 | 114 | func (fa *fieldAdapter) AddInt8(key string, value int8) { 115 | *fa = append(*fa, otlog.Int64(key, int64(value))) 116 | } 117 | 118 | func (fa *fieldAdapter) AddUint(key string, value uint) { 119 | *fa = append(*fa, otlog.Uint64(key, uint64(value))) 120 | } 121 | 122 | func (fa *fieldAdapter) AddUint64(key string, value uint64) { 123 | *fa = append(*fa, otlog.Uint64(key, value)) 124 | } 125 | 126 | func (fa *fieldAdapter) AddUint32(key string, value uint32) { 127 | *fa = append(*fa, otlog.Uint64(key, uint64(value))) 128 | } 129 | 130 | func (fa *fieldAdapter) AddUint16(key string, value uint16) { 131 | *fa = append(*fa, otlog.Uint64(key, uint64(value))) 132 | } 133 | 134 | func (fa *fieldAdapter) AddUint8(key string, value uint8) { 135 | *fa = append(*fa, otlog.Uint64(key, uint64(value))) 136 | } 137 | 138 | func (fa *fieldAdapter) AddUintptr(key string, value uintptr) {} 139 | func (fa *fieldAdapter) AddArray(key string, marshaler zapcore.ArrayMarshaler) error { return nil } 140 | func (fa *fieldAdapter) AddComplex128(key string, value complex128) {} 141 | func (fa *fieldAdapter) AddComplex64(key string, value complex64) {} 142 | func (fa *fieldAdapter) AddObject(key string, value zapcore.ObjectMarshaler) error { return nil } 143 | func (fa *fieldAdapter) AddReflected(key string, value interface{}) error { return nil } 144 | func (fa *fieldAdapter) OpenNamespace(key string) {} 145 | 146 | func (fa *fieldAdapter) AddDuration(key string, value time.Duration) { 147 | // TODO inefficient 148 | *fa = append(*fa, otlog.String(key, value.String())) 149 | } 150 | 151 | func (fa *fieldAdapter) AddTime(key string, value time.Time) { 152 | // TODO inefficient 153 | *fa = append(*fa, otlog.String(key, value.String())) 154 | } 155 | 156 | func (fa *fieldAdapter) AddBinary(key string, value []byte) { 157 | *fa = append(*fa, otlog.Object(key, value)) 158 | } 159 | 160 | func (fa *fieldAdapter) AddByteString(key string, value []byte) { 161 | *fa = append(*fa, otlog.Object(key, value)) 162 | } 163 | 164 | func (fa *fieldAdapter) AddString(key, value string) { 165 | if key != "" && value != "" { 166 | *fa = append(*fa, otlog.String(key, value)) 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/opentracing/opentracing-go" 6 | "github.com/osstotalsoft/bifrost/gateway" 7 | "github.com/osstotalsoft/bifrost/handler" 8 | "github.com/osstotalsoft/bifrost/handler/nats" 9 | "github.com/osstotalsoft/bifrost/handler/reverseproxy" 10 | "github.com/osstotalsoft/bifrost/httputils" 11 | "github.com/osstotalsoft/bifrost/log" 12 | "github.com/osstotalsoft/bifrost/middleware" 13 | "github.com/osstotalsoft/bifrost/middleware/auth" 14 | "github.com/osstotalsoft/bifrost/middleware/cors" 15 | r "github.com/osstotalsoft/bifrost/router" 16 | "github.com/osstotalsoft/bifrost/servicediscovery/provider/kubernetes" 17 | "github.com/osstotalsoft/bifrost/tracing" 18 | "github.com/spf13/viper" 19 | "github.com/uber/jaeger-client-go" 20 | jaegercfg "github.com/uber/jaeger-client-go/config" 21 | jaegerlog "github.com/uber/jaeger-client-go/log" 22 | "go.uber.org/zap" 23 | "go.uber.org/zap/zapcore" 24 | "io" 25 | "net/http" 26 | "os" 27 | "os/signal" 28 | "syscall" 29 | ) 30 | 31 | func main() { 32 | //https://github.com/golang/go/issues/16012 33 | http.DefaultTransport.(*http.Transport).MaxIdleConnsPerHost = 100 34 | 35 | level, zlogger, _ := getZapLogger() 36 | defer zlogger.Sync() 37 | 38 | cfg := getConfig(zlogger) 39 | changeLogLevel(level, cfg.LogLevel) 40 | 41 | loggerFactory := tracing.SpanLoggerFactory(zlogger.With(zap.String("service", "api gateway"))) 42 | logger := loggerFactory(nil) 43 | 44 | closer := setupJaeger(zlogger) 45 | defer closer.Close() 46 | 47 | provider := kubernetes.NewKubernetesServiceDiscoveryProvider(cfg.InCluster, cfg.OverrideServiceAddress, cfg.ServiceNamespacePrefixFilter, loggerFactory) 48 | dynRouter := r.NewDynamicRouter(r.GorillaMuxRouteMatcher, loggerFactory) 49 | //registry := in_memory_registry.NewInMemoryStore() 50 | 51 | natsHandler, closeNatsConnection, err := nats.NewNatsPublisher(getNatsHandlerConfig(zlogger), 52 | nats.TransformMessage(nats.NBBTransformMessage), 53 | nats.BuildResponse(nats.NBBBuildResponse), 54 | nats.Logger(logger), 55 | ) 56 | if err != nil { 57 | logger.Error("cannot connect to nats server", zap.Error(err)) 58 | } 59 | defer closeNatsConnection() 60 | 61 | gate := gateway.NewGateway(cfg, loggerFactory) 62 | registerHandlerFunc := gateway.RegisterHandler(gate) 63 | gateMiddlewareFunc := gateway.UseMiddleware(gate) 64 | 65 | //gateMiddlewareFunc(ratelimit.RateLimitingFilterCode, ratelimit.RateLimiting(ratelimit.MaxRequestLimit)) 66 | 67 | gateMiddlewareFunc(cors.CORSFilterCode, middleware.Compose(tracing.MiddlewareSpanWrapper("CORS Filter"))(cors.CORSFilter(getCORSConfig(zlogger)))) 68 | gateMiddlewareFunc(auth.AuthorizationFilterCode, middleware.Compose( 69 | tracing.MiddlewareSpanWrapper("Authorization Filter"), 70 | )(auth.AuthorizationFilter(getIdentityServerConfig(zlogger)))) 71 | 72 | registerHandlerFunc(handler.EventPublisherHandlerType, handler.Compose(tracing.HandlerSpanWrapper("Nats Handler"))(natsHandler)) 73 | registerHandlerFunc(handler.ReverseProxyHandlerType, handler.Compose( 74 | tracing.HandlerSpanWrapper("Reverse Proxy Handler"), 75 | )(reverseproxy.NewReverseProxy(tracing.NewRoundTripperWithOpenTrancing(), 76 | reverseproxy.AddUserIdToHeader, 77 | reverseproxy.ClearCorsHeaders))) 78 | 79 | addRouteFunc := r.AddRoute(dynRouter) 80 | removeRouteFunc := r.RemoveRoute(dynRouter) 81 | 82 | //configure and start ServiceDiscovery 83 | kubernetes.Compose( 84 | kubernetes.SubscribeOnAddService(gateway.AddService(gate)(addRouteFunc)), 85 | kubernetes.SubscribeOnRemoveService(gateway.RemoveService(gate)(removeRouteFunc)), 86 | kubernetes.SubscribeOnUpdateService(gateway.UpdateService(gate)(addRouteFunc, removeRouteFunc)), 87 | kubernetes.Start, 88 | )(provider) 89 | defer kubernetes.Stop(provider) 90 | 91 | go Shutdown(logger, gate) 92 | 93 | err = gateway.ListenAndServe(gate, httputils.Compose( 94 | httputils.RecoveryHandler(loggerFactory), 95 | tracing.SpanWrapper, 96 | )(r.GetHandler(dynRouter))) 97 | 98 | if err != nil { 99 | logger.Error("gateway cannot start", zap.Error(err)) 100 | } 101 | } 102 | 103 | //Shutdown gateway server and all subscriptions 104 | func Shutdown(logger log.Logger, gate *gateway.Gateway) { 105 | var signalsChannel = make(chan os.Signal, 1) 106 | signal.Notify(signalsChannel, os.Interrupt, syscall.SIGTERM) 107 | 108 | //wait for app closing 109 | <-signalsChannel 110 | logger.Info("Shutting down") 111 | 112 | err := gateway.Shutdown(gate) 113 | if err != nil { 114 | logger.Error("error closing gateway", zap.Error(err)) 115 | } 116 | } 117 | 118 | func getZapLogger() (zap.AtomicLevel, *zap.Logger, error) { 119 | cfg := zap.NewDevelopmentConfig() 120 | cfg.Encoding = "json" 121 | cfg.DisableCaller = true 122 | l, e := cfg.Build() 123 | return cfg.Level, l, e 124 | } 125 | 126 | func changeLogLevel(oldLevel zap.AtomicLevel, newLevel string) { 127 | level := zapcore.InfoLevel 128 | e := level.Set(newLevel) 129 | if e == nil { 130 | oldLevel.SetLevel(level) 131 | } 132 | return 133 | } 134 | 135 | func getConfig(logger *zap.Logger) *gateway.Config { 136 | viper.SetConfigName("config") 137 | viper.AddConfigPath(".") 138 | viper.SetConfigType("json") 139 | viper.AutomaticEnv() 140 | //viper.WatchConfig() 141 | 142 | err := viper.ReadInConfig() // Find and read the config file 143 | if err != nil { // Handle errors reading the config file 144 | logger.Panic("unable to read configuration file", zap.Error(err)) 145 | } 146 | 147 | var cfg = new(gateway.Config) 148 | err = viper.Unmarshal(cfg) 149 | if err != nil { 150 | logger.Panic("unable to decode into struct", zap.Error(err)) 151 | } 152 | logger.Info(fmt.Sprintf("using configuration: %v", viper.AllSettings())) 153 | 154 | return cfg 155 | } 156 | 157 | func getNatsHandlerConfig(logger *zap.Logger) nats.Config { 158 | var cfg = new(nats.Config) 159 | err := viper.UnmarshalKey("handlers.event.nats", cfg) 160 | if err != nil { 161 | logger.Panic("unable to decode into NatsConfig", zap.Error(err)) 162 | } 163 | 164 | return *cfg 165 | } 166 | 167 | func getIdentityServerConfig(logger *zap.Logger) auth.AuthorizationOptions { 168 | var cfg = new(auth.AuthorizationOptions) 169 | err := viper.UnmarshalKey("filters.auth", cfg) 170 | if err != nil { 171 | logger.Panic("unable to decode into AuthorizationOptions", zap.Error(err)) 172 | } 173 | 174 | return *cfg 175 | } 176 | 177 | func getCORSConfig(logger *zap.Logger) cors.Options { 178 | var cfg = new(cors.Options) 179 | err := viper.UnmarshalKey("filters.cors", cfg) 180 | if err != nil { 181 | logger.Panic("unable to decode into cors.Options", zap.Error(err)) 182 | } 183 | 184 | return *cfg 185 | } 186 | 187 | func setupJaeger(logger *zap.Logger) io.Closer { 188 | var cfg = &struct { 189 | Enabled bool `json:"enabled"` 190 | Agent string `json:"agent"` 191 | }{} 192 | 193 | err := viper.UnmarshalKey("opentracing", cfg) 194 | if err != nil { 195 | logger.Panic("unable to decode into Jaeger Config", zap.Error(err)) 196 | } 197 | 198 | jconfig := jaegercfg.Configuration{ 199 | Disabled: !cfg.Enabled, 200 | ServiceName: "Bifrost API Gateway", 201 | Sampler: &jaegercfg.SamplerConfig{ 202 | Type: jaeger.SamplerTypeConst, 203 | Param: 1, 204 | }, 205 | Reporter: &jaegercfg.ReporterConfig{ 206 | LogSpans: false, 207 | LocalAgentHostPort: cfg.Agent, 208 | }, 209 | } 210 | 211 | jLogger := jaegerlog.StdLogger 212 | //jMetricsFactory := jaegerprom.New() //metrics.NullFactory 213 | //jaeger.NewMetrics(factory, map[string]string{"lib": "jaeger"}) 214 | 215 | // Initialize tracer with a logger and a metrics factory 216 | tracer, closer, _ := jconfig.NewTracer( 217 | jaegercfg.Logger(jLogger), 218 | //jaegercfg.Metrics(jMetricsFactory), 219 | ) 220 | // Set the singleton opentracing.Tracer with the Jaeger tracer. 221 | opentracing.SetGlobalTracer(tracer) 222 | 223 | return closer 224 | } 225 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/osstotalsoft/bifrost/gateway" 5 | "github.com/osstotalsoft/bifrost/handler" 6 | "github.com/osstotalsoft/bifrost/handler/reverseproxy" 7 | "github.com/osstotalsoft/bifrost/log" 8 | r "github.com/osstotalsoft/bifrost/router" 9 | "github.com/osstotalsoft/bifrost/servicediscovery" 10 | "go.uber.org/zap" 11 | "io/ioutil" 12 | "net/http" 13 | "net/http/httptest" 14 | "net/url" 15 | "testing" 16 | ) 17 | 18 | type mainTest struct { 19 | title string 20 | responseFromGateway string 21 | requestUrl string 22 | backendUrl string 23 | } 24 | 25 | var ( 26 | testConfig2 = gateway.Config{ 27 | DownstreamPathPrefix: "", 28 | UpstreamPathPrefix: "/api", 29 | Endpoints: []gateway.EndpointConfig{ 30 | { 31 | UpstreamPathPrefix: "/api/v1/users", 32 | UpstreamPath: "", 33 | DownstreamPathPrefix: "/users", 34 | DownstreamPath: "", 35 | ServiceName: "users", 36 | Methods: nil, 37 | }, { 38 | UpstreamPathPrefix: "/api/v1/partners", 39 | UpstreamPath: "", 40 | DownstreamPathPrefix: "/partners", 41 | DownstreamPath: "", 42 | ServiceName: "partners", 43 | Methods: nil, 44 | }, 45 | { 46 | UpstreamPathPrefix: "/api/v2", 47 | UpstreamPath: "", 48 | DownstreamPathPrefix: "/dealers2", 49 | DownstreamPath: "", 50 | ServiceName: "dealers", 51 | Methods: nil, 52 | }, 53 | { 54 | UpstreamPathPrefix: "/api/offers1", 55 | UpstreamPath: "", 56 | DownstreamPathPrefix: "/offers1", 57 | DownstreamPath: "", 58 | ServiceName: "offers1", 59 | Methods: nil, 60 | }, 61 | { 62 | UpstreamPathPrefix: "/api/offers2", 63 | UpstreamPath: "/add/{v1}", 64 | DownstreamPathPrefix: "/offers2", 65 | DownstreamPath: `/add_offer/{v1}`, 66 | ServiceName: "offers2", 67 | Methods: []string{"GET"}, 68 | }, 69 | { 70 | UpstreamPathPrefix: "/offers3", 71 | UpstreamPath: "", 72 | DownstreamPathPrefix: "/offers3", 73 | DownstreamPath: "", 74 | ServiceName: "offers3", 75 | }, 76 | { 77 | UpstreamPathPrefix: "/api/v1", 78 | UpstreamPath: "/offers4?id={id}", 79 | DownstreamPathPrefix: "/offers4", 80 | DownstreamPath: "/{id}", 81 | ServiceName: "offers4", 82 | Methods: nil, 83 | }, 84 | }, 85 | } 86 | 87 | serviceList = []*servicediscovery.Service{ 88 | {Resource: "users", Secured: false}, 89 | {Resource: "partners", Secured: false}, 90 | {Resource: "dealers", Secured: false}, 91 | {Resource: "offers1", Secured: false}, 92 | {Resource: "offers2", Secured: false}, 93 | {Resource: "offers3", Secured: false}, 94 | {Resource: "offers4", Secured: false}, 95 | } 96 | 97 | testCases2 = []*mainTest{ 98 | { 99 | title: "serviceWithPrefixNoPath", 100 | responseFromGateway: "responseFromGateway", 101 | requestUrl: "/users", 102 | backendUrl: "/api/v1/users", 103 | }, 104 | { 105 | title: "serviceWithDefaults", 106 | responseFromGateway: "responseFromGateway1", 107 | requestUrl: "/partners/details/4545", 108 | backendUrl: "/api/v1/partners/details/4545", 109 | }, 110 | { 111 | title: "serviceWithPrefix2", 112 | responseFromGateway: "responseFromGateway2", 113 | requestUrl: "/dealers2", 114 | backendUrl: "/api/v2", 115 | }, 116 | { 117 | title: "serviceWithPrefix3", 118 | responseFromGateway: "responseFromGateway3", 119 | requestUrl: "/offers1/4435", 120 | backendUrl: "/api/offers1/4435", 121 | }, 122 | { 123 | title: "serviceWithPrefix4", 124 | responseFromGateway: "responseFromGateway4", 125 | requestUrl: "/offers2/add_offer/555", 126 | backendUrl: "/api/offers2/add/555", 127 | }, 128 | { 129 | title: "serviceWithPrefix5", 130 | responseFromGateway: "responseFromGateway5", 131 | requestUrl: "/offers3", 132 | backendUrl: "/offers3", 133 | }, 134 | { 135 | title: "serviceWithPrefix6", 136 | responseFromGateway: "responseFromGateway6", 137 | requestUrl: "/offers4/555", 138 | backendUrl: "/api/v1/offers4?id=555", 139 | }, 140 | { 141 | title: "testEncodingUrl", 142 | responseFromGateway: "testEncodingUrlResponse", 143 | requestUrl: "/dealers2/singWebApp%2F2137%2F6026a931-7c35", 144 | backendUrl: "/api/v2/singWebApp%2F2137%2F6026a931-7c35", 145 | }, 146 | { 147 | title: "testEncodingUrl3", 148 | responseFromGateway: "testEncodingUrlResponse", 149 | requestUrl: "/partners/131/RO 1734579", 150 | backendUrl: "/api/v1/partners/131/RO 1734579", 151 | }, 152 | { 153 | title: "testEncodingUrl2", 154 | responseFromGateway: "testEncodingUrl2Response", 155 | requestUrl: "/dealers2/singWe?search=&partnerId=", 156 | backendUrl: "/api/v2/singWe?search=&partnerId=", 157 | }, 158 | } 159 | ) 160 | 161 | func TestGatewayReverseProxy(t *testing.T) { 162 | backendServer := startBackend() 163 | defer backendServer.Close() 164 | 165 | logger, _ := zap.NewDevelopment() 166 | factory := log.ZapLoggerFactory(logger) 167 | 168 | dynRouter := r.NewDynamicRouter(r.GorillaMuxRouteMatcher, factory) 169 | gate := gateway.NewGateway(&testConfig2, factory) 170 | gateway.RegisterHandler(gate)(handler.ReverseProxyHandlerType, reverseproxy.NewReverseProxy(http.DefaultTransport, nil, nil)) 171 | frontendProxy := httptest.NewServer(r.GetHandler(dynRouter)) 172 | defer frontendProxy.Close() 173 | 174 | for _, service := range serviceList { 175 | gateway.AddService(gate)(r.AddRoute(dynRouter))(*service) 176 | } 177 | 178 | t.Run("group", func(t *testing.T) { 179 | for _, tc := range testCases2 { 180 | tc := tc 181 | t.Run(tc.title, func(t *testing.T) { 182 | t.Parallel() 183 | 184 | resp, err := http.Get(frontendProxy.URL + tc.requestUrl) 185 | if err != nil { 186 | t.Fatal(err) 187 | } 188 | 189 | defer resp.Body.Close() 190 | body, err := ioutil.ReadAll(resp.Body) 191 | if err != nil { 192 | t.Fatal(err) 193 | } 194 | 195 | if string(body) != tc.responseFromGateway { 196 | t.Errorf("test %s failed : expected %v, but got %v", tc.title, tc.responseFromGateway, string(body)) 197 | } 198 | }) 199 | } 200 | }) 201 | } 202 | 203 | func startBackend() *httptest.Server { 204 | mux := http.NewServeMux() 205 | backendServer := httptest.NewServer(mux) 206 | 207 | for _, tc := range serviceList { 208 | tc.Address = backendServer.URL 209 | } 210 | 211 | mux.HandleFunc("/", func(w http.ResponseWriter, request *http.Request) { 212 | url1, _ := url.PathUnescape(request.RequestURI) 213 | 214 | for _, tc := range testCases2 { 215 | if tc.backendUrl == url1 { 216 | _, _ = w.Write([]byte(tc.responseFromGateway)) 217 | return 218 | } 219 | } 220 | http.NotFound(w, request) 221 | }) 222 | 223 | return backendServer 224 | } 225 | 226 | func BenchmarkGatewayReverseProxy(b *testing.B) { 227 | backendServer := startBackend() 228 | defer backendServer.Close() 229 | 230 | factory := log.ZapLoggerFactory(zap.NewNop()) 231 | dynRouter := r.NewDynamicRouter(r.GorillaMuxRouteMatcher, factory) 232 | gate := gateway.NewGateway(&testConfig2, factory) 233 | gateway.RegisterHandler(gate)(handler.ReverseProxyHandlerType, reverseproxy.NewReverseProxy(http.DefaultTransport, nil, nil)) 234 | 235 | gateHandler := r.GetHandler(dynRouter) 236 | 237 | for _, service := range serviceList { 238 | gateway.AddService(gate)(r.AddRoute(dynRouter))(*service) 239 | } 240 | 241 | w := httptest.NewRecorder() 242 | req := httptest.NewRequest("GET", "/offers2/add_offer/555", nil) 243 | 244 | b.ReportAllocs() 245 | b.ResetTimer() 246 | 247 | for i := 0; i < b.N; i++ { 248 | gateHandler.ServeHTTP(w, req) 249 | w.Result() 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /servicediscovery/provider/kubernetes/kubernetes.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "context" 5 | "github.com/osstotalsoft/bifrost/log" 6 | "github.com/osstotalsoft/bifrost/servicediscovery" 7 | "go.uber.org/zap" 8 | corev1 "k8s.io/api/core/v1" 9 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 10 | "k8s.io/apimachinery/pkg/runtime" 11 | "k8s.io/apimachinery/pkg/watch" 12 | "k8s.io/client-go/kubernetes" 13 | "k8s.io/client-go/rest" 14 | "k8s.io/client-go/tools/cache" 15 | "k8s.io/client-go/tools/clientcmd" 16 | "os" 17 | "path/filepath" 18 | "strconv" 19 | "strings" 20 | "time" 21 | ) 22 | 23 | //KubeServiceProvider is a service discovery provider implementation, using Kubernetes 24 | type KubeServiceProvider struct { 25 | onAddServiceHandlers []servicediscovery.ServiceFunc 26 | onRemoveServiceHandlers []servicediscovery.ServiceFunc 27 | onUpdateServiceHandlers []func(old servicediscovery.Service, new servicediscovery.Service) 28 | stop chan struct{} 29 | clientset *kubernetes.Clientset 30 | overrideServiceAddress string 31 | logger log.Logger 32 | filterFunc func(name string, namespace string) bool 33 | } 34 | 35 | const resourceLabelName = "api-gateway/resource" 36 | const audienceLabelName = "api-gateway/oidc.audience" 37 | const securedLabelName = "api-gateway/secured" 38 | 39 | //NewKubernetesServiceDiscoveryProvider creates a new kube provider 40 | func NewKubernetesServiceDiscoveryProvider(inCluster bool, overrideServiceAddress string, 41 | filterServiceNamespaceByPrefix string, loggerFactory log.Factory) *KubeServiceProvider { 42 | 43 | logger := loggerFactory(nil) 44 | logger = logger.With(zap.String("component", "kubernetes_service_provider")) 45 | 46 | var config *rest.Config 47 | var err error 48 | 49 | if inCluster { 50 | config, err = rest.InClusterConfig() 51 | } else { 52 | config, err = outOfClusterConfig() 53 | } 54 | 55 | if err != nil { 56 | logger.Panic("KubernetesProvider: cannot connect to discovery provider", zap.Error(err)) 57 | } 58 | 59 | if inCluster && overrideServiceAddress != "" { 60 | logger.Panic("KubernetesProvider: You cannot override service address while in cluster mode", zap.Error(err)) 61 | } 62 | 63 | clientset, err := kubernetes.NewForConfig(config) 64 | if err != nil { 65 | logger.Panic("KubernetesProvider: cannot connect to discovery provider", zap.Error(err)) 66 | } 67 | 68 | p := &KubeServiceProvider{ 69 | onAddServiceHandlers: []servicediscovery.ServiceFunc{}, 70 | onRemoveServiceHandlers: []servicediscovery.ServiceFunc{}, 71 | onUpdateServiceHandlers: []func(old servicediscovery.Service, new servicediscovery.Service){}, 72 | clientset: clientset, 73 | stop: make(chan struct{}), 74 | overrideServiceAddress: overrideServiceAddress, 75 | logger: logger, 76 | } 77 | 78 | if filterServiceNamespaceByPrefix != "" { 79 | p.filterFunc = func(name string, namespace string) bool { 80 | return strings.HasPrefix(namespace, filterServiceNamespaceByPrefix) 81 | } 82 | } 83 | 84 | return p 85 | } 86 | 87 | func outOfClusterConfig() (*rest.Config, error) { 88 | kubeconfig := filepath.Join(os.Getenv("USERPROFILE"), ".kube", "config") 89 | 90 | // use the current context in kubeconfig 91 | return clientcmd.BuildConfigFromFlags("", kubeconfig) 92 | } 93 | 94 | func (provider *KubeServiceProvider) allowService(name string, namespace string) bool { 95 | if provider.filterFunc != nil { 96 | return provider.filterFunc(name, namespace) 97 | } 98 | return true 99 | } 100 | 101 | //Start starts the discovery process 102 | func Start(provider *KubeServiceProvider) *KubeServiceProvider { 103 | watchlist := newServicesListWatch(provider.clientset.CoreV1().RESTClient()) 104 | _, controller := cache.NewInformer(watchlist, &corev1.Service{}, time.Second*0, cache.ResourceEventHandlerFuncs{ 105 | AddFunc: addFunc(provider), 106 | DeleteFunc: deleteFunc(provider), 107 | UpdateFunc: updateFunc(provider), 108 | }) 109 | 110 | go controller.Run(provider.stop) 111 | return provider 112 | } 113 | 114 | func updateFunc(provider *KubeServiceProvider) func(oldObj, newObj interface{}) { 115 | return func(oldObj, newObj interface{}) { 116 | oldSrv := oldObj.(*corev1.Service) 117 | newSrv := newObj.(*corev1.Service) 118 | 119 | if !provider.allowService(newSrv.Name, newSrv.Namespace) { 120 | return 121 | } 122 | provider.logger.Info("KubernetesProvider: service updated", zap.Any("old_service", oldSrv), zap.Any("new_service", newSrv)) 123 | callUpdateSubscribers(provider.onUpdateServiceHandlers, 124 | mapToService(oldSrv, provider.overrideServiceAddress), 125 | mapToService(newSrv, provider.overrideServiceAddress)) 126 | } 127 | } 128 | 129 | func deleteFunc(provider *KubeServiceProvider) func(obj interface{}) { 130 | return func(obj interface{}) { 131 | srv := obj.(*corev1.Service) 132 | if !provider.allowService(srv.Name, srv.Namespace) { 133 | return 134 | } 135 | provider.logger.Info("KubernetesProvider: service deleted", zap.Any("service", srv)) 136 | callSubscribers(provider.onRemoveServiceHandlers, mapToService(srv, provider.overrideServiceAddress)) 137 | } 138 | } 139 | 140 | func addFunc(provider *KubeServiceProvider) func(obj interface{}) { 141 | return func(obj interface{}) { 142 | srv := obj.(*corev1.Service) 143 | if !provider.allowService(srv.Name, srv.Namespace) { 144 | return 145 | } 146 | provider.logger.Info("KubernetesProvider: service added", zap.Any("service", srv)) 147 | callSubscribers(provider.onAddServiceHandlers, mapToService(srv, provider.overrideServiceAddress)) 148 | } 149 | } 150 | 151 | func mapToService(srv *corev1.Service, overrideServiceAddress string) servicediscovery.Service { 152 | secured, _ := strconv.ParseBool(srv.Labels[securedLabelName]) 153 | 154 | address := "http://" + srv.Name + "." + srv.Namespace 155 | if overrideServiceAddress != "" { 156 | address = overrideServiceAddress 157 | } 158 | return servicediscovery.Service{ 159 | Address: address, 160 | Version: srv.ResourceVersion, 161 | UID: string(srv.UID), 162 | Name: srv.Name, 163 | Resource: srv.Labels[resourceLabelName], 164 | OidcAudience: srv.Labels[audienceLabelName], 165 | Secured: secured, 166 | Namespace: srv.Namespace, 167 | } 168 | } 169 | 170 | // to apply modification to ListOptions with a field selector, a label selector, or any other desired options. 171 | func newServicesListWatch(c cache.Getter) *cache.ListWatch { 172 | resource := "services" 173 | listFunc := func(options metav1.ListOptions) (runtime.Object, error) { 174 | options.LabelSelector = resourceLabelName 175 | return c.Get(). 176 | //Namespace(namespace). 177 | Resource(resource). 178 | VersionedParams(&options, metav1.ParameterCodec). 179 | Do(context.TODO()). 180 | Get() 181 | } 182 | watchFunc := func(options metav1.ListOptions) (watch.Interface, error) { 183 | options.Watch = true 184 | options.LabelSelector = resourceLabelName 185 | return c.Get(). 186 | //Namespace(namespace). 187 | Resource(resource). 188 | VersionedParams(&options, metav1.ParameterCodec). 189 | Watch(context.TODO()) 190 | } 191 | return &cache.ListWatch{ListFunc: listFunc, WatchFunc: watchFunc} 192 | } 193 | 194 | func callSubscribers(handlers []servicediscovery.ServiceFunc, service servicediscovery.Service) { 195 | for _, fn := range handlers { 196 | fn(service) 197 | } 198 | } 199 | 200 | func callUpdateSubscribers(handlers []func(old servicediscovery.Service, new servicediscovery.Service), old servicediscovery.Service, new servicediscovery.Service) { 201 | for _, fn := range handlers { 202 | fn(old, new) 203 | } 204 | } 205 | 206 | //Stop stops the discovery process 207 | func Stop(provider *KubeServiceProvider) *KubeServiceProvider { 208 | close(provider.stop) 209 | return provider 210 | } 211 | 212 | //SubscribeOnAddService registers some handlers to be called when a new service is found 213 | func SubscribeOnAddService(f servicediscovery.ServiceFunc) func(provider *KubeServiceProvider) *KubeServiceProvider { 214 | return func(provider *KubeServiceProvider) *KubeServiceProvider { 215 | provider.onAddServiceHandlers = append(provider.onAddServiceHandlers, f) 216 | return provider 217 | } 218 | } 219 | 220 | //SubscribeOnRemoveService registers some handlers to be called when a service is removed 221 | func SubscribeOnRemoveService(f servicediscovery.ServiceFunc) func(provider *KubeServiceProvider) *KubeServiceProvider { 222 | return func(provider *KubeServiceProvider) *KubeServiceProvider { 223 | provider.onRemoveServiceHandlers = append(provider.onRemoveServiceHandlers, f) 224 | return provider 225 | } 226 | } 227 | 228 | //SubscribeOnUpdateService registers some handlers to be called when a service gets updated 229 | func SubscribeOnUpdateService(f func(old servicediscovery.Service, new servicediscovery.Service)) func(provider *KubeServiceProvider) *KubeServiceProvider { 230 | return func(provider *KubeServiceProvider) *KubeServiceProvider { 231 | provider.onUpdateServiceHandlers = append(provider.onUpdateServiceHandlers, f) 232 | return provider 233 | } 234 | } 235 | 236 | //Compose composes provider functions 237 | func Compose(funcs ...func(p *KubeServiceProvider) *KubeServiceProvider) func(p *KubeServiceProvider) *KubeServiceProvider { 238 | return func(p *KubeServiceProvider) *KubeServiceProvider { 239 | for _, f := range funcs { 240 | p = f(p) 241 | } 242 | return p 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /gateway/gateway.go: -------------------------------------------------------------------------------- 1 | package gateway 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "github.com/osstotalsoft/bifrost/abstraction" 8 | "github.com/osstotalsoft/bifrost/handler" 9 | "github.com/osstotalsoft/bifrost/log" 10 | "github.com/osstotalsoft/bifrost/middleware" 11 | "github.com/osstotalsoft/bifrost/servicediscovery" 12 | "github.com/osstotalsoft/bifrost/strutils" 13 | "go.uber.org/zap" 14 | "net/http" 15 | "strconv" 16 | "sync" 17 | ) 18 | 19 | //DefaultHandlerType is the default handler used when a request matches a route 20 | const DefaultHandlerType = handler.ReverseProxyHandlerType 21 | 22 | //Gateway is a http.Handler able to route request to different handlers 23 | type Gateway struct { 24 | config *Config 25 | endPointToRouteMapper sync.Map 26 | middlewares []middlewareTuple 27 | handlers map[string]handler.Func 28 | loggerFactory log.Factory 29 | closer func() error 30 | } 31 | 32 | type middlewareTuple struct { 33 | key string 34 | middleware middleware.Func 35 | } 36 | 37 | //NewGateway is the Gateway constructor 38 | func NewGateway(config *Config, loggerFactory log.Factory) *Gateway { 39 | if config == nil { 40 | loggerFactory(nil).Error("Gateway: Must provide a configuration file") 41 | } 42 | return &Gateway{ 43 | config: config, 44 | handlers: map[string]handler.Func{}, 45 | loggerFactory: loggerFactory, 46 | } 47 | } 48 | 49 | //AddServiceFunc is a type for adding services using the same signature 50 | type AddServiceFunc func(addRouteFunc AddRouteFunc) func(service servicediscovery.Service) 51 | 52 | //AddRouteFunc is a type for adding routes using the same signature 53 | type AddRouteFunc func(path string, pathPrefix string, methods []string, handler http.Handler) (string, error) 54 | 55 | //UpdateEndpointFunc is a type for updating endpoints using the same signature 56 | type UpdateEndpointFunc func(addRouteFunc AddRouteFunc, removeRouteFunc func(routeId string)) func(oldService servicediscovery.Service, newService servicediscovery.Service) 57 | 58 | //AddService adds a service to the gateway 59 | func AddService(gate *Gateway) AddServiceFunc { 60 | return func(addRouteFunc AddRouteFunc) func(service servicediscovery.Service) { 61 | return func(service servicediscovery.Service) { 62 | err := validateService(gate, service) 63 | if err != nil { 64 | gate.loggerFactory(nil).Error("Gateway: invalid service ", zap.Error(err), zap.Any("service", service)) 65 | return 66 | } 67 | internalAddService(gate, service, addRouteFunc) 68 | } 69 | } 70 | } 71 | 72 | //UpdateService updates a service of the gateway 73 | func UpdateService(gate *Gateway) UpdateEndpointFunc { 74 | return func(addRouteFunc AddRouteFunc, removeRouteFunc func(routeId string)) func(oldService servicediscovery.Service, newService servicediscovery.Service) { 75 | return func(oldService servicediscovery.Service, newService servicediscovery.Service) { 76 | //removing routes 77 | removeRoutes(gate, oldService, removeRouteFunc) 78 | err := validateService(gate, newService) 79 | if err != nil { 80 | gate.loggerFactory(nil).Error("Gateway: invalid service ", zap.Error(err), zap.Any("service", newService)) 81 | return 82 | } 83 | //adding routes 84 | internalAddService(gate, newService, addRouteFunc) 85 | } 86 | } 87 | } 88 | 89 | //RemoveService removes a service from the gateway 90 | func RemoveService(gate *Gateway) func(removeRouteFunc func(routeId string)) func(service servicediscovery.Service) { 91 | return func(removeRouteFunc func(routeId string)) func(service servicediscovery.Service) { 92 | return func(service servicediscovery.Service) { 93 | removeRoutes(gate, service, removeRouteFunc) 94 | } 95 | } 96 | } 97 | 98 | //UseMiddleware registers a new middleware 99 | func UseMiddleware(gate *Gateway) func(key string, mwf middleware.Func) { 100 | return func(key string, mwf middleware.Func) { 101 | gate.middlewares = append(gate.middlewares, middlewareTuple{key, mwf}) 102 | } 103 | } 104 | 105 | //RegisterHandler registers a new handler 106 | func RegisterHandler(gate *Gateway) func(key string, handlerFunc handler.Func) { 107 | return func(key string, handlerFunc handler.Func) { 108 | gate.handlers[key] = handlerFunc 109 | } 110 | } 111 | 112 | func validateService(gate *Gateway, service servicediscovery.Service) error { 113 | if service.Resource == "" { 114 | return errors.New("invalid service resource name") 115 | } 116 | 117 | return nil 118 | } 119 | 120 | func internalAddService(gate *Gateway, service servicediscovery.Service, addRouteFunc AddRouteFunc) []abstraction.Endpoint { 121 | var routes []string 122 | 123 | endpoints := createEndpoints(gate.config, service) 124 | gate.loggerFactory(nil).Info("Gateway: created enpoints for service", zap.Any("service", service), zap.Any("endpoints", endpoints)) 125 | for _, endp := range endpoints { 126 | routeId, _ := addRouteFunc(endp.DownstreamPath, endp.DownstreamPathPrefix, endp.Methods, getEndpointHandler(gate, endp)) 127 | routes = append(routes, routeId) 128 | } 129 | gate.endPointToRouteMapper.Store(service.UID, routes) 130 | return endpoints 131 | } 132 | 133 | func removeRoutes(gate *Gateway, oldService servicediscovery.Service, removeRouteFunc func(routeId string)) { 134 | gate.endPointToRouteMapper.Range(func(key, value interface{}) bool { 135 | if key == oldService.UID { 136 | for _, rId := range value.([]string) { 137 | removeRouteFunc(rId) 138 | } 139 | return false 140 | } 141 | return true 142 | }) 143 | } 144 | 145 | func createEndpoints(config *Config, service servicediscovery.Service) []abstraction.Endpoint { 146 | configEndpoints := findConfigEndpoints(config.Endpoints, service.Resource) 147 | var endPoints []abstraction.Endpoint 148 | 149 | for _, endp := range configEndpoints { 150 | var endPoint abstraction.Endpoint 151 | 152 | endPoint.HandlerType = endp.HandlerType 153 | endPoint.HandlerConfig = endp.HandlerConfig 154 | endPoint.Filters = endp.Filters 155 | if endPoint.HandlerType == "" { 156 | endPoint.HandlerType = DefaultHandlerType 157 | } 158 | 159 | endPoint.Secured = service.Secured 160 | endPoint.OidcAudience = service.OidcAudience 161 | if service.OidcAudience == "" { 162 | endPoint.OidcAudience = service.Name 163 | } 164 | endPoint.DownstreamPathPrefix = endp.DownstreamPathPrefix 165 | if endPoint.DownstreamPathPrefix == "" { 166 | endPoint.DownstreamPathPrefix = strutils.SingleJoiningSlash(config.DownstreamPathPrefix, service.Resource) 167 | } 168 | endPoint.UpstreamPathPrefix = endp.UpstreamPathPrefix 169 | if endPoint.UpstreamPathPrefix == "" { 170 | endPoint.UpstreamPathPrefix = config.UpstreamPathPrefix 171 | } 172 | 173 | endPoint.UpstreamURL = strutils.SingleJoiningSlash(service.Address, strutils.SingleJoiningSlash(endPoint.UpstreamPathPrefix, endp.UpstreamPath)) 174 | endPoint.UpstreamPath = endp.UpstreamPath 175 | endPoint.DownstreamPath = endp.DownstreamPath 176 | endPoint.Methods = endp.Methods 177 | endPoints = append(endPoints, endPoint) 178 | } 179 | 180 | //add default route if no config found 181 | if len(endPoints) == 0 { 182 | var endPoint abstraction.Endpoint 183 | 184 | endPoint.Secured = service.Secured 185 | endPoint.OidcAudience = service.OidcAudience 186 | if service.OidcAudience == "" { 187 | endPoint.OidcAudience = service.Name 188 | } 189 | endPoint.HandlerType = DefaultHandlerType 190 | endPoint.DownstreamPathPrefix = strutils.SingleJoiningSlash(config.DownstreamPathPrefix, service.Resource) 191 | endPoint.UpstreamURL = strutils.SingleJoiningSlash(service.Address, config.UpstreamPathPrefix) 192 | endPoint.UpstreamPathPrefix = config.UpstreamPathPrefix 193 | endPoints = append(endPoints, endPoint) 194 | } 195 | 196 | return endPoints 197 | } 198 | 199 | func findConfigEndpoints(endpoints []EndpointConfig, serviceName string) []EndpointConfig { 200 | var result []EndpointConfig //endpoints[:0] 201 | for _, endp := range endpoints { 202 | if endp.ServiceName == serviceName { 203 | result = append(result, endp) 204 | } 205 | } 206 | return result 207 | } 208 | 209 | //ListenAndServe start the gateway server 210 | func ListenAndServe(gate *Gateway, handler http.Handler) error { 211 | name := gate.config.Name 212 | server := &http.Server{ 213 | Addr: ":" + strconv.Itoa(gate.config.Port), 214 | Handler: http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { 215 | writer.Header().Set("X-Gateway", name) 216 | handler.ServeHTTP(writer, request) 217 | })} 218 | 219 | idleConnsClosed := make(chan struct{}) 220 | 221 | gate.closer = func() error { 222 | err := server.Shutdown(context.Background()) 223 | close(idleConnsClosed) 224 | return err 225 | } 226 | 227 | err := server.ListenAndServe() 228 | if err != http.ErrServerClosed { 229 | return err 230 | } 231 | <-idleConnsClosed 232 | return nil 233 | } 234 | 235 | func Shutdown(gate *Gateway) error { 236 | return gate.closer() 237 | } 238 | 239 | func getEndpointHandler(gate *Gateway, endPoint abstraction.Endpoint) http.Handler { 240 | 241 | handlerFunc, ok := gate.handlers[endPoint.HandlerType] 242 | if !ok { 243 | gate.loggerFactory(nil).Fatal(fmt.Sprintf("handler %s is not registered", endPoint.HandlerType), zap.String("handler", endPoint.HandlerType)) 244 | return nil 245 | } 246 | 247 | endpointHandler := handlerFunc(endPoint, gate.loggerFactory) 248 | for i := len(gate.middlewares) - 1; i >= 0; i-- { 249 | endpointHandler = gate.middlewares[i].middleware(endPoint, gate.loggerFactory)(endpointHandler) 250 | } 251 | return endpointHandler 252 | } 253 | -------------------------------------------------------------------------------- /assets/diagram.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /assets/requests_flow.svg: -------------------------------------------------------------------------------- 1 | 2 |
Upstream services
Upstream services
Middlewares
Middlewares
Others
Others
Rate limiting
Rate limiting
Authorization
Authorization
CORS Filter
CORS Filter
Routing
Routing
Recovery
Recovery
Opentracing
Opentracing
Routing handler
Routing handler
Bifrost Gateway
Bifrost Gateway
Handlers
Handlers
GRPC
(TODO)
[Not supported by viewer]
NATS bus
NATS bus
Reverse proxy
Reverse proxy
Requests
Requests
-------------------------------------------------------------------------------- /assets/configuration_flow.svg: -------------------------------------------------------------------------------- 1 | 2 |
Gateway endpoint
generation
Gateway endpoint <br>generation
Endpoints
config overrides
Endpoints <br>config overrides<br>
KubernetesServices
Upstream discovery
Upstream discovery
Label filtering
Label filtering
Endpoint
ex: /api1
[Not supported by viewer]
Endpoint
handler pipeline
Endpoint <br>handler pipeline
Endpoint
ex: /api2
[Not supported by viewer]
Endpoint
handler pipeline
Endpoint <br>handler pipeline
Upstream services
Upstream services
Requests
Requests
Requests
Requests
--------------------------------------------------------------------------------