Files
goma-gateway/pkg/config.go

391 lines
12 KiB
Go
Raw Normal View History

2024-10-27 06:10:27 +01:00
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 BasicRule 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"`
}
// JWTRuler authentication using HTTP GET method
//
// JWTRuler contains the authentication details
type JWTRuler 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"`
}
// 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
Type string `yaml:"type"`
// Rule contains rule type of
Rule interface{} `yaml:"rule"`
}
type MiddlewareName struct {
name string `yaml:"name"`
}
type RouteMiddleware struct {
//Path contains the path to protect
Path string `yaml:"path"`
//Rules defines which specific middleware applies to a route path
Rules []string `yaml:"rules"`
}
// 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"`
2024-10-27 06:10:27 +01:00
// 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"`
// 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"`
2024-10-27 06:10:27 +01:00
// Middlewares Defines route middleware from Middleware names
Middlewares []RouteMiddleware `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 bool `yaml:"disableRouteHealthCheckError"`
//Disable dispelling routes on start
DisableDisplayRouteOnStart bool `yaml:"disableDisplayRouteOnStart"`
InterceptErrors []int `yaml:"interceptErrors"`
EnableKeepAlive bool `yaml:"enableKeepAlive"`
2024-10-27 06:10:27 +01:00
// Cors contains the proxy global cors
Cors Cors `yaml:"cors"`
// Routes defines the proxy routes
Routes []Route `yaml:"routes"`
}
type GatewayConfig struct {
GatewayConfig Gateway `yaml:"gateway"`
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 {
2024-10-27 07:24:50 +01:00
return nil, fmt.Errorf("error parsing yaml %q: %w", configFile, err)
2024-10-27 06:10:27 +01:00
}
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,
2024-10-29 14:21:55 +01:00
InterceptErrors: []int{405, 500},
2024-10-27 06:10:27 +01:00
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{
{
2024-10-29 14:21:55 +01:00
Name: "Public",
Path: "/public",
Destination: "http://localhost:80",
Rewrite: "/healthz",
2024-10-27 06:10:27 +01:00
HealthCheck: "",
},
{
Name: "Basic auth",
Path: "/protected",
Destination: "https://example.com",
Rewrite: "/",
2024-10-27 06:10:27 +01:00
HealthCheck: "",
Blocklist: []string{},
2024-10-28 10:17:55 +01:00
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",
},
},
2024-10-27 06:10:27 +01:00
Middlewares: []RouteMiddleware{
{
Path: "/user",
Rules: []string{"basic-auth"},
2024-10-27 06:10:27 +01:00
},
},
},
2024-10-29 14:21:55 +01:00
{
Name: "Hostname example",
Host: "example.com",
Path: "/",
Destination: "https://example.com",
Rewrite: "/",
HealthCheck: "",
},
2024-10-27 06:10:27 +01:00
},
},
Middlewares: []Middleware{
{
Name: "basic-auth",
2024-10-29 22:55:09 +01:00
Type: "basic",
2024-10-27 06:10:27 +01:00
Rule: BasicRule{
Username: "goma",
Password: "goma",
},
}, {
Name: "jwt",
2024-10-29 22:55:09 +01:00
Type: "jwt",
2024-10-27 06:10:27 +01:00
Rule: JWTRuler{
URL: "https://www.googleapis.com/auth/userinfo.email",
RequiredHeaders: []string{
"Authorization",
},
2024-10-27 06:10:27 +01:00
Headers: map[string]string{},
Params: map[string]string{},
},
},
},
}
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{}
}
func (middleware Middleware) name() {
}
func ToJWTRuler(input interface{}) (JWTRuler, error) {
jWTRuler := new(JWTRuler)
var bytes []byte
bytes, err := yaml.Marshal(input)
if err != nil {
2024-10-27 07:24:50 +01:00
return JWTRuler{}, fmt.Errorf("error parsing yaml: %v", err)
2024-10-27 06:10:27 +01:00
}
err = yaml.Unmarshal(bytes, jWTRuler)
if err != nil {
2024-10-27 07:24:50 +01:00
return JWTRuler{}, fmt.Errorf("error parsing yaml: %v", err)
}
if jWTRuler.URL == "" {
return JWTRuler{}, fmt.Errorf("error parsing yaml: empty url in %s auth middleware", jwtAuth)
2024-10-27 07:24:50 +01:00
2024-10-27 06:10:27 +01:00
}
return *jWTRuler, nil
}
func ToBasicAuth(input interface{}) (BasicRule, error) {
basicAuth := new(BasicRule)
var bytes []byte
bytes, err := yaml.Marshal(input)
if err != nil {
2024-10-27 07:24:50 +01:00
return BasicRule{}, fmt.Errorf("error parsing yaml: %v", err)
2024-10-27 06:10:27 +01:00
}
err = yaml.Unmarshal(bytes, basicAuth)
if err != nil {
2024-10-27 07:24:50 +01:00
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 %s middleware", basicAuth)
2024-10-27 07:24:50 +01:00
2024-10-27 06:10:27 +01:00
}
return *basicAuth, nil
}