refactor: move types into a single file for each package

This commit is contained in:
2024-11-04 08:34:47 +01:00
parent b0443c34d7
commit 096290bcb8
9 changed files with 326 additions and 283 deletions

View File

@@ -16,7 +16,6 @@ See the License for the specific language governing permissions and
limitations under the License. limitations under the License.
*/ */
import ( import (
"context"
"fmt" "fmt"
"github.com/jkaninda/goma-gateway/internal/logger" "github.com/jkaninda/goma-gateway/internal/logger"
"github.com/jkaninda/goma-gateway/util" "github.com/jkaninda/goma-gateway/util"
@@ -27,165 +26,6 @@ import (
var cfg *Gateway 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 // Config reads config file and returns Gateway
func (GatewayServer) Config(configFile string) (*GatewayServer, error) { func (GatewayServer) Config(configFile string) (*GatewayServer, error) {
if util.FileExists(configFile) { if util.FileExists(configFile) {

View File

@@ -23,22 +23,6 @@ import (
"net/url" "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 { func HealthCheck(healthURL string) error {
healthCheckURL, err := url.Parse(healthURL) healthCheckURL, err := url.Parse(healthURL)
if err != nil { if err != nil {

View File

@@ -2,7 +2,6 @@ package pkg
import ( import (
"errors" "errors"
"github.com/gorilla/mux"
"slices" "slices"
"strings" "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, ";") + "]") 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 { func doesExist(tyName string) bool {
middlewareList := []string{BasicAuth, JWTAuth, AccessMiddleware} middlewareList := []string{BasicAuth, JWTAuth, AccessMiddleware}
if slices.Contains(middlewareList, tyName) { if slices.Contains(middlewareList, tyName) {

View File

@@ -24,18 +24,6 @@ import (
"net/http" "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 { func newResponseRecorder(w http.ResponseWriter) *responseRecorder {
return &responseRecorder{ return &responseRecorder{
ResponseWriter: w, ResponseWriter: w,
@@ -59,7 +47,7 @@ func (intercept InterceptErrors) ErrorInterceptor(next http.Handler) http.Handle
next.ServeHTTP(rec, r) next.ServeHTTP(rec, r)
if canIntercept(rec.statusCode, intercept.Errors) { if canIntercept(rec.statusCode, intercept.Errors) {
logger.Error("Backend error") 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.Header().Set("Content-Type", "application/json")
w.WriteHeader(rec.statusCode) w.WriteHeader(rec.statusCode)
err := json.NewEncoder(w).Encode(ProxyResponseError{ err := json.NewEncoder(w).Encode(ProxyResponseError{

View File

@@ -23,78 +23,8 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"strings" "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 // AuthMiddleware authenticate the client using JWT
// //
// authorization based on the result of backend's response and continue the request when the client is authorized // authorization based on the result of backend's response and continue the request when the client is authorized

View File

@@ -34,7 +34,7 @@ func (rl *TokenRateLimiter) RateLimitMiddleware() mux.MiddlewareFunc {
err := json.NewEncoder(w).Encode(ProxyResponseError{ err := json.NewEncoder(w).Encode(ProxyResponseError{
Success: false, Success: false,
Code: http.StatusTooManyRequests, 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 { if err != nil {
return return
@@ -66,13 +66,13 @@ func (rl *RateLimiter) RateLimitMiddleware() mux.MiddlewareFunc {
rl.mu.Unlock() rl.mu.Unlock()
if client.RequestCount > rl.Requests { 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.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusTooManyRequests) w.WriteHeader(http.StatusTooManyRequests)
err := json.NewEncoder(w).Encode(ProxyResponseError{ err := json.NewEncoder(w).Encode(ProxyResponseError{
Success: false, Success: false,
Code: http.StatusTooManyRequests, 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 { if err != nil {
return return

105
pkg/middleware/types.go Normal file
View File

@@ -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
}

View File

@@ -25,19 +25,10 @@ import (
"strings" "strings"
) )
type ProxyRoute struct {
path string
rewrite string
destination string
cors Cors
disableXForward bool
}
// ProxyHandler proxies requests to the backend // ProxyHandler proxies requests to the backend
func (proxyRoute ProxyRoute) ProxyHandler() http.HandlerFunc { func (proxyRoute ProxyRoute) ProxyHandler() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
realIP := getRealIP(r) logger.Info("%s %s %s %s", r.Method, getRealIP(r), r.URL, r.UserAgent())
logger.Info("%s %s %s %s", r.Method, realIP, r.URL, r.UserAgent())
// Set CORS headers from the cors config // Set CORS headers from the cors config
//Update Cors Headers //Update Cors Headers
for k, v := range proxyRoute.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.Host = targetURL.Host
r.URL.Scheme = targetURL.Scheme r.URL.Scheme = targetURL.Scheme
r.Header.Set("X-Forwarded-Host", r.Header.Get("Host")) r.Header.Set("X-Forwarded-Host", r.Header.Get("Host"))
r.Header.Set("X-Forwarded-For", realIP) r.Header.Set("X-Forwarded-For", getRealIP(r))
r.Header.Set("X-Real-IP", realIP) r.Header.Set("X-Real-IP", getRealIP(r))
r.Host = targetURL.Host r.Host = targetURL.Host
} }
// Create proxy // Create proxy

214
pkg/types.go Normal file
View File

@@ -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"`
}