feat(proxy): Add reverse proxy feature

This commit is contained in:
Björn Benouarets
2026-02-05 23:58:47 +01:00
parent 07474afae9
commit 30adf0c701
20 changed files with 514 additions and 323 deletions

46
app/server/api.go Normal file
View File

@@ -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)
}

View File

@@ -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)
}

41
app/server/host.go Normal file
View File

@@ -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]
}

View File

@@ -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)
}

View File

@@ -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)
}

43
app/server/target.go Normal file
View File

@@ -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),
}
}