diff --git a/app/config/config.go b/app/config/config.go index e04d41b..eee2bf9 100644 --- a/app/config/config.go +++ b/app/config/config.go @@ -1,11 +1,73 @@ package config -type Config interface { +type BaseConfiguration interface { GetConfiguration() *Configuration - GetGatewayConfiguration() *GatewayConfiguration - GetFeatures() []string - GetRoutes() []RouteConfiguration - GetApis() []ApiConfiguration - GetHost() string - GetPort() int +} + +type Configuration struct { + Gateway Gateway `yaml:"gateway"` + Hosts []Host `yaml:"hosts"` + Targets []Target `yaml:"targets"` + Apis []Api `yaml:"apis"` + Routes []Route `yaml:"routes"` +} + +type Gateway struct { + Host string `yaml:"host"` + Port int `yaml:"port"` + Features []string `yaml:"features"` +} + +type Host struct { + ID string `yaml:"id"` + Name string `yaml:"name"` + Domain string `yaml:"domain"` + Secure bool `yaml:"secure"` +} + +type Target struct { + ID string `yaml:"id"` + Name string `yaml:"name"` + URL string `yaml:"url"` +} + +type Api struct { + ID string `yaml:"id"` + Host string `yaml:"host"` + Target string `yaml:"target"` +} + +type Route struct { + ID string `yaml:"id"` + Api string `yaml:"api"` + Path string `yaml:"path"` + StripPrefix StripPrefix `yaml:"strip_prefix"` + Security Security `yaml:"security"` +} + +type StripPrefix struct { + Enabled bool `yaml:"enabled"` + Prefix string `yaml:"prefix"` +} + +type Security struct { + Auth Auth `yaml:"auth"` + WAF WAF `yaml:"waf"` +} + +type Auth struct { + Enabled bool `yaml:"enabled"` + Type string `yaml:"type"` + Header string `yaml:"header"` + Path AuthPath `yaml:"path"` +} + +type AuthPath struct { + Include []string `yaml:"include"` + Exclude []string `yaml:"exclude"` +} + +type WAF struct { + Enabled bool `yaml:"enabled"` + Methods []string `yaml:"methods"` } diff --git a/app/config/database.go b/app/config/database.go new file mode 100644 index 0000000..dcc2338 --- /dev/null +++ b/app/config/database.go @@ -0,0 +1,34 @@ +package config + +import "git.secnex.io/secnex/api-gateway/utils" + +type Database struct { + Host string + Port int + User string + Password string + Database string + SSLMode string +} + +func NewDatabaseConfiguration(host string, port int, user string, password string, database string, sslmode string) Database { + return Database{ + Host: host, + Port: port, + User: user, + Password: password, + Database: database, + SSLMode: sslmode, + } +} + +func NewDatabaseConfigurationFromEnv() Database { + return NewDatabaseConfiguration( + utils.GetEnv("DATABASE_HOST", "localhost"), + utils.GetEnvInt("DATABASE_PORT", 5432), + utils.GetEnv("DATABASE_USER", "postgres"), + utils.GetEnv("DATABASE_PASSWORD", "postgres"), + utils.GetEnv("DATABASE_NAME", "secnex"), + utils.GetEnv("DATABASE_SSLMODE", "disable"), + ) +} diff --git a/app/config/file.go b/app/config/file.go index 5046ad0..d2b35cc 100644 --- a/app/config/file.go +++ b/app/config/file.go @@ -6,20 +6,20 @@ import ( "go.yaml.in/yaml/v3" ) -type FileConfig struct { +type File struct { filePath string config *Configuration } -func NewFileConfig(filePath string) (*FileConfig, error) { - c := &FileConfig{filePath: filePath, config: &Configuration{}} +func NewFile(filePath string) (*File, error) { + c := &File{filePath: filePath, config: &Configuration{}} if err := c.loadConfig(); err != nil { return nil, err } return c, nil } -func (c *FileConfig) loadConfig() error { +func (c *File) loadConfig() error { data, err := os.ReadFile(c.filePath) if err != nil { return err @@ -27,34 +27,6 @@ func (c *FileConfig) loadConfig() error { return yaml.Unmarshal(data, c.config) } -func (c *FileConfig) GetConfiguration() *Configuration { +func (c *File) GetConfiguration() *Configuration { return c.config } - -func (c *FileConfig) GetGatewayConfiguration() *GatewayConfiguration { - return &c.config.Gateway -} - -func (c *FileConfig) GetRoutes() []RouteConfiguration { - return c.config.Routes -} - -func (c *FileConfig) GetProxies() []ProxyConfiguration { - return c.config.Proxies -} - -func (c *FileConfig) GetHost() string { - return c.config.Gateway.Host -} - -func (c *FileConfig) GetPort() int { - return c.config.Gateway.Port -} - -func (c *FileConfig) GetApis() []ApiConfiguration { - return c.config.Apis -} - -func (c *FileConfig) GetFeatures() []string { - return c.config.Gateway.Features -} diff --git a/app/config/types.go b/app/config/types.go deleted file mode 100644 index 10db873..0000000 --- a/app/config/types.go +++ /dev/null @@ -1,57 +0,0 @@ -package config - -type Configuration struct { - Gateway GatewayConfiguration `yaml:"gateway"` - Apis []ApiConfiguration `yaml:"apis"` - Routes []RouteConfiguration `yaml:"routes"` - Proxies []ProxyConfiguration `yaml:"proxies"` -} - -type GatewayConfiguration struct { - Host string `yaml:"host"` - Port int `yaml:"port"` - Features []string `yaml:"features"` -} - -type RouteConfiguration struct { - ID string `yaml:"id"` - Path string `yaml:"path"` - StripPrefix struct { - Enabled bool `yaml:"enabled"` - Prefix string `yaml:"prefix"` - } `yaml:"strip_prefix"` - Security SecurityConfiguration `yaml:"security"` -} - -type SecurityConfiguration struct { - Auth AuthConfiguration `yaml:"auth"` - WAF WAFConfiguration `yaml:"waf"` -} - -type WAFConfiguration struct { - Enabled bool `yaml:"enabled"` - Methods []string `yaml:"methods"` -} - -type AuthConfiguration struct { - Enabled bool `yaml:"enabled"` - Type string `yaml:"type"` - Header string `yaml:"header"` - Path AuthPathConfiguration `yaml:"path"` -} - -type AuthPathConfiguration struct { - Include []string `yaml:"include"` - Exclude []string `yaml:"exclude"` -} - -type ProxyConfiguration struct { - ID string `yaml:"id"` - Host string `yaml:"host"` - Target string `yaml:"target"` -} - -type ApiConfiguration struct { - ID string `yaml:"id"` - Target string `yaml:"target"` -} diff --git a/app/database/conn.go b/app/database/conn.go new file mode 100644 index 0000000..d458100 --- /dev/null +++ b/app/database/conn.go @@ -0,0 +1,39 @@ +package database + +import ( + "fmt" + + "git.secnex.io/secnex/api-gateway/config" + "git.secnex.io/secnex/masterlog" + "gorm.io/driver/postgres" + "gorm.io/gorm" + "gorm.io/gorm/logger" +) + +var Connection *gorm.DB + +func Connect(config config.Database) error { + masterlog.Info("Connecting to database", map[string]interface{}{ + "host": config.Host, + "port": config.Port, + "user": config.User, + "database": config.Database, + "sslmode": config.SSLMode, + }) + dsn := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=%s", config.Host, config.Port, config.User, config.Password, config.Database, config.SSLMode) + db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{ + Logger: logger.Default.LogMode(logger.Silent), + }) + if err != nil { + return err + } + masterlog.Info("Connected to database", map[string]interface{}{ + "host": config.Host, + "port": config.Port, + "user": config.User, + "database": config.Database, + "sslmode": config.SSLMode, + }) + Connection = db + return nil +} diff --git a/app/go.mod b/app/go.mod index c3c011f..56b2261 100644 --- a/app/go.mod +++ b/app/go.mod @@ -6,4 +6,20 @@ require ( git.secnex.io/secnex/masterlog v0.1.0 github.com/go-chi/chi/v5 v5.2.4 go.yaml.in/yaml/v3 v3.0.4 + gorm.io/driver/postgres v1.6.0 + gorm.io/gorm v1.31.1 +) + +require ( + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect + github.com/jackc/pgx/v5 v5.6.0 // indirect + github.com/jackc/puddle/v2 v2.2.2 // indirect + github.com/jinzhu/inflection v1.0.0 // indirect + github.com/jinzhu/now v1.1.5 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + golang.org/x/crypto v0.31.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/text v0.21.0 // indirect ) diff --git a/app/go.sum b/app/go.sum index 9fa15bf..312cb3a 100644 --- a/app/go.sum +++ b/app/go.sum @@ -1,8 +1,51 @@ git.secnex.io/secnex/masterlog v0.1.0 h1:74j9CATpfeK0lxpWIQC9ag9083akwG8khi5BwLedD8E= git.secnex.io/secnex/masterlog v0.1.0/go.mod h1:OnDlwEzdkKMnqY+G5O9kHdhoJ6fH1llbVdXpgSc5SdM= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4= github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= +github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY= +github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw= +github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= +github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= +github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= +golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= +gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= +gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= +gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= diff --git a/app/handlers/route.go b/app/handlers/route.go deleted file mode 100644 index 5ac8282..0000000 --- a/app/handlers/route.go +++ /dev/null @@ -1 +0,0 @@ -package handlers diff --git a/app/main.go b/app/main.go index e16178c..d17d8da 100644 --- a/app/main.go +++ b/app/main.go @@ -15,15 +15,14 @@ func main() { masterlog.SetPseudonymizer(pseudonymizer) masterlog.AddSensitiveFields("user_id", "email", "ip") - cfg, err := config.NewFileConfig("../gateway.yaml") + cfg, err := config.NewFile("../gateway.yaml") if err != nil { masterlog.Error("Failed to load config", map[string]interface{}{ "error": err, }) os.Exit(1) } - gateway := server.NewGateway(cfg) - routes := server.NewRoutes(cfg.GetRoutes(), cfg.GetApis()) - gateway.SetRoutes(routes) + + gateway := server.NewGateway(cfg.GetConfiguration()) gateway.Start() } diff --git a/app/middlewares/auth.go b/app/middlewares/auth.go deleted file mode 100644 index 9078fd3..0000000 --- a/app/middlewares/auth.go +++ /dev/null @@ -1,61 +0,0 @@ -package middlewares - -import ( - "net/http" - "path" - "strings" - - "git.secnex.io/secnex/api-gateway/config" - "git.secnex.io/secnex/api-gateway/res" - "git.secnex.io/secnex/masterlog" -) - -func authPathMatches(pattern, requestPath string) bool { - if pattern == "*" { - return true - } - if pattern == requestPath { - return true - } - if strings.Contains(pattern, "*") { - matched, _ := path.Match(pattern, requestPath) - return matched - } - return false -} - -func Auth(next http.Handler, authType string, authHeader string, authPath config.AuthPathConfiguration) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - masterlog.Debug("Auth middleware", map[string]interface{}{ - "path": r.URL.Path, - "include": authPath.Include, - "exclude": authPath.Exclude, - }) - if len(authPath.Include) > 0 { - matched := false - for _, include := range authPath.Include { - if authPathMatches(include, r.URL.Path) { - matched = true - break - } - } - if !matched { - next.ServeHTTP(w, r) - return - } - } else { - for _, exclude := range authPath.Exclude { - if authPathMatches(exclude, r.URL.Path) { - next.ServeHTTP(w, r) - return - } - } - } - if r.Header.Get(authHeader) == "" { - res.Unauthorized(w) - return - } - r.Header.Del(authHeader) - next.ServeHTTP(w, r) - }) -} diff --git a/app/middlewares/host.go b/app/middlewares/host.go new file mode 100644 index 0000000..6fa18f0 --- /dev/null +++ b/app/middlewares/host.go @@ -0,0 +1,16 @@ +package middlewares + +import ( + "net/http" + + "git.secnex.io/secnex/masterlog" +) + +func HostMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + masterlog.Info("HostMiddleware", map[string]interface{}{ + "host": r.Host, + }) + next.ServeHTTP(w, r) + }) +} diff --git a/app/middlewares/logger.go b/app/middlewares/logger.go new file mode 100644 index 0000000..fc2467f --- /dev/null +++ b/app/middlewares/logger.go @@ -0,0 +1,27 @@ +package middlewares + +import ( + "net/http" + "time" + + "git.secnex.io/secnex/masterlog" + "github.com/go-chi/chi/v5/middleware" +) + +func LoggerMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) + next.ServeHTTP(ww, r) + + masterlog.Info("HTTP Request", map[string]interface{}{ + "method": r.Method, + "path": r.URL.Path, + "status": ww.Status(), + "duration": time.Since(start).String(), + "host": r.Host, + "ip": r.RemoteAddr, + }) + }) +} diff --git a/app/middlewares/waf.go b/app/middlewares/waf.go deleted file mode 100644 index aea74b0..0000000 --- a/app/middlewares/waf.go +++ /dev/null @@ -1,27 +0,0 @@ -package middlewares - -import ( - "net/http" - "slices" - - "git.secnex.io/secnex/api-gateway/config" - "git.secnex.io/secnex/api-gateway/res" -) - -func WAF(next http.Handler, wafConfig config.WAFConfiguration) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if !wafConfig.Enabled { - next.ServeHTTP(w, r) - return - } - if slices.Contains(wafConfig.Methods, "*") { - next.ServeHTTP(w, r) - return - } - if !slices.Contains(wafConfig.Methods, r.Method) { - res.Forbidden(w) - return - } - next.ServeHTTP(w, r) - }) -} diff --git a/app/server/api.go b/app/server/api.go new file mode 100644 index 0000000..5367b68 --- /dev/null +++ b/app/server/api.go @@ -0,0 +1,46 @@ +package server + +import ( + "net/http" + + "git.secnex.io/secnex/api-gateway/config" +) + +type Apis map[string]*Api + +type Api struct { + ID string + Host *Host + Target *Target + domain string +} + +func NewApis(cfg *config.Configuration, hosts Hosts, targets Targets) Apis { + apis := make(Apis) + for _, api := range cfg.Apis { + apis[api.ID] = NewApi(&api, hosts[api.Host], targets[api.Target]) + } + return apis +} + +func NewApi(api *config.Api, host *Host, target *Target) *Api { + return &Api{ + ID: api.ID, + Host: host, + Target: target, + domain: host.Domain, + } +} + +func (as Apis) GetApi(domain string) *Api { + for _, api := range as { + if api.domain == domain { + return api + } + } + return nil +} + +func (a *Api) ServeHTTP(w http.ResponseWriter, r *http.Request) { + a.Target.proxy.ServeHTTP(w, r) +} diff --git a/app/server/gateway.go b/app/server/gateway.go index d78e76a..3b9e69e 100644 --- a/app/server/gateway.go +++ b/app/server/gateway.go @@ -5,6 +5,7 @@ import ( "net/http" "git.secnex.io/secnex/api-gateway/config" + "git.secnex.io/secnex/api-gateway/middlewares" "git.secnex.io/secnex/masterlog" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" @@ -12,22 +13,39 @@ import ( type Gateway struct { router *chi.Mux - config config.Config + config config.Configuration + apis Apis routes *Routes - proxy *Proxy } -func NewGateway(config config.Config) *Gateway { +func (g *Gateway) configureProxies() { + for _, api := range g.apis { + masterlog.Info("Configuring proxy", map[string]interface{}{ + "id": api.ID, + "host": api.Host.Domain, + "target": api.Target.URL.String(), + }) + originalDirector := api.Target.proxy.Director + api.Target.proxy.Director = func(r *http.Request) { + originalDirector(r) + r.Host = api.Host.Domain + } + } +} + +func NewGateway(cfg *config.Configuration) *Gateway { r := chi.NewRouter() - for _, feature := range config.GetFeatures() { + for _, feature := range cfg.Gateway.Features { switch feature { case "request_id": r.Use(middleware.RequestID) case "real_ip": r.Use(middleware.RealIP) case "logger": - r.Use(middleware.Logger) + r.Use(middlewares.LoggerMiddleware) + case "host": + r.Use(middlewares.HostMiddleware) } } @@ -38,30 +56,22 @@ func NewGateway(config config.Config) *Gateway { }) }) - return &Gateway{config: config, router: r, routes: nil, proxy: nil} -} + hosts := NewHosts(cfg) + targets := NewTargets(cfg) + apis := NewApis(cfg, hosts, targets) + routes := NewRoutes(cfg, apis) -func (g *Gateway) SetRoutes(routes *Routes) { - g.routes = routes -} - -func (g *Gateway) SetProxy(proxy *Proxy) { - g.proxy = proxy + return &Gateway{config: *cfg, router: r, apis: apis, routes: routes} } func (g *Gateway) Start() { masterlog.Info("Starting gateway", map[string]interface{}{ - "host": g.config.GetGatewayConfiguration().Host, - "port": g.config.GetGatewayConfiguration().Port, + "host": g.config.Gateway.Host, + "port": g.config.Gateway.Port, }) - for path, handler := range g.routes.handlers { - masterlog.Info("Registering route", map[string]interface{}{ - "path": path, - }) - g.router.Handle(path, handler) - } - gatewayConfig := g.config.GetGatewayConfiguration() + g.configureProxies() + g.routes.Register(g.router) - http.ListenAndServe(fmt.Sprintf("%s:%d", gatewayConfig.Host, gatewayConfig.Port), g.router) + http.ListenAndServe(fmt.Sprintf("%s:%d", g.config.Gateway.Host, g.config.Gateway.Port), g.router) } diff --git a/app/server/host.go b/app/server/host.go new file mode 100644 index 0000000..3a0028a --- /dev/null +++ b/app/server/host.go @@ -0,0 +1,41 @@ +package server + +import ( + "net/http/httputil" + "net/url" + + "git.secnex.io/secnex/api-gateway/config" +) + +type Hosts map[string]*Host + +type Host struct { + ID string + Name string + Domain string + proxy *httputil.ReverseProxy +} + +func NewHosts(cfg *config.Configuration) Hosts { + hosts := make(Hosts) + for _, host := range cfg.Hosts { + hosts[host.ID] = NewHost(&host) + } + return hosts +} + +func NewHost(host *config.Host) *Host { + return &Host{ + ID: host.ID, + Name: host.Name, + Domain: host.Domain, + proxy: httputil.NewSingleHostReverseProxy(&url.URL{ + Scheme: "https", + Host: host.Domain, + }), + } +} + +func (hs Hosts) GetHost(domain string) *Host { + return hs[domain] +} diff --git a/app/server/proxy.go b/app/server/proxy.go deleted file mode 100644 index b9ceaf2..0000000 --- a/app/server/proxy.go +++ /dev/null @@ -1,42 +0,0 @@ -package server - -import ( - "log" - "net/http" - "net/http/httputil" - "net/url" - - "git.secnex.io/secnex/api-gateway/config" -) - -type Proxy struct { - proxies []config.ProxyConfiguration - handlers map[string]http.Handler -} - -func NewProxy(proxies []config.ProxyConfiguration) *Proxy { - handlers := make(map[string]http.Handler) - for _, proxy := range proxies { - backend, err := url.Parse(proxy.Target) - if err != nil { - log.Fatalf("Failed to parse proxy target: %v", err) - } - p := httputil.NewSingleHostReverseProxy(backend) - originalDirector := p.Director - p.Director = func(r *http.Request) { - originalDirector(r) - r.Host = backend.Host - } - handlers[proxy.Host] = p - } - return &Proxy{proxies: proxies, handlers: handlers} -} - -func (p *Proxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { - handler, ok := p.handlers[r.Host] - if !ok { - http.NotFound(w, r) - return - } - handler.ServeHTTP(w, r) -} diff --git a/app/server/routes.go b/app/server/routes.go index eee3ca4..d402ee6 100644 --- a/app/server/routes.go +++ b/app/server/routes.go @@ -1,85 +1,81 @@ package server import ( - "log" "net/http" - "net/http/httputil" - "net/url" + "strings" "git.secnex.io/secnex/api-gateway/config" - "git.secnex.io/secnex/api-gateway/middlewares" "git.secnex.io/secnex/masterlog" + "github.com/go-chi/chi/v5" ) type Routes struct { - routes []config.RouteConfiguration - handlers map[string]http.Handler + routes []Route } -func NewRoutes(routes []config.RouteConfiguration, apis []config.ApiConfiguration) *Routes { - handlers := createHandlers(routes, apis) - return &Routes{routes: routes, handlers: handlers} +type Route struct { + ID string + Path string + StripPrefix config.StripPrefix + Api *Api } -func findApi(apis []config.ApiConfiguration, id string) *config.ApiConfiguration { - for _, api := range apis { - if api.ID == id { - return &api - } - } - return nil -} - -func createHandlers(routes []config.RouteConfiguration, apis []config.ApiConfiguration) map[string]http.Handler { - handlers := make(map[string]http.Handler) - for _, route := range routes { - masterlog.Debug("Creating handler for route", map[string]interface{}{ - "path": route.Path, - "id": route.ID, +func NewRoutes(cfg *config.Configuration, apis Apis) *Routes { + routes := make([]Route, 0) + for _, route := range cfg.Routes { + routes = append(routes, Route{ + ID: route.ID, + Path: route.Path, + StripPrefix: route.StripPrefix, + Api: apis[route.Api], }) - api := findApi(apis, route.ID) - if api == nil { - log.Fatalf("API not found: %s", route.ID) - continue - } - backendUrl, err := url.Parse( - api.Target, - ) - if err != nil { - log.Fatalf("Failed to parse backend URL: %v", err) - } - proxy := httputil.NewSingleHostReverseProxy(backendUrl) - handlers[route.Path] = proxy - if route.StripPrefix.Enabled { - masterlog.Debug("Stripping prefix", map[string]interface{}{ - "id": route.ID, - "path": route.Path, - "prefix": route.StripPrefix.Prefix, - }) - handlers[route.Path] = http.StripPrefix(route.StripPrefix.Prefix, handlers[route.Path]) - } - if route.Security.WAF.Enabled { - masterlog.Debug("Applying WAF middleware", map[string]interface{}{ - "id": route.ID, - "path": route.Path, - "methods": route.Security.WAF.Methods, - }) - handlers[route.Path] = middlewares.WAF(handlers[route.Path], route.Security.WAF) - } - if route.Security.Auth.Enabled { - masterlog.Debug("Applying auth middleware", map[string]interface{}{ - "id": route.ID, - "path": route.Path, - "type": route.Security.Auth.Type, - "header": route.Security.Auth.Header, - }) - handlers[route.Path] = middlewares.Auth( - handlers[route.Path], - route.Security.Auth.Type, - route.Security.Auth.Header, - route.Security.Auth.Path, - ) + } + return &Routes{routes: routes} +} + +func (rs *Routes) Register(r *chi.Mux) { + for _, route := range rs.routes { + masterlog.Info("Registering route", map[string]interface{}{ + "id": route.ID, + "path": route.Path, + "api": route.Api.ID, + }) + + handler := route.createHandler() + r.Handle(route.Path, handler) + } +} + +func (r *Route) createHandler() http.Handler { + handler := http.Handler(r.Api) + + if r.StripPrefix.Enabled { + handler = newStripPrefixMiddleware(r.StripPrefix.Prefix, handler) + } + + return handler +} + +type stripPrefixMiddleware struct { + prefix string + handler http.Handler +} + +func newStripPrefixMiddleware(prefix string, handler http.Handler) http.Handler { + return &stripPrefixMiddleware{ + prefix: prefix, + handler: handler, + } +} + +func (m *stripPrefixMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // Remove prefix from path + if strings.HasPrefix(r.URL.Path, m.prefix) { + r.URL.Path = strings.TrimPrefix(r.URL.Path, m.prefix) + // Ensure path starts with / + if !strings.HasPrefix(r.URL.Path, "/") { + r.URL.Path = "/" + r.URL.Path } } - return handlers + m.handler.ServeHTTP(w, r) } diff --git a/app/server/target.go b/app/server/target.go new file mode 100644 index 0000000..d95f52b --- /dev/null +++ b/app/server/target.go @@ -0,0 +1,43 @@ +package server + +import ( + "net/http/httputil" + "net/url" + + "git.secnex.io/secnex/api-gateway/config" + "git.secnex.io/secnex/masterlog" +) + +type Targets map[string]*Target + +type Target struct { + ID string + Name string + URL *url.URL + proxy *httputil.ReverseProxy +} + +func NewTargets(cfg *config.Configuration) Targets { + targets := make(Targets) + for _, target := range cfg.Targets { + targets[target.ID] = NewTarget(&target) + } + return targets +} + +func NewTarget(target *config.Target) *Target { + url, err := url.Parse(target.URL) + if err != nil { + masterlog.Error("Failed to parse target URL", map[string]interface{}{ + "error": err, + "target": target.URL, + }) + return nil + } + return &Target{ + ID: target.ID, + Name: target.Name, + URL: url, + proxy: httputil.NewSingleHostReverseProxy(url), + } +} diff --git a/app/utils/env.go b/app/utils/env.go new file mode 100644 index 0000000..db38d9a --- /dev/null +++ b/app/utils/env.go @@ -0,0 +1,35 @@ +package utils + +import ( + "os" + "strconv" + "strings" +) + +func GetEnv(key string, defaultValue string) string { + value := os.Getenv(key) + if value == "" { + return defaultValue + } + return value +} + +func GetEnvInt(key string, defaultValue int) int { + value := os.Getenv(key) + if value == "" { + return defaultValue + } + intValue, err := strconv.Atoi(value) + if err != nil { + return defaultValue + } + return intValue +} + +func GetEnvBool(key string, defaultValue bool) bool { + value := strings.ToLower(os.Getenv(key)) + if value == "" { + return defaultValue + } + return value == "true" || value == "1" || value == "yes" || value == "y" || value == "on" || value == "enabled" +}