From 5494abded4bae7efe7cc5854f290bca19093a41c Mon Sep 17 00:00:00 2001 From: William Petit Date: Wed, 26 Jun 2024 16:22:30 +0200 Subject: [PATCH] feat: passthrough proxies --- README.md | 13 +++- doc/README.md | 3 +- doc/fr/general-architecture.md | 29 +------ doc/fr/terminology.md | 29 +++++++ ...uml => deployment_single_node_fr.plantuml} | 0 ...t_fr.png => deployment_single_node_fr.png} | Bin go.mod | 2 +- go.sum | 2 + internal/admin/proxy_route.go | 2 +- internal/command/admin/layer/query.go | 17 ++++- internal/command/admin/proxy/create.go | 2 +- internal/command/admin/proxy/flag/flag.go | 9 +-- internal/command/admin/proxy/query.go | 13 ++++ internal/command/admin/proxy/update.go | 2 +- internal/proxy/director/context.go | 13 ---- internal/proxy/director/director.go | 71 +++++++++--------- internal/proxy/server.go | 14 +++- 17 files changed, 128 insertions(+), 93 deletions(-) create mode 100644 doc/fr/terminology.md rename doc/resources/{deployment_fr.plantuml => deployment_single_node_fr.plantuml} (100%) rename doc/resources/{deployment_fr.png => deployment_single_node_fr.png} (100%) diff --git a/README.md b/README.md index 0f3354e..80efcba 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,16 @@ # Bouncer -Serveur mandataire inverse (_"reverse proxy"_) filtrant avec gestion de files d'attente dynamiques. +Serveur mandataire inverse (_"reverse proxy"_) avec fonctionnalités avancées pilotable par API REST. + +## Fonctionnalités + +- Authentification unique basée sur entêtes HTTP ("Trusted headers SSO") avec: + - Fournisseur d'identité OpenID Connect ; + - Basic Auth ; + - Origine réseau ; +- Gestion de files d'attente dynamiques pour maîtriser la charge sur les services protégés ; +- Réécriture dynamique des attributs (notamment entêtes HTTP) des requêtes/réponses via un DSL. ## Documentation @@ -12,4 +21,4 @@ Serveur mandataire inverse (_"reverse proxy"_) filtrant avec gestion de files d' ## Licence -AGPL-3.0 \ No newline at end of file +AGPL-3.0 diff --git a/doc/README.md b/doc/README.md index 5f6e39d..df35eca 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1,7 +1,8 @@ # Documentation -- [(FR) - Premiers pas](./fr/getting-started.md) - [(FR) - Architecture générale](./fr/general-architecture.md) +- [(FR) - Terminologie](./fr/terminology.md) +- [(FR) - Premiers pas](./fr/getting-started.md) ## Exemples diff --git a/doc/fr/general-architecture.md b/doc/fr/general-architecture.md index b490d45..9dfe8b6 100644 --- a/doc/fr/general-architecture.md +++ b/doc/fr/general-architecture.md @@ -2,31 +2,6 @@ ## Modèles de déploiement -### Déploiement mono-noeud +### Mode mono-noeud -![](../resources/deployment_fr.png) - -## Terminologie - -Voici une liste des termes utilisés dans le lexique Bouncer. - -### Proxy - -Un "proxy" est une entité logique définissant le relation suivante: - -- Un ou plusieurs patrons de filtrage sous la forme d'un patron d'URL avec le caractère `*` comme caractère générique. Ceux ci identifient le ou les domaines/chemins associés à l'entité; -- Une URL cible qui servira de base pour la réécriture des requêtes. - -Un "proxy" peut avoir zéro ou plusieurs "layers" associés. - -Un "proxy" peut être activé ou désactivé. - -Un "proxy" a un poids qui définit son niveau de priorité dans la pile de traitement (plus son poids est élevé plus il est prioritaire). - -### Layer - -Un "layer" (calque) est une entité logique définissant un traitement à appliquer aux requêtes et/ou aux réponses transitant par un proxy. - -Un "layer" peut être activé ou désactivé. - -Un "layer" a un poids qui définit son niveau de priorité dans la pile de traitement (plus son poids est élevé plus il est prioritaire). +![](../resources/deployment_single_node_fr.png) diff --git a/doc/fr/terminology.md b/doc/fr/terminology.md new file mode 100644 index 0000000..d70d80d --- /dev/null +++ b/doc/fr/terminology.md @@ -0,0 +1,29 @@ +# Terminologie + +Voici une liste des termes utilisés dans le lexique Bouncer. + +## Proxy + +Un proxy est une entité logique définie par les propriétés suivantes: + +- Il possède **un ou plusieurs filtres d'origine** sous la forme de motifs d'URL avec le caractère `*` comme joker. Ces filtres identifient le ou les URLs associées au proxy. +- Il peut avoir **zéro ou une URL cible**, qui servira de base pour la réécriture des requêtes. Si l'URL est absente, on parle alors de "passthrough" (voir note). +- Il peut avoir **zéro ou plusieurs "layers" associés**. +- Il peut être **activé ou désactivé**. +- Il a **un poids qui définit son niveau de priorité** dans la pile de traitement (plus son poids est élevé plus il est prioritaire). + +Pour résumer un proxy répond à la question "_Quelle URL orienter vers quel serveur cible ?_". + +> **Passthrough** +> +> Un proxy "passthrough" est un proxy n'ayant pas d'URL cible (champ vide). Dans ce cas si les motifs d'URLs correspondent à l'URL de la requête Bouncer appliquera les layers associés puis passera la main aux proxies suivants. + +## Layer + +Un layer est une entité logique définie par les propriétés suivantes: + +- Il a **un type auquel est associé un schéma d'options** permettant de configurer son comportement. +- Il peut être **activé ou désactivé**. +- Il a **un poids qui définit son niveau de priorité** dans la pile de traitement (plus son poids est élevé plus il est prioritaire). + +Pour résumer un layer répond à la question "_Quel traitement appliquer à la requête et/ou réponse ?_". diff --git a/doc/resources/deployment_fr.plantuml b/doc/resources/deployment_single_node_fr.plantuml similarity index 100% rename from doc/resources/deployment_fr.plantuml rename to doc/resources/deployment_single_node_fr.plantuml diff --git a/doc/resources/deployment_fr.png b/doc/resources/deployment_single_node_fr.png similarity index 100% rename from doc/resources/deployment_fr.png rename to doc/resources/deployment_single_node_fr.png diff --git a/go.mod b/go.mod index 4f31540..bb07870 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.21 toolchain go1.22.0 require ( - forge.cadoles.com/Cadoles/go-proxy v0.0.0-20230701194111-c6b3d482cca6 + forge.cadoles.com/Cadoles/go-proxy v0.0.0-20240626132607-e1db6466a926 github.com/Masterminds/sprig/v3 v3.2.3 github.com/bsm/redislock v0.9.4 github.com/btcsuite/btcd/btcutil v1.1.3 diff --git a/go.sum b/go.sum index 7417ad1..33a3a64 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ cloud.google.com/go/longrunning v0.5.1 h1:Fr7TXftcqTudoyRJa113hyaqlGdiBQkp0Gq7tE cloud.google.com/go/longrunning v0.5.1/go.mod h1:spvimkwdz6SPWKEt/XBij79E9fiTkHSQl/fRUUQJYJc= forge.cadoles.com/Cadoles/go-proxy v0.0.0-20230701194111-c6b3d482cca6 h1:FTk0ZoaV5N8Tkps5Da5RrDMZZXSHZIuD67Hy1Y4fsos= forge.cadoles.com/Cadoles/go-proxy v0.0.0-20230701194111-c6b3d482cca6/go.mod h1:o8ZK5v/3J1dRmklFVn1l6WHAyQ3LgegyHjRIT8KLAFw= +forge.cadoles.com/Cadoles/go-proxy v0.0.0-20240626132607-e1db6466a926 h1:gSTTuW2lqH66cGVrhplrVrqos62BY1/GxR3KYh2TElk= +forge.cadoles.com/Cadoles/go-proxy v0.0.0-20240626132607-e1db6466a926/go.mod h1:o8ZK5v/3J1dRmklFVn1l6WHAyQ3LgegyHjRIT8KLAFw= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= diff --git a/internal/admin/proxy_route.go b/internal/admin/proxy_route.go index 47a6daf..7f4c573 100644 --- a/internal/admin/proxy_route.go +++ b/internal/admin/proxy_route.go @@ -121,7 +121,7 @@ func (s *Server) deleteProxy(w http.ResponseWriter, r *http.Request) { type CreateProxyRequest struct { Name string `json:"name" validate:"required"` - To string `json:"to" validate:"required"` + To string `json:"to"` From []string `json:"from" validate:"required"` } diff --git a/internal/command/admin/layer/query.go b/internal/command/admin/layer/query.go index a886297..daefcac 100644 --- a/internal/command/admin/layer/query.go +++ b/internal/command/admin/layer/query.go @@ -2,6 +2,7 @@ package layer import ( "os" + "slices" "forge.cadoles.com/cadoles/bouncer/internal/client" "forge.cadoles.com/cadoles/bouncer/internal/command/admin/apierr" @@ -52,14 +53,16 @@ func QueryCommand() *cli.Command { client := client.New(baseFlags.ServerURL, client.WithToken(token)) - proxies, err := client.QueryLayer(ctx.Context, proxyName, options...) + layers, err := client.QueryLayer(ctx.Context, proxyName, options...) if err != nil { return errors.WithStack(apierr.Wrap(err)) } + slices.SortFunc(layers, sortLayerssByWeight) + hints := layerHeaderHints(baseFlags.OutputMode) - if err := format.Write(baseFlags.Format, os.Stdout, hints, clientFlag.AsAnySlice(proxies)...); err != nil { + if err := format.Write(baseFlags.Format, os.Stdout, hints, clientFlag.AsAnySlice(layers)...); err != nil { return errors.WithStack(err) } @@ -67,3 +70,13 @@ func QueryCommand() *cli.Command { }, } } + +func sortLayerssByWeight(a *store.LayerHeader, b *store.LayerHeader) int { + if a.Weight < b.Weight { + return 1 + } + if a.Weight > b.Weight { + return -1 + } + return 0 +} diff --git a/internal/command/admin/proxy/create.go b/internal/command/admin/proxy/create.go index 700510e..2f02288 100644 --- a/internal/command/admin/proxy/create.go +++ b/internal/command/admin/proxy/create.go @@ -19,7 +19,7 @@ func CreateCommand() *cli.Command { Name: "create", Usage: "Create proxy", Flags: proxyFlag.WithProxyFlags( - flag.ProxyTo(true), + flag.ProxyTo(), flag.ProxyFrom(), ), Action: func(ctx *cli.Context) error { diff --git a/internal/command/admin/proxy/flag/flag.go b/internal/command/admin/proxy/flag/flag.go index ccbf5a8..db09578 100644 --- a/internal/command/admin/proxy/flag/flag.go +++ b/internal/command/admin/proxy/flag/flag.go @@ -30,12 +30,11 @@ func ProxyName() cli.Flag { const KeyProxyTo = "proxy-to" -func ProxyTo(required bool) cli.Flag { +func ProxyTo() cli.Flag { return &cli.StringFlag{ - Name: KeyProxyTo, - Usage: "Set `PROXY_TO` as proxy's destination url", - Value: "", - Required: required, + Name: KeyProxyTo, + Usage: "Set `PROXY_TO` as proxy's destination url", + Value: "", } } diff --git a/internal/command/admin/proxy/query.go b/internal/command/admin/proxy/query.go index 4da5845..4c9c583 100644 --- a/internal/command/admin/proxy/query.go +++ b/internal/command/admin/proxy/query.go @@ -2,6 +2,7 @@ package proxy import ( "os" + "slices" "forge.cadoles.com/cadoles/bouncer/internal/client" "forge.cadoles.com/cadoles/bouncer/internal/command/admin/apierr" @@ -51,6 +52,8 @@ func QueryCommand() *cli.Command { return errors.WithStack(apierr.Wrap(err)) } + slices.SortFunc(proxies, sortProxiesByWeight) + hints := proxyHeaderHints(baseFlags.OutputMode) if err := format.Write(baseFlags.Format, os.Stdout, hints, clientFlag.AsAnySlice(proxies)...); err != nil { @@ -61,3 +64,13 @@ func QueryCommand() *cli.Command { }, } } + +func sortProxiesByWeight(a *store.ProxyHeader, b *store.ProxyHeader) int { + if a.Weight < b.Weight { + return 1 + } + if a.Weight > b.Weight { + return -1 + } + return 0 +} diff --git a/internal/command/admin/proxy/update.go b/internal/command/admin/proxy/update.go index e0778b1..2367975 100644 --- a/internal/command/admin/proxy/update.go +++ b/internal/command/admin/proxy/update.go @@ -19,7 +19,7 @@ func UpdateCommand() *cli.Command { Name: "update", Usage: "Update proxy", Flags: proxyFlag.WithProxyFlags( - flag.ProxyTo(false), + flag.ProxyTo(), flag.ProxyFrom(), flag.ProxyEnabled(), flag.ProxyWeight(), diff --git a/internal/proxy/director/context.go b/internal/proxy/director/context.go index 3dc1868..9b97c3f 100644 --- a/internal/proxy/director/context.go +++ b/internal/proxy/director/context.go @@ -34,19 +34,6 @@ func OriginalURL(ctx context.Context) (*url.URL, error) { return url, nil } -func withProxy(ctx context.Context, proxy *store.Proxy) context.Context { - return context.WithValue(ctx, contextKeyProxy, proxy) -} - -func ctxProxy(ctx context.Context) (*store.Proxy, error) { - proxy, err := ctxValue[*store.Proxy](ctx, contextKeyProxy) - if err != nil { - return nil, errors.WithStack(err) - } - - return proxy, nil -} - func withLayers(ctx context.Context, layers []*store.Layer) context.Context { return context.WithValue(ctx, contextKeyLayers, layers) } diff --git a/internal/proxy/director/director.go b/internal/proxy/director/director.go index 89da473..9cd731d 100644 --- a/internal/proxy/director/director.go +++ b/internal/proxy/director/director.go @@ -36,15 +36,15 @@ func (d *Director) rewriteRequest(r *http.Request) (*http.Request, error) { ctx = withOriginalURL(ctx, url) ctx = logger.With(ctx, logger.F("url", url.String())) - var match *store.Proxy + layers := make([]*store.Layer, 0) -MAIN: for _, p := range proxies { for _, from := range p.From { logger.Debug( ctx, "matching request with proxy's from", logger.F("from", from), ) + if matches := wildcard.Match(url.String(), from); !matches { continue } @@ -54,44 +54,41 @@ MAIN: logger.F("from", from), ) - match = p - break MAIN + ctx = logger.With(ctx, + logger.F("proxy", p.Name), + logger.F("host", r.Host), + logger.F("remoteAddr", r.RemoteAddr), + ) + + metricProxyRequestsTotal.With(prometheus.Labels{metricLabelProxy: string(p.Name)}).Add(1) + + proxyLayers, err := d.getLayers(ctx, p.Name) + if err != nil { + return r, errors.WithStack(err) + } + + layers = append(layers, proxyLayers...) + + if p.To == "" { + continue + } + + toURL, err := url.Parse(p.To) + if err != nil { + return r, errors.WithStack(err) + } + + r.URL.Host = toURL.Host + r.URL.Scheme = toURL.Scheme + r.URL.Path = toURL.JoinPath(r.URL.Path).Path + + ctx = withLayers(ctx, layers) + r = r.WithContext(ctx) + + return r, nil } } - if match == nil { - return r, nil - } - - toURL, err := url.Parse(match.To) - if err != nil { - return r, errors.WithStack(err) - } - - r.URL.Host = toURL.Host - r.URL.Scheme = toURL.Scheme - r.URL.Path = toURL.JoinPath(r.URL.Path).Path - - ctx = logger.With(ctx, - logger.F("proxy", match.Name), - logger.F("host", r.Host), - logger.F("remoteAddr", r.RemoteAddr), - ) - - logger.Debug( - ctx, "rewritten url", - logger.F("rewrittenURL", r.URL.String()), - ) - - metricProxyRequestsTotal.With(prometheus.Labels{metricLabelProxy: string(match.Name)}).Add(1) - - ctx = withProxy(ctx, match) - - layers, err := d.getLayers(ctx, match.Name) - if err != nil { - return r, errors.WithStack(err) - } - ctx = withLayers(ctx, layers) r = r.WithContext(ctx) diff --git a/internal/proxy/server.go b/internal/proxy/server.go index c55daa8..fec04f9 100644 --- a/internal/proxy/server.go +++ b/internal/proxy/server.go @@ -157,6 +157,7 @@ func (s *Server) run(parentCtx context.Context, addrs chan net.Addr, errs chan e director.ResponseTransformer(), ), proxy.WithReverseProxyFactory(s.createReverseProxy), + proxy.WithDefaultHandler(http.HandlerFunc(s.handleDefault)), ) r.Handle("/*", handler) @@ -185,12 +186,21 @@ func (s *Server) createReverseProxy(ctx context.Context, target *url.URL) *httpu httpTransport.DialContext = dialer.DialContext reverseProxy.Transport = httpTransport - reverseProxy.ErrorHandler = s.errorHandler + reverseProxy.ErrorHandler = s.handleError return reverseProxy } -func (s *Server) errorHandler(w http.ResponseWriter, r *http.Request, err error) { +func (s *Server) handleDefault(w http.ResponseWriter, r *http.Request) { + err := errors.Errorf("no proxy target found") + + logger.Error(r.Context(), "proxy error", logger.E(err)) + sentry.CaptureException(err) + + s.renderErrorPage(w, r, err, http.StatusBadGateway, http.StatusText(http.StatusBadGateway)) +} + +func (s *Server) handleError(w http.ResponseWriter, r *http.Request, err error) { err = errors.WithStack(err) logger.Error(r.Context(), "proxy error", logger.E(err))