Files
goma-gateway/pkg/config.go

411 lines
12 KiB
Go

package pkg
/*
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 (
"context"
"fmt"
"github.com/jkaninda/goma-gateway/internal/logger"
"github.com/jkaninda/goma-gateway/util"
"github.com/spf13/cobra"
"gopkg.in/yaml.v3"
"os"
)
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"`
// 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) {
buf, err := os.ReadFile(configFile)
if err != nil {
return nil, err
}
util.SetEnv("GOMA_CONFIG_FILE", configFile)
c := &GatewayConfig{}
err = yaml.Unmarshal(buf, c)
if err != nil {
return nil, fmt.Errorf("error parsing yaml %q: %w", configFile, err)
}
return &GatewayServer{
ctx: nil,
gateway: c.GatewayConfig,
middlewares: c.Middlewares,
}, nil
}
logger.Error("Configuration file not found: %v", configFile)
logger.Info("Generating new configuration file...")
initConfig(ConfigFile)
logger.Info("Server configuration file is available at %s", ConfigFile)
util.SetEnv("GOMA_CONFIG_FILE", ConfigFile)
buf, err := os.ReadFile(ConfigFile)
if err != nil {
return nil, err
}
c := &GatewayConfig{}
err = yaml.Unmarshal(buf, c)
if err != nil {
return nil, fmt.Errorf("in file %q: %w", ConfigFile, err)
}
logger.Info("Generating new configuration file...done")
logger.Info("Starting server with default configuration")
return &GatewayServer{
ctx: nil,
gateway: c.GatewayConfig,
middlewares: c.Middlewares,
}, nil
}
func GetConfigPaths() string {
return util.GetStringEnv("GOMAY_CONFIG_FILE", ConfigFile)
}
func InitConfig(cmd *cobra.Command) {
configFile, _ := cmd.Flags().GetString("output")
if configFile == "" {
configFile = GetConfigPaths()
}
initConfig(configFile)
return
}
func initConfig(configFile string) {
if configFile == "" {
configFile = GetConfigPaths()
}
conf := &GatewayConfig{
GatewayConfig: Gateway{
ListenAddr: "0.0.0.0:80",
WriteTimeout: 15,
ReadTimeout: 15,
IdleTimeout: 60,
AccessLog: "/dev/Stdout",
ErrorLog: "/dev/stderr",
DisableRouteHealthCheckError: false,
DisableDisplayRouteOnStart: false,
RateLimiter: 0,
InterceptErrors: []int{405, 500},
Cors: Cors{
Origins: []string{"http://localhost:8080", "https://example.com"},
Headers: map[string]string{
"Access-Control-Allow-Headers": "Origin, Authorization, Accept, Content-Type, Access-Control-Allow-Headers, X-Client-Id, X-Session-Id",
"Access-Control-Allow-Credentials": "true",
"Access-Control-Max-Age": "1728000",
},
},
Routes: []Route{
{
Name: "Public",
Path: "/public",
Destination: "https://example.com",
Rewrite: "/",
HealthCheck: "",
Middlewares: []string{"api-forbidden-paths"},
},
{
Name: "Basic auth",
Path: "/protected",
Destination: "https://example.com",
Rewrite: "/",
HealthCheck: "",
Cors: Cors{
Origins: []string{"http://localhost:3000", "https://dev.example.com"},
Headers: map[string]string{
"Access-Control-Allow-Headers": "Origin, Authorization",
"Access-Control-Allow-Credentials": "true",
"Access-Control-Max-Age": "1728000",
},
},
Middlewares: []string{"basic-auth", "api-forbidden-paths"},
},
{
Name: "Hostname example",
Path: "/",
Destination: "https://example.com",
Rewrite: "/",
HealthCheck: "",
},
},
},
Middlewares: []Middleware{
{
Name: "basic-auth",
Type: BasicAuth,
Paths: []string{
"/*",
},
Rule: BasicRuleMiddleware{
Username: "admin",
Password: "admin",
},
}, {
Name: "jwt",
Type: JWTAuth,
Paths: []string{
"/protected-access",
"/example-of-jwt",
},
Rule: JWTRuleMiddleware{
URL: "https://www.googleapis.com/auth/userinfo.email",
RequiredHeaders: []string{
"Authorization",
},
Headers: map[string]string{},
Params: map[string]string{},
},
},
{
Name: "api-forbidden-paths",
Type: AccessMiddleware,
Paths: []string{
"/swagger-ui/*",
"/v2/swagger-ui/*",
"/api-docs/*",
"/internal/*",
"/actuator/*",
},
},
},
}
yamlData, err := yaml.Marshal(&conf)
if err != nil {
logger.Fatal("Error serializing configuration %v", err.Error())
}
err = os.WriteFile(configFile, yamlData, 0644)
if err != nil {
logger.Fatal("Unable to write config file %s", err)
}
logger.Info("Configuration file has been initialized successfully")
}
func Get() *Gateway {
if cfg == nil {
c := &Gateway{}
c.Setup(GetConfigPaths())
cfg = c
}
return cfg
}
func (Gateway) Setup(conf string) *Gateway {
if util.FileExists(conf) {
buf, err := os.ReadFile(conf)
if err != nil {
return &Gateway{}
}
util.SetEnv("GOMA_CONFIG_FILE", conf)
c := &GatewayConfig{}
err = yaml.Unmarshal(buf, c)
if err != nil {
logger.Fatal("Error loading configuration %v", err.Error())
}
return &c.GatewayConfig
}
return &Gateway{}
}
// getJWTMiddleware returns JWTRuleMiddleware,error
func getJWTMiddleware(input interface{}) (JWTRuleMiddleware, error) {
jWTRuler := new(JWTRuleMiddleware)
var bytes []byte
bytes, err := yaml.Marshal(input)
if err != nil {
return JWTRuleMiddleware{}, fmt.Errorf("error parsing yaml: %v", err)
}
err = yaml.Unmarshal(bytes, jWTRuler)
if err != nil {
return JWTRuleMiddleware{}, fmt.Errorf("error parsing yaml: %v", err)
}
if jWTRuler.URL == "" {
return JWTRuleMiddleware{}, fmt.Errorf("error parsing yaml: empty url in jwt auth middleware")
}
return *jWTRuler, nil
}
// getBasicAuthMiddleware returns BasicRuleMiddleware,error
func getBasicAuthMiddleware(input interface{}) (BasicRuleMiddleware, error) {
basicAuth := new(BasicRuleMiddleware)
var bytes []byte
bytes, err := yaml.Marshal(input)
if err != nil {
return BasicRuleMiddleware{}, fmt.Errorf("error parsing yaml: %v", err)
}
err = yaml.Unmarshal(bytes, basicAuth)
if err != nil {
return BasicRuleMiddleware{}, fmt.Errorf("error parsing yaml: %v", err)
}
if basicAuth.Username == "" || basicAuth.Password == "" {
return BasicRuleMiddleware{}, fmt.Errorf("error parsing yaml: empty username/password in %s middleware", basicAuth)
}
return *basicAuth, nil
}