diff --git a/.gitignore b/.gitignore index bfa678e..603d510 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,5 @@ Makefile NOTES.md tests configs -/config \ No newline at end of file +/config +compose.yaml \ No newline at end of file diff --git a/cmd/config/check.go b/cmd/config/check.go new file mode 100644 index 0000000..2a563c5 --- /dev/null +++ b/cmd/config/check.go @@ -0,0 +1,45 @@ +/* + * Copyright 2024 Jonas Kaninda + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package config + +import ( + pkg "github.com/jkaninda/goma-gateway/internal" + "github.com/spf13/cobra" + "log" +) + +var CheckConfigCmd = &cobra.Command{ + Use: "check", + Short: "Check Goma Gateway configuration file", + Run: func(cmd *cobra.Command, args []string) { + configFile, _ := cmd.Flags().GetString("config") + if configFile == "" { + log.Fatalln("no config file specified") + } + err := pkg.CheckConfig(configFile) + if err != nil { + log.Fatalf(" Error checking config file: %s\n", err) + } + log.Println("Goma Gateway configuration file checked successfully") + + }, +} + +func init() { + CheckConfigCmd.Flags().StringP("config", "c", "", "Path to the configuration filename") +} diff --git a/cmd/config/config.go b/cmd/config/config.go index f0ebb35..302ec75 100644 --- a/cmd/config/config.go +++ b/cmd/config/config.go @@ -17,8 +17,8 @@ limitations under the License. package config import ( - "github.com/jkaninda/goma-gateway/pkg/logger" "github.com/spf13/cobra" + "log" ) var Cmd = &cobra.Command{ @@ -28,7 +28,7 @@ var Cmd = &cobra.Command{ if len(args) == 0 { return } else { - logger.Fatal(`"config" accepts no argument %q`, args) + log.Fatalf("Config accepts no argument %q", args) } @@ -37,4 +37,5 @@ var Cmd = &cobra.Command{ func init() { Cmd.AddCommand(InitConfigCmd) + Cmd.AddCommand(CheckConfigCmd) } diff --git a/go.mod b/go.mod index f53fad8..087d6ec 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be github.com/golang-jwt/jwt v3.2.2+incompatible github.com/gorilla/mux v1.8.1 + github.com/prometheus/client_golang v1.20.5 github.com/spf13/cobra v1.8.1 golang.org/x/oauth2 v0.24.0 gopkg.in/yaml.v3 v3.0.1 @@ -20,7 +21,15 @@ require ( require ( cloud.google.com/go/compute/metadata v0.3.0 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.55.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/spf13/pflag v1.0.5 // indirect + google.golang.org/protobuf v1.34.2 // indirect ) diff --git a/go.sum b/go.sum index 0721c90..a866903 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,9 @@ cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2QxYC4trgAKZc= cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ= github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be/go.mod h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -15,12 +19,24 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jedib0t/go-pretty/v6 v6.6.1 h1:iJ65Xjb680rHcikRj6DSIbzCex2huitmc7bDtxYVWyc= github.com/jedib0t/go-pretty/v6 v6.6.1/go.mod h1:zbn98qrYlh95FIhwwsbIip0LYpwSG8SUOScs+v9/t0E= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= +github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc= +github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= @@ -40,6 +56,8 @@ golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/checkConfig.go b/internal/checkConfig.go new file mode 100644 index 0000000..67c568e --- /dev/null +++ b/internal/checkConfig.go @@ -0,0 +1,67 @@ +/* + * Copyright 2024 Jonas Kaninda + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package pkg + +import ( + "fmt" + "github.com/jkaninda/goma-gateway/util" + "gopkg.in/yaml.v3" + "log" + "os" +) + +func CheckConfig(fileName string) error { + if !util.FileExists(fileName) { + return fmt.Errorf("config file not found: %s", fileName) + } + buf, err := os.ReadFile(fileName) + if err != nil { + return err + } + c := &GatewayConfig{} + err = yaml.Unmarshal(buf, c) + if err != nil { + return fmt.Errorf("parsing the configuration file %q: %w", fileName, err) + } + gateway := &GatewayServer{ + ctx: nil, + version: c.Version, + gateway: c.GatewayConfig, + middlewares: c.Middlewares, + } + for index, route := range gateway.gateway.Routes { + if len(route.Name) == 0 { + log.Printf("Warning: route name is empty, index: [%d]", index) + } + if route.Destination == "" && len(route.Backends) == 0 { + log.Printf("Error: no destination or backends specified for route: %s | index: [%d] \n", route.Name, index) + } + } + + //Check middleware + for index, mid := range c.Middlewares { + if util.HasWhitespace(mid.Name) { + log.Printf("Warning: Middleware contains whitespace: %s | index: [%d], please remove whitespace characters\n", mid.Name, index) + } + } + + log.Printf("Routes count=%d Middlewares count=%d\n", len(gateway.gateway.Routes), len(gateway.middlewares)) + + return nil + +} diff --git a/internal/config.go b/internal/config.go index e02e780..5baf3d4 100644 --- a/internal/config.go +++ b/internal/config.go @@ -48,6 +48,7 @@ func (GatewayServer) Config(configFile string) (*GatewayServer, error) { } return &GatewayServer{ ctx: nil, + version: c.Version, gateway: c.GatewayConfig, middlewares: c.Middlewares, }, nil @@ -122,7 +123,7 @@ func initConfig(configFile string) { GatewayConfig: Gateway{ WriteTimeout: 15, ReadTimeout: 15, - IdleTimeout: 60, + IdleTimeout: 30, AccessLog: "/dev/Stdout", ErrorLog: "/dev/stderr", DisableRouteHealthCheckError: false, @@ -140,11 +141,14 @@ func initConfig(configFile string) { Routes: []Route{ { Name: "Public", - Path: "/public", + Path: "/", Methods: []string{"GET"}, Destination: "https://example.com", Rewrite: "/", - HealthCheck: "", + HealthCheck: RouteHealthCheck{ + Path: "/", + HealthyStatuses: []int{200, 404}, + }, Middlewares: []string{"api-forbidden-paths"}, }, { @@ -152,7 +156,7 @@ func initConfig(configFile string) { Path: "/protected", Destination: "https://example.com", Rewrite: "/", - HealthCheck: "", + HealthCheck: RouteHealthCheck{}, Cors: Cors{ Origins: []string{"http://localhost:3000", "https://dev.example.com"}, Headers: map[string]string{ @@ -164,12 +168,35 @@ func initConfig(configFile string) { Middlewares: []string{"basic-auth", "api-forbidden-paths"}, }, { - Name: "Hostname example", - Hosts: []string{"example.com", "example.localhost"}, - Path: "/", - Destination: "https://example.com", + Path: "/", + Name: "Hostname and load balancing example", + Hosts: []string{"example.com", "example.localhost"}, + InterceptErrors: []int{404, 405, 500}, + RateLimit: 60, + Backends: []string{ + "https://example.com", + "https://example2.com", + "https://example4.com", + }, Rewrite: "/", - HealthCheck: "", + HealthCheck: RouteHealthCheck{}, + }, + { + Path: "/loadbalancing", + Name: "loadBalancing example", + Hosts: []string{"example.com", "example.localhost"}, + Backends: []string{ + "https://example.com", + "https://example2.com", + "https://example4.com", + }, + Rewrite: "/", + HealthCheck: RouteHealthCheck{ + Path: "/health/live", + HealthyStatuses: []int{200, 404}, + Interval: 30, + Timeout: 10, + }, }, }, }, @@ -207,7 +234,6 @@ func initConfig(configFile string) { "/swagger-ui/*", "/v2/swagger-ui/*", "/api-docs/*", - "/internal/*", "/actuator/*", }, }, @@ -234,12 +260,11 @@ func initConfig(configFile string) { Name: "oauth-authentik", Type: OAuth, Paths: []string{ - "/protected", - "/example-of-oauth", + "/*", }, Rule: OauthRulerMiddleware{ - ClientID: "xxx", - ClientSecret: "xxx", + ClientID: "xxxx", + ClientSecret: "xxxx", RedirectURL: "http://localhost:8080/callback", Scopes: []string{"email", "openid"}, JWTSecret: "your-strong-jwt-secret | It's optional", diff --git a/internal/handler.go b/internal/handler.go index 33fec61..bf11111 100644 --- a/internal/handler.go +++ b/internal/handler.go @@ -72,8 +72,8 @@ func (heathRoute HealthCheckRoute) HealthCheckHandler(w http.ResponseWriter, r * for _, route := range heathRoute.Routes { go func() { defer wg.Done() - if route.HealthCheck != "" { - err := healthCheck(route.Destination + route.HealthCheck) + if route.HealthCheck.Path != "" { + err := healthCheck(route.Destination+route.HealthCheck.Path, route.HealthCheck.HealthyStatuses) if err != nil { if heathRoute.DisableRouteHealthCheckError { routes = append(routes, HealthCheckRouteResponse{Name: route.Name, Status: "unhealthy", Error: "Route healthcheck errors disabled"}) diff --git a/internal/healthCheck.go b/internal/healthCheck.go index 931f1a4..518baf0 100644 --- a/internal/healthCheck.go +++ b/internal/healthCheck.go @@ -21,9 +21,10 @@ import ( "io" "net/http" "net/url" + "slices" ) -func healthCheck(healthURL string) error { +func healthCheck(healthURL string, healthyStatuses []int) error { healthCheckURL, err := url.Parse(healthURL) if err != nil { return fmt.Errorf("error parsing HealthCheck URL: %v ", err) @@ -45,10 +46,16 @@ func healthCheck(healthURL string) error { if err != nil { } }(healthResp.Body) - - if healthResp.StatusCode >= 400 { - logger.Debug("Error performing HealthCheck request: %v ", err) - return fmt.Errorf("health check failed with status code %v", healthResp.StatusCode) + if len(healthyStatuses) > 0 { + if !slices.Contains(healthyStatuses, healthResp.StatusCode) { + logger.Error("Error performing HealthCheck request: %v ", err) + return fmt.Errorf("health check failed with status code %v", healthResp.StatusCode) + } + } else { + if healthResp.StatusCode >= 400 { + logger.Debug("Error performing HealthCheck request: %v ", err) + return fmt.Errorf("health check failed with status code %v", healthResp.StatusCode) + } } return nil } diff --git a/internal/prometheus.go b/internal/prometheus.go new file mode 100644 index 0000000..a1c0273 --- /dev/null +++ b/internal/prometheus.go @@ -0,0 +1,67 @@ +/* + * Copyright 2024 Jonas Kaninda + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package pkg + +import ( + "github.com/gorilla/mux" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "net/http" + "strconv" +) + +type PrometheusRoute struct { + name string + path string +} + +var totalRequests = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "http_requests_total", + Help: "Number of get requests.", + }, + []string{"path"}, +) + +var responseStatus = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "response_status", + Help: "Status of HTTP response", + }, + []string{"status"}, +) + +var httpDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Name: "http_response_time_seconds", + Help: "Duration of HTTP requests.", +}, []string{"path"}) + +func prometheusMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + route := mux.CurrentRoute(r) + path, _ := route.GetPathTemplate() + timer := prometheus.NewTimer(httpDuration.WithLabelValues(path)) + + responseStatus.WithLabelValues(strconv.Itoa(http.StatusOK)).Inc() + totalRequests.WithLabelValues(path).Inc() + + timer.ObserveDuration() + next.ServeHTTP(w, r) + }) + +} diff --git a/internal/proxy.go b/internal/proxy.go index 92d82c3..7188603 100644 --- a/internal/proxy.go +++ b/internal/proxy.go @@ -29,7 +29,7 @@ import ( // ProxyHandler proxies requests to the backend func (proxyRoute ProxyRoute) ProxyHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - logger.Info("%s %s %s %s", r.Method, getRealIP(r), r.URL, r.UserAgent()) + logger.Info("%s %s %s %s", r.Method, getRealIP(r), r.URL.Path, r.UserAgent()) // Check Method if is allowed if len(proxyRoute.methods) > 0 { if !slices.Contains(proxyRoute.methods, r.Method) { diff --git a/internal/route.go b/internal/route.go index 7a44822..83b0987 100644 --- a/internal/route.go +++ b/internal/route.go @@ -20,9 +20,17 @@ import ( "github.com/jkaninda/goma-gateway/internal/middleware" "github.com/jkaninda/goma-gateway/pkg/logger" "github.com/jkaninda/goma-gateway/util" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promhttp" "time" ) +func init() { + _ = prometheus.Register(totalRequests) + _ = prometheus.Register(responseStatus) + _ = prometheus.Register(httpDuration) +} + // Initialize the routes func (gatewayServer GatewayServer) Initialize() *mux.Router { gateway := gatewayServer.gateway @@ -32,11 +40,18 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router { DisableRouteHealthCheckError: gateway.DisableRouteHealthCheckError, Routes: gateway.Routes, } + if gateway.EnableMetrics { + // Prometheus endpoint + r.Path("/metrics").Handler(promhttp.Handler()) + r.Use(prometheusMiddleware) + } + // Routes health check if !gateway.DisableHealthCheckStatus { r.HandleFunc("/healthz", heath.HealthCheckHandler).Methods("GET") r.HandleFunc("/health/routes", heath.HealthCheckHandler).Methods("GET") } + // Health check r.HandleFunc("/health/live", heath.HealthReadyHandler).Methods("GET") r.HandleFunc("/readyz", heath.HealthReadyHandler).Methods("GET") @@ -199,7 +214,21 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router { disableXForward: route.DisableHeaderXForward, cors: route.Cors, } + // create route router := r.PathPrefix(route.Path).Subrouter() + // Apply common exploits to the route + // Enable common exploits + if route.BlockCommonExploits { + logger.Info("Block common exploits enabled") + router.Use(middleware.BlockExploitsMiddleware) + } + // Apply route rate limit + if route.RateLimit > 0 { + //rateLimiter := middleware.NewRateLimiter(gateway.RateLimit, time.Minute) + limiter := middleware.NewRateLimiterWindow(route.RateLimit, time.Minute, route.Cors.Origins) // requests per minute + // Add rate limit middleware to all routes, if defined + router.Use(limiter.RateLimitMiddleware()) + } // Apply route Cors router.Use(CORSHandler(route.Cors)) if len(route.Hosts) > 0 { @@ -209,6 +238,7 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router { } else { router.PathPrefix("").Handler(proxyRoute.ProxyHandler()) } + } else { logger.Error("Error, path is empty in route %s", route.Name) logger.Error("Route path ignored: %s", route.Path) diff --git a/internal/types.go b/internal/types.go index b181cbd..7fa4700 100644 --- a/internal/types.go +++ b/internal/types.go @@ -143,27 +143,29 @@ type Route struct { // // E.g. /cart to / => It will rewrite /cart path to / Rewrite string `yaml:"rewrite"` - // Destination Defines backend URL - Destination string `yaml:"destination"` // - Backends []string `yaml:"backends"` - // Cors contains the route cors headers - Cors Cors `yaml:"cors"` - //RateLimit int `yaml:"rateLimit"` // Methods allowed method Methods []string `yaml:"methods"` + // HealthCheck Defines the backend is health + HealthCheck RouteHealthCheck `yaml:"healthCheck"` + // Destination Defines backend URL + Destination string `yaml:"destination"` + Backends []string `yaml:"backends"` + // Cors contains the route cors headers + Cors Cors `yaml:"cors"` + RateLimit int `yaml:"rateLimit"` // DisableHeaderXForward Disable X-forwarded header. // // [X-Forwarded-Host, X-Forwarded-For, Host, Scheme ] // // It will not match the backend route DisableHeaderXForward bool `yaml:"disableHeaderXForward"` - // HealthCheck Defines the backend is health check PATH - HealthCheck string `yaml:"healthCheck"` // InterceptErrors intercepts backend errors based on the status codes // // Eg: [ 403, 405, 500 ] InterceptErrors []int `yaml:"interceptErrors"` + // BlockCommonExploits enable, disable block common exploits + BlockCommonExploits bool `yaml:"blockCommonExploits"` // Middlewares Defines route middleware from Middleware names Middlewares []string `yaml:"middlewares"` } @@ -194,6 +196,7 @@ type Gateway struct { DisableDisplayRouteOnStart bool `yaml:"disableDisplayRouteOnStart"` // DisableKeepAlive allows enabling and disabling KeepALive server DisableKeepAlive bool `yaml:"disableKeepAlive"` + EnableMetrics bool `yaml:"enableMetrics"` // InterceptErrors holds the status codes to intercept the error from backend InterceptErrors []int `yaml:"interceptErrors"` // Cors holds proxy global cors @@ -203,11 +206,10 @@ type Gateway struct { } type RouteHealthCheck struct { - Path string `yaml:"path"` - Interval int `yaml:"interval"` - Timeout int `yaml:"timeout"` - HealthyStatuses []int `yaml:"healthyStatuses"` - UnhealthyStatuses []int `yaml:"unhealthyStatuses"` + Path string `yaml:"path"` + Interval int `yaml:"interval"` + Timeout int `yaml:"timeout"` + HealthyStatuses []int `yaml:"healthyStatuses"` } type GatewayConfig struct { Version string `yaml:"version"` @@ -225,6 +227,7 @@ type ErrorResponse struct { } type GatewayServer struct { ctx context.Context + version string gateway Gateway middlewares []Middleware } diff --git a/util/helpers.go b/util/helpers.go index 6389439..a78464a 100644 --- a/util/helpers.go +++ b/util/helpers.go @@ -12,6 +12,7 @@ You may get a copy of the License at import ( "net/url" "os" + "regexp" "strconv" "strings" ) @@ -115,3 +116,7 @@ func UrlParsePath(uri string) string { } return parse.Path } + +func HasWhitespace(s string) bool { + return regexp.MustCompile(`\s`).MatchString(s) +}