Merge pull request #105 from jkaninda/refactor

chore: add skip SSL certificate verification
This commit is contained in:
2024-11-15 09:02:43 +01:00
committed by GitHub
23 changed files with 281 additions and 183 deletions

View File

@@ -1,4 +1,4 @@
# .github/workflows/release.yaml
name: Bin release
on:
release:
types: [created]

23
.github/workflows/lint.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
name: Lint
on:
push:
pull_request:
jobs:
lint:
name: Run on Ubuntu
runs-on: ubuntu-latest
steps:
- name: Clone the code
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '~1.22'
- name: Run linter
uses: golangci/golangci-lint-action@v6
with:
version: v1.61

View File

@@ -1,4 +1,4 @@
name: CI
name: Release
on:
push:
tags:

View File

@@ -1,4 +1,4 @@
name: Go
name: Test
on:
push:

View File

@@ -135,6 +135,7 @@ gateway:
- https://example.com
- https://example2.com
- https://example4.com
insecureSkipVerify: true
methods: []
healthCheck:
path: "/health/live"

View File

@@ -87,6 +87,7 @@ gateway:
backends:
- https://example.com
- https://example2.com
insecureSkipVerify: true
methods:
- GET
# Route healthcheck

View File

@@ -30,8 +30,6 @@ import (
"os"
)
var cfg *Gateway
// Config reads config file and returns Gateway
func (GatewayServer) Config(configFile string) (*GatewayServer, error) {
if util.FileExists(configFile) {

37
internal/cors_type.go Normal file
View File

@@ -0,0 +1,37 @@
/*
* 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
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"`
}

57
internal/gateway_type.go Normal file
View File

@@ -0,0 +1,57 @@
/*
* 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
// Gateway contains Goma Proxy Gateway's configs
type Gateway struct {
// SSLCertFile SSL Certificate file
SSLCertFile string `yaml:"sslCertFile" env:"GOMA_SSL_CERT_FILE, overwrite"`
// SSLKeyFile SSL Private key file
SSLKeyFile string `yaml:"sslKeyFile" env:"GOMA_SSL_KEY_FILE, overwrite"`
// Redis contains redis database details
Redis Redis `yaml:"redis"`
// 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"`
// RateLimit Defines the number of request peer minutes
RateLimit int `yaml:"rateLimit" env:"GOMA_RATE_LIMIT, overwrite"`
// BlockCommonExploits enable, disable block common exploits
BlockCommonExploits bool `yaml:"blockCommonExploits"`
AccessLog string `yaml:"accessLog" env:"GOMA_ACCESS_LOG, overwrite"`
ErrorLog string `yaml:"errorLog" env:"GOMA_ERROR_LOG=, overwrite"`
LogLevel string `yaml:"logLevel" env:"GOMA_LOG_LEVEL, 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"`
EnableMetrics bool `yaml:"enableMetrics"`
// 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"`
}

View File

@@ -18,6 +18,7 @@ limitations under the License.
import (
"context"
"encoding/json"
"fmt"
"github.com/gorilla/mux"
"github.com/jkaninda/goma-gateway/pkg/logger"
"net/http"
@@ -56,32 +57,33 @@ func CORSHandler(cors Cors) mux.MiddlewareFunc {
func ProxyErrorHandler(w http.ResponseWriter, r *http.Request, err error) {
logger.Error("Proxy error: %v", err)
w.WriteHeader(http.StatusBadGateway)
_, err = w.Write([]byte("Bad Gateway"))
_, err = w.Write([]byte(fmt.Sprintf("%d %s ", http.StatusBadGateway, http.StatusText(http.StatusBadGateway))))
if err != nil {
return
}
return
}
// HealthCheckHandler handles health check of routes
func (heathRoute HealthCheckRoute) HealthCheckHandler(w http.ResponseWriter, r *http.Request) {
logger.Debug("%s %s %s %s", r.Method, r.RemoteAddr, r.URL, r.UserAgent())
healthRoutes := healthCheckRoutes(heathRoute.Routes)
wg := sync.WaitGroup{}
wg.Add(len(heathRoute.Routes))
wg.Add(len(healthRoutes))
var routes []HealthCheckRouteResponse
for _, health := range healthCheckRoutes(heathRoute.Routes) {
for _, health := range healthRoutes {
go func() {
defer wg.Done()
err := health.Check()
if err != nil {
if heathRoute.DisableRouteHealthCheckError {
routes = append(routes, HealthCheckRouteResponse{Name: health.Name, Status: "unhealthy", Error: "Route healthcheck errors disabled"})
} else {
routes = append(routes, HealthCheckRouteResponse{Name: health.Name, Status: "unhealthy", Error: "Error: " + err.Error()})
}
routes = append(routes, HealthCheckRouteResponse{Name: health.Name, Status: "unhealthy", Error: "Error: " + err.Error()})
} else {
logger.Debug("Route %s is healthy", health.Name)
routes = append(routes, HealthCheckRouteResponse{Name: health.Name, Status: "healthy", Error: ""})
}
defer wg.Done()
}()

View File

@@ -16,6 +16,7 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import (
"crypto/tls"
"fmt"
"github.com/jkaninda/goma-gateway/pkg/logger"
"github.com/jkaninda/goma-gateway/util"
@@ -36,8 +37,14 @@ func (health Health) Check() error {
if err != nil {
return fmt.Errorf("error route %s: creating HealthCheck request: %v ", health.Name, err)
}
// Create custom transport with TLS configuration
transport := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: health.InsecureSkipVerify, // Skip SSL certificate verification
},
}
// Perform the request to the route's healthcheck
client := &http.Client{Timeout: health.TimeOut}
client := &http.Client{Transport: transport, Timeout: health.TimeOut}
healthResp, err := client.Do(healthReq)
if err != nil {
logger.Debug("Error route %s: performing HealthCheck request: %v ", health.Name, err)
@@ -46,6 +53,7 @@ func (health Health) Check() error {
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
logger.Debug("Error performing HealthCheck request: %v ", err)
}
}(healthResp.Body)
if len(health.HealthyStatuses) > 0 {

View File

@@ -118,20 +118,22 @@ func healthCheckRoutes(routes []Route) []Health {
if len(route.Backends) > 0 {
for index, backend := range route.Backends {
health := Health{
Name: fmt.Sprintf("%s - [%d]", route.Name, index),
URL: backend + route.HealthCheck.Path,
TimeOut: timeout,
HealthyStatuses: route.HealthCheck.HealthyStatuses,
Name: fmt.Sprintf("%s - [%d]", route.Name, index),
URL: backend + route.HealthCheck.Path,
TimeOut: timeout,
HealthyStatuses: route.HealthCheck.HealthyStatuses,
InsecureSkipVerify: route.InsecureSkipVerify,
}
healthRoutes = append(healthRoutes, health)
}
} else {
health := Health{
Name: route.Name,
URL: route.Destination + route.HealthCheck.Path,
TimeOut: timeout,
HealthyStatuses: route.HealthCheck.HealthyStatuses,
Name: route.Name,
URL: route.Destination + route.HealthCheck.Path,
TimeOut: timeout,
HealthyStatuses: route.HealthCheck.HealthyStatuses,
InsecureSkipVerify: route.InsecureSkipVerify,
}
healthRoutes = append(healthRoutes, health)
}

View File

@@ -19,11 +19,7 @@ func getMiddleware(rules []string, middlewares []Middleware) (Middleware, error)
func doesExist(tyName string) bool {
middlewareList := []string{BasicAuth, JWTAuth, AccessMiddleware}
if slices.Contains(middlewareList, tyName) {
return true
}
return false
return slices.Contains(middlewareList, tyName)
}
func GetMiddleware(rule string, middlewares []Middleware) (Middleware, error) {
for _, m := range middlewares {

View File

@@ -79,7 +79,7 @@ func (jwtAuth JwtAuth) AuthMiddleware(next http.Handler) http.Handler {
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
logger.Error("Error closing body: %v", err)
}
}(authResp.Body)
// Inject specific header tp the current request's header

View File

@@ -26,13 +26,13 @@ import (
// RateLimiter defines requests limit properties.
type RateLimiter struct {
requests int
id string
window time.Duration
clientMap map[string]*Client
mu sync.Mutex
origins []string
hosts []string
requests int
id string
window time.Duration
clientMap map[string]*Client
mu sync.Mutex
origins []string
//hosts []string
redisBased bool
}

View File

@@ -0,0 +1,31 @@
/*
* 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
// 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
}

View File

@@ -236,10 +236,15 @@ func (gatewayServer GatewayServer) Initialize() *mux.Router {
logger.Info("Block common exploits enabled")
router.Use(middleware.BlockExploitsMiddleware)
}
id := string(rune(rIndex))
if len(route.Name) != 0 {
// Use route name as ID
id = util.Slug(route.Name)
}
// Apply route rate limit
if route.RateLimit > 0 {
rateLimit := middleware.RateLimit{
Id: string(rune(rIndex)), // Use route index as ID
Id: id, // Use route index as ID
Requests: route.RateLimit,
Window: time.Minute, // requests per minute
Origins: route.Cors.Origins,

58
internal/route_type.go Normal file
View File

@@ -0,0 +1,58 @@
/*
* 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
// Route defines gateway route
type Route struct {
// Path defines route path
Path string `yaml:"path"`
// Name defines route name
Name string `yaml:"name"`
//Host Domain/host based request routing
//Host string `yaml:"host"`
//Hosts Domains/hosts based request routing
Hosts []string `yaml:"hosts"`
// Rewrite rewrites route path to desired path
//
// E.g. /cart to / => It will rewrite /cart path to /
Rewrite string `yaml:"rewrite"`
//
// Methods allowed method
Methods []string `yaml:"methods"`
// Destination Defines backend URL
Destination string `yaml:"destination"`
Backends []string `yaml:"backends"`
InsecureSkipVerify bool `yaml:"insecureSkipVerify"`
// HealthCheck Defines the backend is health
HealthCheck RouteHealthCheck `yaml:"healthCheck"`
// Cors contains the route cors headers
Cors Cors `yaml:"cors"`
RateLimit int `yaml:"rateLimit"`
// DisableHostFording Disable X-forwarded header.
//
// [X-Forwarded-Host, X-Forwarded-For, Host, Scheme ]
//
// It will not match the backend route
DisableHostFording bool `yaml:"disableHostFording"`
// InterceptErrors holds the status codes to intercept the error from backend
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"`
}

View File

@@ -24,11 +24,11 @@ func TestCheckConfig(t *testing.T) {
TestInit(t)
err := initConfig(configFile)
if err != nil {
t.Fatalf(err.Error())
t.Fatal("Error init config:", err)
}
err = CheckConfig(configFile)
if err != nil {
t.Fatalf(err.Error())
t.Fatalf("Error checking config: %s", err.Error())
}
log.Println("Goma Gateway configuration file checked successfully")
}
@@ -37,7 +37,7 @@ func TestStart(t *testing.T) {
TestInit(t)
err := initConfig(configFile)
if err != nil {
t.Fatalf(err.Error())
t.Fatalf("Error initializing config: %s", err.Error())
}
g := GatewayServer{}
gatewayServer, err := g.Config(configFile)

View File

@@ -19,37 +19,14 @@ package pkg
import (
"context"
"github.com/gorilla/mux"
"time"
)
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
@@ -114,101 +91,6 @@ 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 {
// Path defines route path
Path string `yaml:"path"`
// Name defines route name
Name string `yaml:"name"`
//Host Domain/host based request routing
//Host string `yaml:"host"`
//Hosts Domains/hosts based request routing
Hosts []string `yaml:"hosts"`
// Rewrite rewrites route path to desired path
//
// E.g. /cart to / => It will rewrite /cart path to /
Rewrite string `yaml:"rewrite"`
//
// Methods allowed method
Methods []string `yaml:"methods"`
// Destination Defines backend URL
Destination string `yaml:"destination"`
Backends []string `yaml:"backends"`
InsecureSkipVerify bool `yaml:"insecureSkipVerify"`
// HealthCheck Defines the backend is health
HealthCheck RouteHealthCheck `yaml:"healthCheck"`
// Cors contains the route cors headers
Cors Cors `yaml:"cors"`
RateLimit int `yaml:"rateLimit"`
// DisableHostFording Disable X-forwarded header.
//
// [X-Forwarded-Host, X-Forwarded-For, Host, Scheme ]
//
// It will not match the backend route
DisableHostFording bool `yaml:"disableHostFording"`
// InterceptErrors holds the status codes to intercept the error from backend
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"`
}
// Gateway contains Goma Proxy Gateway's configs
type Gateway struct {
// SSLCertFile SSL Certificate file
SSLCertFile string `yaml:"sslCertFile" env:"GOMA_SSL_CERT_FILE, overwrite"`
// SSLKeyFile SSL Private key file
SSLKeyFile string `yaml:"sslKeyFile" env:"GOMA_SSL_KEY_FILE, overwrite"`
// Redis contains redis database details
Redis Redis `yaml:"redis"`
// 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"`
// RateLimit Defines the number of request peer minutes
RateLimit int `yaml:"rateLimit" env:"GOMA_RATE_LIMIT, overwrite"`
// BlockCommonExploits enable, disable block common exploits
BlockCommonExploits bool `yaml:"blockCommonExploits"`
AccessLog string `yaml:"accessLog" env:"GOMA_ACCESS_LOG, overwrite"`
ErrorLog string `yaml:"errorLog" env:"GOMA_ERROR_LOG=, overwrite"`
LogLevel string `yaml:"logLevel" env:"GOMA_LOG_LEVEL, 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"`
EnableMetrics bool `yaml:"enableMetrics"`
// 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 RouteHealthCheck struct {
Path string `yaml:"path"`
Interval string `yaml:"interval"`
@@ -236,24 +118,16 @@ type GatewayServer struct {
middlewares []Middleware
}
type ProxyRoute struct {
path string
rewrite string
destination string
backends []string
healthCheck RouteHealthCheck
path string
rewrite string
destination string
backends []string
//healthCheck RouteHealthCheck
methods []string
cors Cors
disableHostFording bool
insecureSkipVerify bool
}
type RoutePath struct {
route Route
path string
rules []string
middlewares []Middleware
router *mux.Router
}
type HealthCheckRoute struct {
DisableRouteHealthCheckError bool
Routes []Route
@@ -282,11 +156,12 @@ type JWTSecret struct {
// Health represents the health check content for a route
type Health struct {
Name string
URL string
TimeOut time.Duration
Interval string
HealthyStatuses []int
Name string
URL string
TimeOut time.Duration
Interval string
HealthyStatuses []int
InsecureSkipVerify bool
}
type Redis struct {
// Addr redis hostname and port number :

View File

@@ -3,16 +3,11 @@ package pkg
const ConfigDir = "/etc/goma/" // Default configuration file
const ConfigFile = "/etc/goma/goma.yml" // Default configuration file
const accessControlAllowOrigin = "Access-Control-Allow-Origin" // Cors
const serverName = "Goma"
const gatewayName = "Goma Gateway"
const AccessMiddleware = "access" // access middleware
const BasicAuth = "basic" // basic authentication middleware
const JWTAuth = "jwt" // JWT authentication middleware
const OAuth = "oauth" // OAuth authentication middleware
const applicationJson = "application/json"
const textPlain = "text/plain"
const applicationXml = "application/xml"
// Round-robin counter
var counter uint32

View File

@@ -26,11 +26,6 @@ import (
"github.com/jkaninda/goma-gateway/util"
)
type Logger struct {
msg string
args interface{}
}
// Info returns info log
func Info(msg string, args ...interface{}) {
log.SetOutput(getStd(util.GetStringEnv("GOMA_ACCESS_LOG", "/dev/stdout")))

View File

@@ -142,3 +142,17 @@ func ParseDuration(durationStr string) (time.Duration, error) {
}
return duration, nil
}
func Slug(text string) string {
// Convert to lowercase
text = strings.ToLower(text)
// Replace spaces and special characters with hyphens
re := regexp.MustCompile(`[^\w]+`)
text = re.ReplaceAllString(text, "-")
// Remove leading and trailing hyphens
text = strings.Trim(text, "-")
return text
}