feat: add proxy backend errors interceptor
This commit is contained in:
@@ -125,6 +125,10 @@ type Route struct {
|
||||
HealthCheck string `yaml:"healthCheck"`
|
||||
// Blocklist Defines route blacklist
|
||||
Blocklist []string `yaml:"blocklist"`
|
||||
// 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 []RouteMiddleware `yaml:"middlewares"`
|
||||
}
|
||||
@@ -147,7 +151,8 @@ type Gateway struct {
|
||||
ErrorLog string `yaml:"errorLog" env:"GOMA_ERROR_LOG=, overwrite"`
|
||||
DisableRouteHealthCheckError bool `yaml:"disableRouteHealthCheckError"`
|
||||
//Disable dispelling routes on start
|
||||
DisableDisplayRouteOnStart bool `yaml:"disableDisplayRouteOnStart"`
|
||||
DisableDisplayRouteOnStart bool `yaml:"disableDisplayRouteOnStart"`
|
||||
InterceptErrors []int `yaml:"interceptErrors"`
|
||||
// Cors contains the proxy global cors
|
||||
Cors Cors `yaml:"cors"`
|
||||
// Routes defines the proxy routes
|
||||
@@ -351,7 +356,7 @@ func ToJWTRuler(input interface{}) (JWTRuler, error) {
|
||||
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{}, fmt.Errorf("error parsing yaml: empty url in %s auth middleware", jwtAuth)
|
||||
|
||||
}
|
||||
return *jWTRuler, nil
|
||||
@@ -369,7 +374,7 @@ func ToBasicAuth(input interface{}) (BasicRule, error) {
|
||||
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 BasicRule{}, fmt.Errorf("error parsing yaml: empty username/password in %s middleware", basicAuth)
|
||||
|
||||
}
|
||||
return *basicAuth, nil
|
||||
|
||||
94
pkg/middleware/error-interceptor.go
Normal file
94
pkg/middleware/error-interceptor.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package middleware
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*
|
||||
*/
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"github.com/jkaninda/goma-gateway/internal/logger"
|
||||
"io"
|
||||
"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,
|
||||
statusCode: http.StatusOK,
|
||||
body: &bytes.Buffer{},
|
||||
}
|
||||
}
|
||||
|
||||
func (rec *responseRecorder) WriteHeader(code int) {
|
||||
rec.statusCode = code
|
||||
}
|
||||
|
||||
func (rec *responseRecorder) Write(data []byte) (int, error) {
|
||||
return rec.body.Write(data)
|
||||
}
|
||||
|
||||
// ErrorInterceptor Middleware intercepts backend errors
|
||||
func (intercept InterceptErrors) ErrorInterceptor(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
rec := newResponseRecorder(w)
|
||||
next.ServeHTTP(rec, r)
|
||||
if canIntercept(rec.statusCode, intercept.Errors) {
|
||||
logger.Debug("Backend error intercepted")
|
||||
logger.Debug("An error occurred in the backend, %d", rec.statusCode)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(rec.statusCode)
|
||||
err := json.NewEncoder(w).Encode(ProxyResponseError{
|
||||
Success: false,
|
||||
Code: rec.statusCode,
|
||||
Message: http.StatusText(rec.statusCode),
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
// No error: write buffered response to client
|
||||
w.WriteHeader(rec.statusCode)
|
||||
_, err := io.Copy(w, rec.body)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
func canIntercept(code int, errors []int) bool {
|
||||
for _, er := range errors {
|
||||
if er == code {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -94,7 +94,9 @@ type AuthBasic struct {
|
||||
Params map[string]string
|
||||
}
|
||||
|
||||
// AuthMiddleware function, which will be called for each request
|
||||
// AuthMiddleware authenticate the client using JWT
|
||||
//
|
||||
// authorization based on the result of backend's response and continue the request when the client is authorized
|
||||
func (amw AuthJWT) AuthMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
for _, header := range amw.RequiredHeaders {
|
||||
|
||||
@@ -66,7 +66,7 @@ func (rl *RateLimiter) RateLimitMiddleware() mux.MiddlewareFunc {
|
||||
rl.mu.Unlock()
|
||||
|
||||
if client.RequestCount > rl.Requests {
|
||||
logger.Warn("Too many request from IP: %s %s %s", clientID, r.URL, r.UserAgent())
|
||||
logger.Debug("Too many request 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{
|
||||
@@ -47,7 +47,7 @@ func (proxyRoute ProxyRoute) ProxyHandler() http.HandlerFunc {
|
||||
//Update Origin Cors Headers
|
||||
for _, origin := range proxyRoute.cors.Origins {
|
||||
if origin == r.Header.Get("Origin") {
|
||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||
w.Header().Set(accessControlAllowOrigin, origin)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -90,12 +90,6 @@ func (proxyRoute ProxyRoute) ProxyHandler() http.HandlerFunc {
|
||||
r.URL.Path = strings.Replace(r.URL.Path, fmt.Sprintf("%s/", proxyRoute.path), proxyRoute.rewrite, 1)
|
||||
}
|
||||
}
|
||||
proxy.ModifyResponse = func(response *http.Response) error {
|
||||
if response.StatusCode < 200 || response.StatusCode >= 300 {
|
||||
//TODO || Add override backend errors | user can enable or disable it
|
||||
}
|
||||
return nil
|
||||
}
|
||||
// Custom error handler for proxy errors
|
||||
proxy.ErrorHandler = ProxyErrorHandler
|
||||
proxy.ServeHTTP(w, r)
|
||||
|
||||
16
pkg/route.go
16
pkg/route.go
@@ -40,6 +40,8 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router {
|
||||
// Add rate limit middleware to all routes, if defined
|
||||
r.Use(limiter.RateLimitMiddleware())
|
||||
}
|
||||
// Add the errorInterceptor middleware
|
||||
//r.Use(middleware.ErrorInterceptor)
|
||||
for _, route := range gateway.Routes {
|
||||
if route.Path != "" {
|
||||
blM := middleware.BlockListMiddleware{
|
||||
@@ -63,7 +65,7 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router {
|
||||
} else {
|
||||
//Check Authentication middleware
|
||||
switch rMiddleware.Type {
|
||||
case "basic":
|
||||
case basicAuth, "basic":
|
||||
basicAuth, err := ToBasicAuth(rMiddleware.Rule)
|
||||
if err != nil {
|
||||
logger.Error("Error: %s", err.Error())
|
||||
@@ -80,7 +82,7 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router {
|
||||
secureRouter.PathPrefix("/").Handler(proxyRoute.ProxyHandler()) // Proxy handler
|
||||
secureRouter.PathPrefix("").Handler(proxyRoute.ProxyHandler()) // Proxy handler
|
||||
}
|
||||
case "jwt":
|
||||
case jwtAuth, "jwt":
|
||||
jwt, err := ToJWTRuler(rMiddleware.Rule)
|
||||
if err != nil {
|
||||
logger.Error("Error: %s", err.Error())
|
||||
@@ -98,6 +100,9 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router {
|
||||
secureRouter.PathPrefix("").Handler(proxyRoute.ProxyHandler()) // Proxy handler
|
||||
|
||||
}
|
||||
case OAuth, "auth0":
|
||||
logger.Error("OAuth is not yet implemented")
|
||||
logger.Info("Auth middleware ignored")
|
||||
default:
|
||||
logger.Error("Unknown middleware type %s", rMiddleware.Type)
|
||||
|
||||
@@ -129,11 +134,16 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router {
|
||||
}
|
||||
} else {
|
||||
logger.Error("Error, path is empty in route %s", route.Name)
|
||||
logger.Info("Route path ignored: %s", route.Path)
|
||||
logger.Debug("Route path ignored: %s", route.Path)
|
||||
}
|
||||
}
|
||||
// Apply global Cors middlewares
|
||||
r.Use(CORSHandler(gateway.Cors)) // Apply CORS middleware
|
||||
// Apply errorInterceptor middleware
|
||||
interceptErrors := middleware.InterceptErrors{
|
||||
Errors: gateway.InterceptErrors,
|
||||
}
|
||||
r.Use(interceptErrors.ErrorInterceptor)
|
||||
return r
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
package pkg
|
||||
|
||||
const ConfigFile = "/config/goma.yml"
|
||||
const accessControlAllowOrigin = "Access-Control-Allow-Origin"
|
||||
const basicAuth = "basicAuth"
|
||||
const jwtAuth = "jwtAuth"
|
||||
const OAuth = "OAuth"
|
||||
|
||||
Reference in New Issue
Block a user