From ff3dbe2a27fef48ee0e6fe10368e531e474f6de1 Mon Sep 17 00:00:00 2001 From: Jonas Kaninda Date: Sun, 27 Oct 2024 07:24:50 +0100 Subject: [PATCH] chore: add route gateway verification --- README.md | 7 ++- pkg/config.go | 18 +++++-- pkg/handler.go | 12 +++++ pkg/helpers.go | 2 +- pkg/route.go | 142 +++++++++++++++++++++++++++---------------------- pkg/server.go | 42 +++++++++++++++ 6 files changed, 151 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index 5d668b9..1f1dae7 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ ``` Goma Gateway is a lightweight API Gateway and Reverse Proxy. -[![Build](https://github.com/jkaninda/goma-gateway/actions/workflows/release.yml/badge.svg)](https://github.com/jkaninda/goma/actions/workflows/release.yml) +[![Build](https://github.com/jkaninda/goma-gateway/actions/workflows/release.yml/badge.svg)](https://github.com/jkaninda/goma-gateway/actions/workflows/release.yml) [![Go Report](https://goreportcard.com/badge/github.com/jkaninda/goma-gateway)](https://goreportcard.com/report/github.com/jkaninda/goma-gateway) [![Go Reference](https://pkg.go.dev/badge/github.com/jkaninda/goma-gateway.svg)](https://pkg.go.dev/github.com/jkaninda/goma-gateway) ![Docker Image Size (latest by date)](https://img.shields.io/docker/image-size/jkaninda/goma-gateway?style=flat-square) @@ -65,8 +65,13 @@ docker run --rm --name goma-gateway \ ``` ### 4. Healthcheck +- Goma Gateway readiness: `/readyz` +- Routes health check: `/healthz` + [http://localhost/healthz](http://localhost/healthz) +[http://localhost/readyz](http://localhost/readyz) + > Healthcheck response body ```json diff --git a/pkg/config.go b/pkg/config.go index b0c0c6b..19e460d 100644 --- a/pkg/config.go +++ b/pkg/config.go @@ -179,7 +179,7 @@ func (GatewayServer) Config(configFile string) (*GatewayServer, error) { c := &GatewayConfig{} err = yaml.Unmarshal(buf, c) if err != nil { - return nil, fmt.Errorf("in file %q: %w", configFile, err) + return nil, fmt.Errorf("error parsing yaml %q: %w", configFile, err) } return &GatewayServer{ ctx: nil, @@ -338,11 +338,15 @@ func ToJWTRuler(input interface{}) (JWTRuler, error) { var bytes []byte bytes, err := yaml.Marshal(input) if err != nil { - return JWTRuler{}, fmt.Errorf("error marshalling yaml: %v", err) + return JWTRuler{}, fmt.Errorf("error parsing yaml: %v", err) } err = yaml.Unmarshal(bytes, jWTRuler) if err != nil { - return JWTRuler{}, fmt.Errorf("error unmarshalling yaml: %v", err) + return JWTRuler{}, fmt.Errorf("error parsing yaml: %v", err) + } + if jWTRuler.URL == "" { + return JWTRuler{}, fmt.Errorf("error parsing yaml: empty url in jwt auth middleware") + } return *jWTRuler, nil } @@ -352,11 +356,15 @@ func ToBasicAuth(input interface{}) (BasicRule, error) { var bytes []byte bytes, err := yaml.Marshal(input) if err != nil { - return BasicRule{}, fmt.Errorf("error marshalling yaml: %v", err) + return BasicRule{}, fmt.Errorf("error parsing yaml: %v", err) } err = yaml.Unmarshal(bytes, basicAuth) if err != nil { - return BasicRule{}, fmt.Errorf("error unmarshalling yaml: %v", err) + return BasicRule{}, fmt.Errorf("error parsing yaml: %v", err) + } + if basicAuth.Username == "" || basicAuth.Password == "" { + return BasicRule{}, fmt.Errorf("error parsing yaml: empty username/password in basic auth middleware") + } return *basicAuth, nil } diff --git a/pkg/handler.go b/pkg/handler.go index 456613c..d380a4c 100644 --- a/pkg/handler.go +++ b/pkg/handler.go @@ -105,3 +105,15 @@ func (heathRoute HealthCheckRoute) HealthCheckHandler(w http.ResponseWriter, r * return } } +func (heathRoute HealthCheckRoute) HealthReadyHandler(w http.ResponseWriter, r *http.Request) { + response := HealthCheckRouteResponse{ + Status: "healthy", + Error: "", + } + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + err := json.NewEncoder(w).Encode(response) + if err != nil { + return + } +} diff --git a/pkg/helpers.go b/pkg/helpers.go index fba4a60..f9193bc 100644 --- a/pkg/helpers.go +++ b/pkg/helpers.go @@ -20,5 +20,5 @@ func Intro() { nameFigure.Print() fmt.Printf("Version: %s\n", util.FullVersion()) fmt.Println("Copyright (c) 2024 Jonas Kaninda") - fmt.Println("Starting Goma server...") + fmt.Println("Starting Goma Gateway server...") } diff --git a/pkg/route.go b/pkg/route.go index e7308c8..8db8867 100644 --- a/pkg/route.go +++ b/pkg/route.go @@ -34,8 +34,8 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router { Routes: gateway.Routes, } // Define the health check route - r.HandleFunc("/health", heath.HealthCheckHandler).Methods("GET") r.HandleFunc("/healthz", heath.HealthCheckHandler).Methods("GET") + r.HandleFunc("/readyz", heath.HealthReadyHandler).Methods("GET") // Apply global Cors middlewares r.Use(CORSHandler(gateway.Cors)) // Apply CORS middleware if gateway.RateLimiter != 0 { @@ -45,15 +45,76 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router { r.Use(limiter.RateLimitMiddleware()) } for _, route := range gateway.Routes { - blM := middleware.BlockListMiddleware{ - Path: route.Path, - List: route.Blocklist, - } - // Add block access middleware to all route, if defined - r.Use(blM.BlocklistMiddleware) - //if route.Middlewares != nil { - for _, mid := range route.Middlewares { - secureRouter := r.PathPrefix(util.ParseURLPath(route.Path + mid.Path)).Subrouter() + if route.Path != "" { + blM := middleware.BlockListMiddleware{ + Path: route.Path, + List: route.Blocklist, + } + // Add block access middleware to all route, if defined + r.Use(blM.BlocklistMiddleware) + // Apply route middleware + for _, mid := range route.Middlewares { + if mid.Path != "" { + secureRouter := r.PathPrefix(util.ParseURLPath(route.Path + mid.Path)).Subrouter() + proxyRoute := ProxyRoute{ + path: route.Path, + rewrite: route.Rewrite, + destination: route.Destination, + disableXForward: route.DisableHeaderXForward, + cors: route.Cors, + } + rMiddleware, err := searchMiddleware(mid.Rules, middlewares) + if err != nil { + logger.Error("Middleware name not found") + } else { + //Check Authentication middleware + switch rMiddleware.Type { + case "basic": + basicAuth, err := ToBasicAuth(rMiddleware.Rule) + if err != nil { + logger.Error("Error: %s", err.Error()) + } else { + amw := middleware.AuthBasic{ + Username: basicAuth.Username, + Password: basicAuth.Password, + Headers: nil, + Params: nil, + } + // Apply JWT authentication middleware + secureRouter.Use(amw.AuthMiddleware) + secureRouter.Use(CORSHandler(route.Cors)) + secureRouter.PathPrefix("/").Handler(proxyRoute.ProxyHandler()) // Proxy handler + secureRouter.PathPrefix("").Handler(proxyRoute.ProxyHandler()) // Proxy handler + } + case "jwt": + jwt, err := ToJWTRuler(rMiddleware.Rule) + if err != nil { + logger.Error("Error: %s", err.Error()) + } else { + amw := middleware.AuthJWT{ + AuthURL: jwt.URL, + RequiredHeaders: jwt.RequiredHeaders, + Headers: jwt.Headers, + Params: jwt.Params, + } + // Apply JWT authentication middleware + secureRouter.Use(amw.AuthMiddleware) + secureRouter.Use(CORSHandler(route.Cors)) + secureRouter.PathPrefix("/").Handler(proxyRoute.ProxyHandler()) // Proxy handler + secureRouter.PathPrefix("").Handler(proxyRoute.ProxyHandler()) // Proxy handler + + } + default: + logger.Error("Unknown middleware type %s", rMiddleware.Type) + + } + + } + } else { + logger.Error("Error, middleware path is empty") + logger.Error("Middleware ignored") + } + } proxyRoute := ProxyRoute{ path: route.Path, rewrite: route.Rewrite, @@ -61,63 +122,14 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router { disableXForward: route.DisableHeaderXForward, cors: route.Cors, } - rMiddleware, err := searchMiddleware(mid.Rules, middlewares) - if err != nil { - logger.Error("Middleware name not found") - } else { - //Check Authentication middleware - switch rMiddleware.Type { - case "basic": - basicAuth, err := ToBasicAuth(rMiddleware.Rule) - if err != nil { - logger.Error("Error: %s", err.Error()) - } else { - amw := middleware.AuthBasic{ - Username: basicAuth.Username, - Password: basicAuth.Password, - Headers: nil, - Params: nil, - } - // Apply JWT authentication middleware - secureRouter.Use(amw.AuthMiddleware) - } - case "jwt": - jwt, err := ToJWTRuler(rMiddleware.Rule) - if err != nil { - - } else { - amw := middleware.AuthJWT{ - AuthURL: jwt.URL, - RequiredHeaders: jwt.RequiredHeaders, - Headers: jwt.Headers, - Params: jwt.Params, - } - // Apply JWT authentication middleware - secureRouter.Use(amw.AuthMiddleware) - - } - default: - logger.Error("Unknown middleware type %s", rMiddleware.Type) - - } - - } - secureRouter.Use(CORSHandler(route.Cors)) - secureRouter.PathPrefix("/").Handler(proxyRoute.ProxyHandler()) // Proxy handler - secureRouter.PathPrefix("").Handler(proxyRoute.ProxyHandler()) // Proxy handler + router := r.PathPrefix(route.Path).Subrouter() + router.Use(CORSHandler(route.Cors)) + router.PathPrefix("/").Handler(proxyRoute.ProxyHandler()) + } else { + logger.Error("Error, path is empty in route %s", route.Name) + logger.Info("Route path ignored: %s", route.Path) } - proxyRoute := ProxyRoute{ - path: route.Path, - rewrite: route.Rewrite, - destination: route.Destination, - disableXForward: route.DisableHeaderXForward, - cors: route.Cors, - } - - router := r.PathPrefix(route.Path).Subrouter() - router.Use(CORSHandler(route.Cors)) - router.PathPrefix("/").Handler(proxyRoute.ProxyHandler()) } return r diff --git a/pkg/server.go b/pkg/server.go index 9dfbcdc..8850be6 100644 --- a/pkg/server.go +++ b/pkg/server.go @@ -66,3 +66,45 @@ func (gatewayServer GatewayServer) Start(ctx context.Context) error { return nil } +func waitForReady( + ctx context.Context, + timeout time.Duration, + endpoint string, +) error { + client := http.Client{} + startTime := time.Now() + for { + req, err := http.NewRequestWithContext( + ctx, + http.MethodGet, + endpoint, + nil, + ) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + resp, err := client.Do(req) + if err != nil { + fmt.Printf("Error making request: %s\n", err.Error()) + continue + } + if resp.StatusCode == http.StatusOK { + fmt.Println("Endpoint is ready!") + resp.Body.Close() + return nil + } + resp.Body.Close() + + select { + case <-ctx.Done(): + return ctx.Err() + default: + if time.Since(startTime) >= timeout { + return fmt.Errorf("timeout reached while waiting for endpoint") + } + // wait a little while between checks + time.Sleep(250 * time.Millisecond) + } + } +}