init: Initial commit

This commit is contained in:
Björn Benouarets
2026-02-05 18:37:35 +01:00
commit 31f5b4081a
25 changed files with 1759 additions and 0 deletions

11
app/config/config.go Normal file
View File

@@ -0,0 +1,11 @@
package config
type Config interface {
GetConfiguration() *Configuration
GetGatewayConfiguration() *GatewayConfiguration
GetFeatures() []string
GetRoutes() []RouteConfiguration
GetApis() []ApiConfiguration
GetHost() string
GetPort() int
}

60
app/config/file.go Normal file
View File

@@ -0,0 +1,60 @@
package config
import (
"os"
"go.yaml.in/yaml/v3"
)
type FileConfig struct {
filePath string
config *Configuration
}
func NewFileConfig(filePath string) (*FileConfig, error) {
c := &FileConfig{filePath: filePath, config: &Configuration{}}
if err := c.loadConfig(); err != nil {
return nil, err
}
return c, nil
}
func (c *FileConfig) loadConfig() error {
data, err := os.ReadFile(c.filePath)
if err != nil {
return err
}
return yaml.Unmarshal(data, c.config)
}
func (c *FileConfig) 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
}

57
app/config/types.go Normal file
View File

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

9
app/go.mod Normal file
View File

@@ -0,0 +1,9 @@
module git.secnex.io/secnex/api-gateway
go 1.25.5
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
)

8
app/go.sum Normal file
View File

@@ -0,0 +1,8 @@
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/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4=
github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
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=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

1
app/handlers/route.go Normal file
View File

@@ -0,0 +1 @@
package handlers

29
app/main.go Normal file
View File

@@ -0,0 +1,29 @@
package main
import (
"os"
"git.secnex.io/secnex/api-gateway/config"
"git.secnex.io/secnex/api-gateway/server"
"git.secnex.io/secnex/masterlog"
)
func main() {
masterlog.SetLevel(masterlog.LevelDebug)
masterlog.AddEncoder(&masterlog.JSONEncoder{})
pseudonymizer := masterlog.NewPseudonymizerFromString("your-secret-key")
masterlog.SetPseudonymizer(pseudonymizer)
masterlog.AddSensitiveFields("user_id", "email", "ip")
cfg, err := config.NewFileConfig("../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.Start()
}

61
app/middlewares/auth.go Normal file
View File

@@ -0,0 +1,61 @@
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)
})
}

27
app/middlewares/waf.go Normal file
View File

@@ -0,0 +1,27 @@
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)
})
}

21
app/res/error.go Normal file
View File

@@ -0,0 +1,21 @@
package res
import (
"encoding/json"
"net/http"
)
type ErrorResponse struct {
Message string `json:"message"`
Code int `json:"code"`
}
func Unauthorized(w http.ResponseWriter) {
w.WriteHeader(http.StatusUnauthorized)
json.NewEncoder(w).Encode(ErrorResponse{Message: "Unauthorized", Code: http.StatusUnauthorized})
}
func Forbidden(w http.ResponseWriter) {
w.WriteHeader(http.StatusForbidden)
json.NewEncoder(w).Encode(ErrorResponse{Message: "Forbidden", Code: http.StatusForbidden})
}

67
app/server/gateway.go Normal file
View File

@@ -0,0 +1,67 @@
package server
import (
"fmt"
"net/http"
"git.secnex.io/secnex/api-gateway/config"
"git.secnex.io/secnex/masterlog"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
)
type Gateway struct {
router *chi.Mux
config config.Config
routes *Routes
proxy *Proxy
}
func NewGateway(config config.Config) *Gateway {
r := chi.NewRouter()
for _, feature := range config.GetFeatures() {
switch feature {
case "request_id":
r.Use(middleware.RequestID)
case "real_ip":
r.Use(middleware.RealIP)
case "logger":
r.Use(middleware.Logger)
}
}
r.Route("/_/health", func(r chi.Router) {
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
})
})
return &Gateway{config: config, router: r, routes: nil, proxy: nil}
}
func (g *Gateway) SetRoutes(routes *Routes) {
g.routes = routes
}
func (g *Gateway) SetProxy(proxy *Proxy) {
g.proxy = proxy
}
func (g *Gateway) Start() {
masterlog.Info("Starting gateway", map[string]interface{}{
"host": g.config.GetGatewayConfiguration().Host,
"port": g.config.GetGatewayConfiguration().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()
http.ListenAndServe(fmt.Sprintf("%s:%d", gatewayConfig.Host, gatewayConfig.Port), g.router)
}

42
app/server/proxy.go Normal file
View File

@@ -0,0 +1,42 @@
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)
}

85
app/server/routes.go Normal file
View File

@@ -0,0 +1,85 @@
package server
import (
"log"
"net/http"
"net/http/httputil"
"net/url"
"git.secnex.io/secnex/api-gateway/config"
"git.secnex.io/secnex/api-gateway/middlewares"
"git.secnex.io/secnex/masterlog"
)
type Routes struct {
routes []config.RouteConfiguration
handlers map[string]http.Handler
}
func NewRoutes(routes []config.RouteConfiguration, apis []config.ApiConfiguration) *Routes {
handlers := createHandlers(routes, apis)
return &Routes{routes: routes, handlers: handlers}
}
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,
})
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 handlers
}