feat(proxy): Add reverse proxy feature
This commit is contained in:
46
app/server/api.go
Normal file
46
app/server/api.go
Normal 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)
|
||||
}
|
||||
@@ -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
41
app/server/host.go
Normal 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]
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
43
app/server/target.go
Normal 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),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user