init: Initial commit
This commit is contained in:
11
app/config/config.go
Normal file
11
app/config/config.go
Normal 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
60
app/config/file.go
Normal 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
57
app/config/types.go
Normal 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
9
app/go.mod
Normal 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
8
app/go.sum
Normal 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
1
app/handlers/route.go
Normal file
@@ -0,0 +1 @@
|
||||
package handlers
|
||||
29
app/main.go
Normal file
29
app/main.go
Normal 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
61
app/middlewares/auth.go
Normal 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
27
app/middlewares/waf.go
Normal 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
21
app/res/error.go
Normal 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
67
app/server/gateway.go
Normal 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
42
app/server/proxy.go
Normal 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
85
app/server/routes.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user