diff --git a/pkg/config.go b/pkg/config.go index 13d1664..84c9827 100644 --- a/pkg/config.go +++ b/pkg/config.go @@ -16,7 +16,6 @@ See the License for the specific language governing permissions and limitations under the License. */ import ( - "context" "fmt" "github.com/jkaninda/goma-gateway/internal/logger" "github.com/jkaninda/goma-gateway/util" @@ -27,165 +26,6 @@ import ( var cfg *Gateway -type Config struct { - file string -} -type BasicRuleMiddleware struct { - Username string `yaml:"username"` - Password string `yaml:"password"` -} - -type Cors struct { - // Cors Allowed origins, - //e.g: - // - // - http://localhost:80 - // - // - https://example.com - Origins []string `yaml:"origins"` - // - //e.g: - // - //Access-Control-Allow-Origin: '*' - // - // Access-Control-Allow-Methods: 'GET, POST, PUT, DELETE, OPTIONS' - // - // Access-Control-Allow-Cors: 'Content-Type, Authorization' - Headers map[string]string `yaml:"headers"` -} - -// JWTRuleMiddleware authentication using HTTP GET method -// -// JWTRuleMiddleware contains the authentication details -type JWTRuleMiddleware struct { - // URL contains the authentication URL, it supports HTTP GET method only. - URL string `yaml:"url"` - // RequiredHeaders , contains required before sending request to the backend. - RequiredHeaders []string `yaml:"requiredHeaders"` - // Headers Add header to the backend from Authentication request's header, depending on your requirements. - // Key is Http's response header Key, and value is the backend Request's header Key. - // In case you want to get headers from Authentication service and inject them to backend request's headers. - Headers map[string]string `yaml:"headers"` - // Params same as Headers, contains the request params. - // - // Gets authentication headers from authentication request and inject them as request params to the backend. - // - // Key is Http's response header Key, and value is the backend Request's request param Key. - // - // In case you want to get headers from Authentication service and inject them to next request's params. - // - //e.g: Header X-Auth-UserId to query userId - Params map[string]string `yaml:"params"` -} -type RateLimiter struct { - // ipBased, tokenBased - Type string `yaml:"type"` - Rate float64 `yaml:"rate"` - Rule int `yaml:"rule"` -} - -type AccessRuleMiddleware struct { - ResponseCode int `yaml:"responseCode"` // HTTP Response code -} - -// Middleware defined the route middleware -type Middleware struct { - //Path contains the name of middleware and must be unique - Name string `yaml:"name"` - // Type contains authentication types - // - // basic, jwt, auth0, rateLimit, access - Type string `yaml:"type"` // Middleware type [basic, jwt, auth0, rateLimit, access] - Paths []string `yaml:"paths"` // Protected paths - // Rule contains rule type of - Rule interface{} `yaml:"rule"` // Middleware rule -} -type MiddlewareName struct { - name string `yaml:"name"` -} - -// Route defines gateway route -type Route struct { - // Name defines route name - Name string `yaml:"name"` - //Host Domain/host based request routing - Host string `yaml:"host"` - // Path defines route path - Path string `yaml:"path"` - // Rewrite rewrites route path to desired path - // - // E.g. /cart to / => It will rewrite /cart path to / - Rewrite string `yaml:"rewrite"` - // Destination Defines backend URL - Destination string `yaml:"destination"` - // Cors contains the route cors headers - Cors Cors `yaml:"cors"` - // 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"` - // Middlewares Defines route middleware from Middleware names - Middlewares []string `yaml:"middlewares"` -} - -// Gateway contains Goma Proxy Gateway's configs -type Gateway struct { - // ListenAddr Defines the server listenAddr - // - //e.g: localhost:8080 - ListenAddr string `yaml:"listenAddr" env:"GOMA_LISTEN_ADDR, overwrite"` - // WriteTimeout defines proxy write timeout - WriteTimeout int `yaml:"writeTimeout" env:"GOMA_WRITE_TIMEOUT, overwrite"` - // ReadTimeout defines proxy read timeout - ReadTimeout int `yaml:"readTimeout" env:"GOMA_READ_TIMEOUT, overwrite"` - // IdleTimeout defines proxy idle timeout - IdleTimeout int `yaml:"idleTimeout" env:"GOMA_IDLE_TIMEOUT, overwrite"` - // RateLimiter Defines number of request peer minute - RateLimiter int `yaml:"rateLimiter" env:"GOMA_RATE_LIMITER, overwrite"` - AccessLog string `yaml:"accessLog" env:"GOMA_ACCESS_LOG, overwrite"` - ErrorLog string `yaml:"errorLog" env:"GOMA_ERROR_LOG=, overwrite"` - // DisableHealthCheckStatus enable and disable routes health check - DisableHealthCheckStatus bool `yaml:"disableHealthCheckStatus"` - // DisableRouteHealthCheckError allows enabling and disabling backend healthcheck errors - DisableRouteHealthCheckError bool `yaml:"disableRouteHealthCheckError"` - //Disable allows enabling and disabling displaying routes on start - DisableDisplayRouteOnStart bool `yaml:"disableDisplayRouteOnStart"` - // DisableKeepAlive allows enabling and disabling KeepALive server - DisableKeepAlive bool `yaml:"disableKeepAlive"` - // InterceptErrors holds the status codes to intercept the error from backend - InterceptErrors []int `yaml:"interceptErrors"` - // Cors holds proxy global cors - Cors Cors `yaml:"cors"` - // Routes holds proxy routes - Routes []Route `yaml:"routes"` -} -type GatewayConfig struct { - // GatewayConfig holds Gateway config - GatewayConfig Gateway `yaml:"gateway"` - // Middlewares holds proxy middlewares - Middlewares []Middleware `yaml:"middlewares"` -} - -// ErrorResponse represents the structure of the JSON error response -type ErrorResponse struct { - Success bool `json:"success"` - Code int `json:"code"` - Message string `json:"message"` -} -type GatewayServer struct { - ctx context.Context - gateway Gateway - middlewares []Middleware -} - // Config reads config file and returns Gateway func (GatewayServer) Config(configFile string) (*GatewayServer, error) { if util.FileExists(configFile) { diff --git a/pkg/healthCheck.go b/pkg/healthCheck.go index 1a66735..8b5c283 100644 --- a/pkg/healthCheck.go +++ b/pkg/healthCheck.go @@ -23,22 +23,6 @@ import ( "net/url" ) -type HealthCheckRoute struct { - DisableRouteHealthCheckError bool - Routes []Route -} - -// HealthCheckResponse represents the health check response structure -type HealthCheckResponse struct { - Status string `json:"status"` - Routes []HealthCheckRouteResponse `json:"routes"` -} -type HealthCheckRouteResponse struct { - Name string `json:"name"` - Status string `json:"status"` - Error string `json:"error"` -} - func HealthCheck(healthURL string) error { healthCheckURL, err := url.Parse(healthURL) if err != nil { diff --git a/pkg/middleware.go b/pkg/middleware.go index aee826a..3ee792a 100644 --- a/pkg/middleware.go +++ b/pkg/middleware.go @@ -2,7 +2,6 @@ package pkg import ( "errors" - "github.com/gorilla/mux" "slices" "strings" ) @@ -18,14 +17,6 @@ func getMiddleware(rules []string, middlewares []Middleware) (Middleware, error) return Middleware{}, errors.New("middleware not found with name: [" + strings.Join(rules, ";") + "]") } -type RoutePath struct { - route Route - path string - rules []string - middlewares []Middleware - router *mux.Router -} - func doesExist(tyName string) bool { middlewareList := []string{BasicAuth, JWTAuth, AccessMiddleware} if slices.Contains(middlewareList, tyName) { diff --git a/pkg/middleware/error-interceptor.go b/pkg/middleware/error-interceptor.go index 7630ef0..b12f503 100644 --- a/pkg/middleware/error-interceptor.go +++ b/pkg/middleware/error-interceptor.go @@ -24,18 +24,6 @@ import ( "net/http" ) -// InterceptErrors contains backend status code errors to intercept -type InterceptErrors struct { - Errors []int -} - -// responseRecorder intercepts the response body and status code -type responseRecorder struct { - http.ResponseWriter - statusCode int - body *bytes.Buffer -} - func newResponseRecorder(w http.ResponseWriter) *responseRecorder { return &responseRecorder{ ResponseWriter: w, @@ -59,7 +47,7 @@ func (intercept InterceptErrors) ErrorInterceptor(next http.Handler) http.Handle next.ServeHTTP(rec, r) if canIntercept(rec.statusCode, intercept.Errors) { logger.Error("Backend error") - logger.Error("An error occurred in the backend, %d", rec.statusCode) + logger.Error("An error occurred from the backend with the status code: %d", rec.statusCode) w.Header().Set("Content-Type", "application/json") w.WriteHeader(rec.statusCode) err := json.NewEncoder(w).Encode(ProxyResponseError{ diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 75343c0..00822b9 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -23,78 +23,8 @@ import ( "net/http" "net/url" "strings" - "sync" - "time" ) -// RateLimiter defines rate limit properties. -type RateLimiter struct { - Requests int - Window time.Duration - ClientMap map[string]*Client - mu sync.Mutex -} - -// Client stores request count and window expiration for each client. -type Client struct { - RequestCount int - ExpiresAt time.Time -} - -// NewRateLimiterWindow creates a new RateLimiter. -func NewRateLimiterWindow(requests int, window time.Duration) *RateLimiter { - return &RateLimiter{ - Requests: requests, - Window: window, - ClientMap: make(map[string]*Client), - } -} - -// TokenRateLimiter stores tokenRate limit -type TokenRateLimiter struct { - tokens int - maxTokens int - refillRate time.Duration - lastRefill time.Time - mu sync.Mutex -} - -// ProxyResponseError represents the structure of the JSON error response -type ProxyResponseError struct { - Success bool `json:"success"` - Code int `json:"code"` - Message string `json:"message"` -} - -// JwtAuth stores JWT configuration -type JwtAuth struct { - AuthURL string - RequiredHeaders []string - Headers map[string]string - Params map[string]string -} - -// AuthenticationMiddleware Define struct -type AuthenticationMiddleware struct { - AuthURL string - RequiredHeaders []string - Headers map[string]string - Params map[string]string -} -type AccessListMiddleware struct { - Path string - Destination string - List []string -} - -// AuthBasic contains Basic auth configuration -type AuthBasic struct { - Username string - Password string - Headers map[string]string - Params map[string]string -} - // AuthMiddleware authenticate the client using JWT // // authorization based on the result of backend's response and continue the request when the client is authorized diff --git a/pkg/middleware/rate-limiter.go b/pkg/middleware/rate-limit.go similarity index 91% rename from pkg/middleware/rate-limiter.go rename to pkg/middleware/rate-limit.go index d814a9f..0d6ac7d 100644 --- a/pkg/middleware/rate-limiter.go +++ b/pkg/middleware/rate-limit.go @@ -34,7 +34,7 @@ func (rl *TokenRateLimiter) RateLimitMiddleware() mux.MiddlewareFunc { err := json.NewEncoder(w).Encode(ProxyResponseError{ Success: false, Code: http.StatusTooManyRequests, - Message: "Too many requests. Please try again later.", + Message: "Too many requests, API rate limit exceeded. Please try again later.", }) if err != nil { return @@ -66,13 +66,13 @@ func (rl *RateLimiter) RateLimitMiddleware() mux.MiddlewareFunc { rl.mu.Unlock() if client.RequestCount > rl.Requests { - logger.Error("Too many request from IP: %s %s %s", clientID, r.URL, r.UserAgent()) + logger.Error("Too many requests from IP: %s %s %s", clientID, r.URL, r.UserAgent()) w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusTooManyRequests) err := json.NewEncoder(w).Encode(ProxyResponseError{ Success: false, Code: http.StatusTooManyRequests, - Message: "Too many requests. Please try again later.", + Message: "Too many requests, API rate limit exceeded. Please try again later.", }) if err != nil { return diff --git a/pkg/middleware/types.go b/pkg/middleware/types.go new file mode 100644 index 0000000..7d2695d --- /dev/null +++ b/pkg/middleware/types.go @@ -0,0 +1,105 @@ +/* + * 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 middleware + +import ( + "bytes" + "net/http" + "sync" + "time" +) + +// RateLimiter defines rate limit properties. +type RateLimiter struct { + Requests int + Window time.Duration + ClientMap map[string]*Client + mu sync.Mutex +} + +// Client stores request count and window expiration for each client. +type Client struct { + RequestCount int + ExpiresAt time.Time +} + +// NewRateLimiterWindow creates a new RateLimiter. +func NewRateLimiterWindow(requests int, window time.Duration) *RateLimiter { + return &RateLimiter{ + Requests: requests, + Window: window, + ClientMap: make(map[string]*Client), + } +} + +// TokenRateLimiter stores tokenRate limit +type TokenRateLimiter struct { + tokens int + maxTokens int + refillRate time.Duration + lastRefill time.Time + mu sync.Mutex +} + +// ProxyResponseError represents the structure of the JSON error response +type ProxyResponseError struct { + Success bool `json:"success"` + Code int `json:"code"` + Message string `json:"message"` +} + +// JwtAuth stores JWT configuration +type JwtAuth struct { + AuthURL string + RequiredHeaders []string + Headers map[string]string + Params map[string]string +} + +// AuthenticationMiddleware Define struct +type AuthenticationMiddleware struct { + AuthURL string + RequiredHeaders []string + Headers map[string]string + Params map[string]string +} +type AccessListMiddleware struct { + Path string + Destination string + List []string +} + +// AuthBasic contains Basic auth configuration +type AuthBasic struct { + Username string + Password string + Headers map[string]string + Params map[string]string +} + +// InterceptErrors contains backend status code errors to intercept +type InterceptErrors struct { + Errors []int +} + +// responseRecorder intercepts the response body and status code +type responseRecorder struct { + http.ResponseWriter + statusCode int + body *bytes.Buffer +} diff --git a/pkg/proxy.go b/pkg/proxy.go index 3268824..0e7abe4 100644 --- a/pkg/proxy.go +++ b/pkg/proxy.go @@ -25,19 +25,10 @@ import ( "strings" ) -type ProxyRoute struct { - path string - rewrite string - destination string - cors Cors - disableXForward bool -} - // ProxyHandler proxies requests to the backend func (proxyRoute ProxyRoute) ProxyHandler() http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { - realIP := getRealIP(r) - logger.Info("%s %s %s %s", r.Method, realIP, r.URL, r.UserAgent()) + logger.Info("%s %s %s %s", r.Method, getRealIP(r), r.URL, r.UserAgent()) // Set CORS headers from the cors config //Update Cors Headers for k, v := range proxyRoute.cors.Headers { @@ -75,8 +66,8 @@ func (proxyRoute ProxyRoute) ProxyHandler() http.HandlerFunc { r.URL.Host = targetURL.Host r.URL.Scheme = targetURL.Scheme r.Header.Set("X-Forwarded-Host", r.Header.Get("Host")) - r.Header.Set("X-Forwarded-For", realIP) - r.Header.Set("X-Real-IP", realIP) + r.Header.Set("X-Forwarded-For", getRealIP(r)) + r.Header.Set("X-Real-IP", getRealIP(r)) r.Host = targetURL.Host } // Create proxy diff --git a/pkg/types.go b/pkg/types.go new file mode 100644 index 0000000..53d2a7e --- /dev/null +++ b/pkg/types.go @@ -0,0 +1,214 @@ +/* + * 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 ( + "context" + "github.com/gorilla/mux" +) + +type Config struct { + file string +} +type BasicRuleMiddleware struct { + Username string `yaml:"username"` + Password string `yaml:"password"` +} + +type Cors struct { + // Cors Allowed origins, + //e.g: + // + // - http://localhost:80 + // + // - https://example.com + Origins []string `yaml:"origins"` + // + //e.g: + // + //Access-Control-Allow-Origin: '*' + // + // Access-Control-Allow-Methods: 'GET, POST, PUT, DELETE, OPTIONS' + // + // Access-Control-Allow-Cors: 'Content-Type, Authorization' + Headers map[string]string `yaml:"headers"` +} + +// JWTRuleMiddleware authentication using HTTP GET method +// +// JWTRuleMiddleware contains the authentication details +type JWTRuleMiddleware struct { + // URL contains the authentication URL, it supports HTTP GET method only. + URL string `yaml:"url"` + // RequiredHeaders , contains required before sending request to the backend. + RequiredHeaders []string `yaml:"requiredHeaders"` + // Headers Add header to the backend from Authentication request's header, depending on your requirements. + // Key is Http's response header Key, and value is the backend Request's header Key. + // In case you want to get headers from Authentication service and inject them to backend request's headers. + Headers map[string]string `yaml:"headers"` + // Params same as Headers, contains the request params. + // + // Gets authentication headers from authentication request and inject them as request params to the backend. + // + // Key is Http's response header Key, and value is the backend Request's request param Key. + // + // In case you want to get headers from Authentication service and inject them to next request's params. + // + //e.g: Header X-Auth-UserId to query userId + Params map[string]string `yaml:"params"` +} +type RateLimiter struct { + // ipBased, tokenBased + Type string `yaml:"type"` + Rate float64 `yaml:"rate"` + Rule int `yaml:"rule"` +} + +type AccessRuleMiddleware struct { + ResponseCode int `yaml:"responseCode"` // HTTP Response code +} + +// Middleware defined the route middleware +type Middleware struct { + //Path contains the name of middleware and must be unique + Name string `yaml:"name"` + // Type contains authentication types + // + // basic, jwt, auth0, rateLimit, access + Type string `yaml:"type"` // Middleware type [basic, jwt, auth0, rateLimit, access] + Paths []string `yaml:"paths"` // Protected paths + // Rule contains rule type of + Rule interface{} `yaml:"rule"` // Middleware rule +} +type MiddlewareName struct { + name string `yaml:"name"` +} + +// Route defines gateway route +type Route struct { + // Name defines route name + Name string `yaml:"name"` + //Host Domain/host based request routing + Host string `yaml:"host"` + // Path defines route path + Path string `yaml:"path"` + // Rewrite rewrites route path to desired path + // + // E.g. /cart to / => It will rewrite /cart path to / + Rewrite string `yaml:"rewrite"` + // Destination Defines backend URL + Destination string `yaml:"destination"` + // Cors contains the route cors headers + Cors Cors `yaml:"cors"` + // 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"` + // Middlewares Defines route middleware from Middleware names + Middlewares []string `yaml:"middlewares"` +} + +// Gateway contains Goma Proxy Gateway's configs +type Gateway struct { + // ListenAddr Defines the server listenAddr + // + //e.g: localhost:8080 + ListenAddr string `yaml:"listenAddr" env:"GOMA_LISTEN_ADDR, overwrite"` + // WriteTimeout defines proxy write timeout + WriteTimeout int `yaml:"writeTimeout" env:"GOMA_WRITE_TIMEOUT, overwrite"` + // ReadTimeout defines proxy read timeout + ReadTimeout int `yaml:"readTimeout" env:"GOMA_READ_TIMEOUT, overwrite"` + // IdleTimeout defines proxy idle timeout + IdleTimeout int `yaml:"idleTimeout" env:"GOMA_IDLE_TIMEOUT, overwrite"` + // RateLimiter Defines number of request peer minute + RateLimiter int `yaml:"rateLimiter" env:"GOMA_RATE_LIMITER, overwrite"` + AccessLog string `yaml:"accessLog" env:"GOMA_ACCESS_LOG, overwrite"` + ErrorLog string `yaml:"errorLog" env:"GOMA_ERROR_LOG=, overwrite"` + // DisableHealthCheckStatus enable and disable routes health check + DisableHealthCheckStatus bool `yaml:"disableHealthCheckStatus"` + // DisableRouteHealthCheckError allows enabling and disabling backend healthcheck errors + DisableRouteHealthCheckError bool `yaml:"disableRouteHealthCheckError"` + //Disable allows enabling and disabling displaying routes on start + DisableDisplayRouteOnStart bool `yaml:"disableDisplayRouteOnStart"` + // DisableKeepAlive allows enabling and disabling KeepALive server + DisableKeepAlive bool `yaml:"disableKeepAlive"` + // InterceptErrors holds the status codes to intercept the error from backend + InterceptErrors []int `yaml:"interceptErrors"` + // Cors holds proxy global cors + Cors Cors `yaml:"cors"` + // Routes holds proxy routes + Routes []Route `yaml:"routes"` +} +type GatewayConfig struct { + // GatewayConfig holds Gateway config + GatewayConfig Gateway `yaml:"gateway"` + // Middlewares holds proxy middlewares + Middlewares []Middleware `yaml:"middlewares"` +} + +// ErrorResponse represents the structure of the JSON error response +type ErrorResponse struct { + Success bool `json:"success"` + Code int `json:"code"` + Message string `json:"message"` +} +type GatewayServer struct { + ctx context.Context + gateway Gateway + middlewares []Middleware +} +type ProxyRoute struct { + path string + rewrite string + destination string + cors Cors + disableXForward bool +} +type RoutePath struct { + route Route + path string + rules []string + middlewares []Middleware + router *mux.Router +} + +type HealthCheckRoute struct { + DisableRouteHealthCheckError bool + Routes []Route +} + +// HealthCheckResponse represents the health check response structure +type HealthCheckResponse struct { + Status string `json:"status"` + Routes []HealthCheckRouteResponse `json:"routes"` +} + +// HealthCheckRouteResponse represents the health check response for a route +type HealthCheckRouteResponse struct { + Name string `json:"name"` + Status string `json:"status"` + Error string `json:"error"` +}