Compare commits

..

No commits in common. "0b032fccc9a516ef0a52453fdd2aeb61426f6f2d" and "f37425018b5a8ed656be9ebdcfca6df0a2c42c39" have entirely different histories.

23 changed files with 198 additions and 885 deletions

View File

@ -132,7 +132,7 @@ tools/grafterm/bin/grafterm:
GOBIN=$(PWD)/tools/grafterm/bin go install github.com/slok/grafterm/cmd/grafterm@v0.2.0 GOBIN=$(PWD)/tools/grafterm/bin go install github.com/slok/grafterm/cmd/grafterm@v0.2.0
bench: bench:
go test -bench=. -run '^$$' -benchtime=10s ./internal/bench go test -bench=. -run '^$$' ./internal/bench
tools/benchstat/bin/benchstat: tools/benchstat/bin/benchstat:
mkdir -p tools/benchstat/bin mkdir -p tools/benchstat/bin

View File

@ -27,8 +27,8 @@ Bouncer utilise le projet [`expr`](https://expr-lang.org/) comme DSL. En plus de
Le comportement des règles par défaut est le suivant: Le comportement des règles par défaut est le suivant:
1. L'ensemble des entêtes HTTP correspondant au patron `Remote-*` sont supprimés ; 1. L'ensemble des entêtes HTTP correspondant au patron `Remote-*` sont supprimés ;
2. L'identifiant de l'utilisateur identifié (`vars.user.subject`) est exporté sous la forme de l'entête HTTP `Remote-User` ; 2. L'identifiant de l'utilisateur identifié (`user.subject`) est exporté sous la forme de l'entête HTTP `Remote-User` ;
3. L'ensemble des attributs de l'utilisateur identifié (`vars.user.attrs`) sont exportés sous la forme `Remote-User-Attr-<name>``<name>` est le nom de l'attribut en minuscule, avec les `_` transformés en `-`. 3. L'ensemble des attributs de l'utilisateur identifié (`user.attrs`) sont exportés sous la forme `Remote-User-Attr-<name>``<name>` est le nom de l'attribut en minuscule, avec les `_` transformés en `-`.
### Fonctions ### Fonctions
@ -36,25 +36,25 @@ Le comportement des règles par défaut est le suivant:
Interdire l'accès à l'utilisateur. Interdire l'accès à l'utilisateur.
##### `add_header(ctx, name string, value string)` ##### `add_header(name string, value string)`
Ajouter une valeur à un entête HTTP via son nom `name` et sa valeur `value`. Ajouter une valeur à un entête HTTP via son nom `name` et sa valeur `value`.
##### `set_header(ctx, name string, value string)` ##### `set_header(name string, value string)`
Définir la valeur d'un entête HTTP via son nom `name` et sa valeur `value`. La valeur précédente est écrasée. Définir la valeur d'un entête HTTP via son nom `name` et sa valeur `value`. La valeur précédente est écrasée.
##### `del_headers(ctx, pattern string)` ##### `del_headers(pattern string)`
Supprimer un ou plusieurs entêtes HTTP dont le nom correspond au patron `pattern`. Supprimer un ou plusieurs entêtes HTTP dont le nom correspond au patron `pattern`.
Le patron est défini par une chaîne comprenant un ou plusieurs caractères `*`, signifiant un ou plusieurs caractères arbitraires. Le patron est défini par une chaîne comprenant un ou plusieurs caractères `*`, signifiant un ou plusieurs caractères arbitraires.
##### `set_host(ctx, host string)` ##### `set_host(host string)`
Modifier la valeur de l'entête `Host` de la requête. Modifier la valeur de l'entête `Host` de la requête.
##### `set_url(ctx, url string)` ##### `set_url(url string)`
Modifier l'URL du serveur cible. Modifier l'URL du serveur cible.
@ -62,7 +62,7 @@ Modifier l'URL du serveur cible.
Les règles ont accès aux variables suivantes pendant leur exécution. Les règles ont accès aux variables suivantes pendant leur exécution.
#### `vars.user` #### `user`
L'utilisateur identifié par le layer. L'utilisateur identifié par le layer.

View File

@ -14,12 +14,12 @@ Les options disponibles pour le layer sont décrites via un [schéma JSON](https
En plus de ces options spécifiques le layer peut également être configuré via [les options communes aux layers `authn-*`](../../../../../internal/proxy/director/layer/authn/layer-options.json). En plus de ces options spécifiques le layer peut également être configuré via [les options communes aux layers `authn-*`](../../../../../internal/proxy/director/layer/authn/layer-options.json).
## Objet `vars.user` et attributs ## Objet `user` et attributs
L'objet `user` exposé au moteur de règles sera construit de la manière suivante: L'objet `user` exposé au moteur de règles sera construit de la manière suivante:
- `vars.user.subject` sera initialisé avec le nom d'utilisateur identifié ; - `user.subject` sera initialisé avec le nom d'utilisateur identifié ;
- `vars.user.attrs` sera composé des attributs associés à l'utilisation (voir les options). - `user.attrs` sera composé des attributs associés à l'utilisation (voir les options).
## Métriques ## Métriques

View File

@ -14,12 +14,12 @@ Les options disponibles pour le layer sont décrites via un [schéma JSON](https
En plus de ces options spécifiques le layer peut également être configuré via [les options communes aux layers `authn-*`](../../../../../internal/proxy/director/layer/authn/layer-options.json). En plus de ces options spécifiques le layer peut également être configuré via [les options communes aux layers `authn-*`](../../../../../internal/proxy/director/layer/authn/layer-options.json).
## Objet `vars.user` et attributs ## Objet `user` et attributs
L'objet `vars.user` exposé au moteur de règles sera construit de la manière suivante: L'objet `user` exposé au moteur de règles sera construit de la manière suivante:
- `vars.user.subject` sera initialisé avec le couple `<remote_address>:<remote_port>` ; - `user.subject` sera initialisé avec le couple `<remote_address>:<remote_port>` ;
- `vars.user.attrs` sera vide. - `user.attrs` sera vide.
## Métriques ## Métriques

View File

@ -16,18 +16,18 @@ Les options disponibles pour le layer sont décrites via un [schéma JSON](https
En plus de ces options spécifiques le layer peut également être configuré via [les options communes aux layers `authn-*`](../../../../../internal/proxy/director/layer/authn/layer-options.json). En plus de ces options spécifiques le layer peut également être configuré via [les options communes aux layers `authn-*`](../../../../../internal/proxy/director/layer/authn/layer-options.json).
## Objet `vars.user` et attributs ## Objet `user` et attributs
L'objet `vars.user` exposé au moteur de règles sera construit de la manière suivante: L'objet `user` exposé au moteur de règles sera construit de la manière suivante:
- `vars.user.subject` sera initialisé avec la valeur du [claim](https://openid.net/specs/openid-connect-core-1_0.html#Claims) `sub` extrait de l'`idToken` récupéré lors de l'authentification ; - `user.subject` sera initialisé avec la valeur du [claim](https://openid.net/specs/openid-connect-core-1_0.html#Claims) `sub` extrait de l'`idToken` récupéré lors de l'authentification ;
- `vars.user.attrs` comportera les propriétés suivantes: - `user.attrs` comportera les propriétés suivantes:
- L'ensemble des `claims` provenant de l'`idToken` seront transposés en `claim_<name>` (ex: `idToken.iss` sera transposé en `vars.user.attrs.claim_iss`) ; - L'ensemble des `claims` provenant de l'`idToken` seront transposés en `claim_<name>` (ex: `idToken.iss` sera transposé en `user.attrs.claim_iss`) ;
- `vars.user.attrs.access_token`: le jeton d'accès associé à l'authentification ; - `user.attrs.access_token`: le jeton d'accès associé à l'authentification ;
- `vars.user.attrs.refresh_token`: le jeton de rafraîchissement associé à l'authentification (si disponible, en fonction des `scopes` demandés par le client) ; - `user.attrs.refresh_token`: le jeton de rafraîchissement associé à l'authentification (si disponible, en fonction des `scopes` demandés par le client) ;
- `vars.user.attrs.token_expiry`: Horodatage Unix (en secondes) associé à la date d'expiration du jeton d'accès ; - `user.attrs.token_expiry`: Horodatage Unix (en secondes) associé à la date d'expiration du jeton d'accès ;
- `vars.user.attrs.logout_url`: URL de déconnexion pour la suppression de la session Bouncer. - `user.attrs.logout_url`: URL de déconnexion pour la suppression de la session Bouncer.
**Attention** Cette URL ne permet dans la plupart des cas que de supprimer la session côté Bouncer. La suppression de la session côté fournisseur d'identité est conditionné à la présence ou non de l'attribut [`end_session_endpoint`](https://openid.net/specs/openid-connect-session-1_0-17.html#OPMetadata) dans les données du point d'entrée de découverte de service (`.wellknown/openid-configuration`). **Attention** Cette URL ne permet dans la plupart des cas que de supprimer la session côté Bouncer. La suppression de la session côté fournisseur d'identité est conditionné à la présence ou non de l'attribut [`end_session_endpoint`](https://openid.net/specs/openid-connect-session-1_0-17.html#OPMetadata) dans les données du point d'entrée de découverte de service (`.wellknown/openid-configuration`).

View File

@ -24,15 +24,15 @@ Bouncer utilise le projet [`expr`](https://expr-lang.org/) comme DSL. En plus de
#### Communes #### Communes
##### `add_header(ctx, name string, value string)` ##### `add_header(name string, value string)`
Ajouter une valeur à un entête HTTP via son nom `name` et sa valeur `value`. Ajouter une valeur à un entête HTTP via son nom `name` et sa valeur `value`.
##### `set_header(ctx, name string, value string)` ##### `set_header(name string, value string)`
Définir la valeur d'un entête HTTP via son nom `name` et sa valeur `value`. La valeur précédente est écrasée. Définir la valeur d'un entête HTTP via son nom `name` et sa valeur `value`. La valeur précédente est écrasée.
##### `del_headers(ctx, pattern string)` ##### `del_headers(pattern string)`
Supprimer un ou plusieurs entêtes HTTP dont le nom correspond au patron `pattern`. Supprimer un ou plusieurs entêtes HTTP dont le nom correspond au patron `pattern`.
@ -40,11 +40,11 @@ Le patron est défini par une chaîne comprenant un ou plusieurs caractères `*`
#### Requête #### Requête
##### `set_host(ctx, host string)` ##### `set_host(host string)`
Modifier la valeur de l'entête `Host` de la requête. Modifier la valeur de l'entête `Host` de la requête.
##### `set_url(ctx, url string)` ##### `set_url(url string)`
Modifier l'URL du serveur cible. Modifier l'URL du serveur cible.
@ -58,28 +58,7 @@ Les règles ont accès aux variables suivantes pendant leur exécution. **Ces do
#### Requête #### Requête
##### `vars.original_url` ##### `request`
L'URL originale, avant réécriture du `Host` par Bouncer.
```js
{
scheme: "string", // Schéma HTTP de l'URL
opaque: "string", // Données opaque de l'URL
user: { // Identifiants d'URL (Basic Auth)
username: "",
password: ""
},
host: "string", // Nom d'hôte (<domaine>:<port>) de l'URL
path: "string", // Chemin de l'URL (format assaini)
rawPath: "string", // Chemin de l'URL (format brut)
raw_query: "string", // Variables d'URL (format brut)
fragment : "string", // Fragment d'URL (format assaini)
raw_fragment : "string" // Fragment d'URL (format brut)
}
```
##### `vars.request`
La requête en cours de traitement. La requête en cours de traitement.
@ -88,65 +67,61 @@ La requête en cours de traitement.
method: "string", // Méthode HTTP method: "string", // Méthode HTTP
host: "string", // Nom d'hôte (`Host`) associé à la requête host: "string", // Nom d'hôte (`Host`) associé à la requête
url: { // URL associée à la requête sous sa forme structurée url: { // URL associée à la requête sous sa forme structurée
scheme: "string", // Schéma HTTP de l'URL "scheme": "string", // Schéma HTTP de l'URL
opaque: "string", // Données opaque de l'URL "opaque": "string", // Données opaque de l'URL
user: { // Identifiants d'URL (Basic Auth) "user": { // Identifiants d'URL (Basic Auth)
username: "", "username": "",
password: "" "password": ""
}, },
host: "string", // Nom d'hôte (<domaine>:<port>) de l'URL "host": "string", // Nom d'hôte (<domaine>:<port>) de l'URL
path: "string", // Chemin de l'URL (format assaini) "path": "string", // Chemin de l'URL (format assaini)
rawPath: "string", // Chemin de l'URL (format brut) "rawPath": "string", // Chemin de l'URL (format brut)
raw_query: "string", // Variables d'URL (format brut) "rawQuery": "string", // Variables d'URL (format brut)
fragment : "string", // Fragment d'URL (format assaini) "fragment" : "string", // Fragment d'URL (format assaini)
raw_fragment : "string" // Fragment d'URL (format brut) "rawFragment" : "string" // Fragment d'URL (format brut)
}, },
raw_url: "string", // URL associée à la requête (format assaini) rawUrl: "string", // URL associée à la requête (format assaini)
proto: "string", // Numéro de version du protocole utilisé proto: "string", // Numéro de version du protocole utilisé
proto_major: "int", // Numéro de version majeure du protocole utilisé protoMajor: "int", // Numéro de version majeure du protocole utilisé
proto_minor: "int", // Numéro de version mineur du protocole utilisé protoMinor: "int", // Numéro de version mineur du protocole utilisé
header: { // Table associative des entêtes HTTP associés à la requête header: { // Table associative des entêtes HTTP associés à la requête
"string": ["string"] "string": ["string"]
}, },
content_length: "int", // Taille du corps de la requête contentLength: "int", // Taille du corps de la requête
transfer_encoding: ["string"], // MIME-Type(s) d'encodage du corps de la requête transferEncoding: ["string"], // MIME-Type(s) d'encodage du corps de la requête
trailer: { // Table associative des entêtes HTTP associés à la requête, transmises après le corps de la requête trailer: { // Table associative des entêtes HTTP associés à la requête, transmises après le corps de la requête
"string": ["string"] "string": ["string"]
}, },
remote_addr: "string", // Adresse du client HTTP à l'origine de la requête remoteAddr: "string", // Adresse du client HTTP à l'origine de la requête
request_uri: "string" // URL "brute" associée à la requêtes (avant opérations d'assainissement, utiliser "url" plutôt) requestUri: "string" // URL "brute" associée à la requêtes (avant opérations d'assainissement, utiliser "url" plutôt)
} }
``` ```
#### Réponse #### Réponse
##### `vars.response` ##### `response`
La réponse en cours de traitement. La réponse en cours de traitement.
```js ```js
{ {
status_code: "int", // Code de statut de la réponse statusCode: "int", // Code de statut de la réponse
status: "string", // Message associé au code de statut status: "string", // Message associé au code de statut
proto: "string", // Numéro de version du protocole utilisé proto: "string", // Numéro de version du protocole utilisé
proto_major: "int", // Numéro de version majeure du protocole utilisé protoMajor: "int", // Numéro de version majeure du protocole utilisé
proto_minor: "int", // Numéro de version mineur du protocole utilisé protoMinor: "int", // Numéro de version mineur du protocole utilisé
header: { // Table associative des entêtes HTTP associés à la requête header: { // Table associative des entêtes HTTP associés à la requête
"string": ["string"] "string": ["string"]
}, },
content_length: "int", // Taille du corps de la réponse contentLength: "int", // Taille du corps de la réponse
transfer_encoding: ["string"], // MIME-Type(s) d'encodage du corps de la requête transferEncoding: ["string"], // MIME-Type(s) d'encodage du corps de la requête
trailer: { // Table associative des entêtes HTTP associés à la requête, transmises après le corps de la requête trailer: { // Table associative des entêtes HTTP associés à la requête, transmises après le corps de la requête
"string": ["string"] "string": ["string"]
}, },
} }
``` ```
##### `vars.request` ##### `request`
_Voir section précédente._
##### `vars.original_url`
_Voir section précédente._ _Voir section précédente._

View File

@ -3,6 +3,7 @@ package proxy_test
import ( import (
"context" "context"
"io" "io"
"log"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/http/httputil" "net/http/httputil"
@ -23,7 +24,6 @@ import (
redisStore "forge.cadoles.com/cadoles/bouncer/internal/store/redis" redisStore "forge.cadoles.com/cadoles/bouncer/internal/store/redis"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/redis/go-redis/v9" "github.com/redis/go-redis/v9"
"gitlab.com/wpetit/goweb/logger"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
"forge.cadoles.com/cadoles/bouncer/internal/setup" "forge.cadoles.com/cadoles/bouncer/internal/setup"
@ -39,19 +39,6 @@ func BenchmarkProxies(b *testing.B) {
name := strings.TrimSuffix(filepath.Base(f), filepath.Ext(f)) name := strings.TrimSuffix(filepath.Base(f), filepath.Ext(f))
b.Run(name, func(b *testing.B) { b.Run(name, func(b *testing.B) {
heap, err := os.Create(filepath.Join("testdata", "proxies", name+"_heap.prof"))
if err != nil {
b.Fatalf("%+v", errors.Wrapf(err, "could not create heap profile"))
}
defer func() {
defer heap.Close()
if err := pprof.WriteHeapProfile(heap); err != nil {
b.Fatalf("%+v", errors.WithStack(err))
}
}()
conf, err := loadProxyBenchConfig(f) conf, err := loadProxyBenchConfig(f)
if err != nil { if err != nil {
b.Fatalf("%+v", errors.Wrapf(err, "could notre load bench config")) b.Fatalf("%+v", errors.Wrapf(err, "could notre load bench config"))
@ -91,7 +78,7 @@ func BenchmarkProxies(b *testing.B) {
b.Logf("fetching url '%s'", rawProxyURL) b.Logf("fetching url '%s'", rawProxyURL)
profile, err := os.Create(filepath.Join("testdata", "proxies", name+"_cpu.prof")) profile, err := os.Create(filepath.Join("testdata", "proxies", name+".prof"))
if err != nil { if err != nil {
b.Fatalf("%+v", errors.Wrapf(err, "could not create cpu profile")) b.Fatalf("%+v", errors.Wrapf(err, "could not create cpu profile"))
} }
@ -99,7 +86,7 @@ func BenchmarkProxies(b *testing.B) {
defer profile.Close() defer profile.Close()
if err := pprof.StartCPUProfile(profile); err != nil { if err := pprof.StartCPUProfile(profile); err != nil {
b.Fatalf("%+v", errors.WithStack(err)) log.Fatal(err)
} }
defer pprof.StopCPUProfile() defer pprof.StopCPUProfile()
@ -240,12 +227,7 @@ func createProxy(name string, conf *proxyBenchConfig, logf func(format string, a
} }
appConf := config.NewDefault() layers, err := setup.GetLayers(context.Background(), config.NewDefault())
appConf.Logger.Level = config.InterpolatedInt(logger.LevelError)
appConf.Layers.Authn.TemplateDir = "../../layers/authn/templates"
appConf.Layers.Queue.TemplateDir = "../../layers/queue/templates"
layers, err := setup.GetLayers(context.Background(), appConf)
if err != nil { if err != nil {
return nil, nil, errors.WithStack(err) return nil, nil, errors.WithStack(err)
} }

View File

@ -12,7 +12,7 @@ proxy:
attributes: attributes:
email: foo@bar.com email: foo@bar.com
rules: rules:
- set_header(ctx, "Remote-User-Attr-Email", vars.user.attrs.email) - set_header("Remote-User-Attr-Email", user.attrs.email)
fetch: fetch:
url: url:
user: user:

View File

@ -1,10 +0,0 @@
proxy:
from: ["*"]
to: ""
layers:
queue:
type: queue
enabled: true
options:
capacity: 100
keepAlive: 10s

View File

@ -8,5 +8,5 @@ proxy:
options: options:
rules: rules:
request: request:
- set_host(ctx, vars.request.url.host) - set_host(request.url.host)
- set_header(ctx, "X-Proxied-With", "bouncer") - set_header("X-Proxied-With", "bouncer")

View File

@ -4,14 +4,14 @@
<h2>Incoming headers</h2> <h2>Incoming headers</h2>
<table style="width: 100%"> <table style="width: 100%">
<thead> <thead>
<tr style="text-align: left"> <tr>
<th>Key</th> <th>Key</th>
<th>Value</th> <th>Value</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{ range $key, $val := .Request.Header }} {{ range $key, $val := .Request.Header }}
<tr style="text-align: left"> <tr>
<td> <td>
<b>{{ $key }}</b> <b>{{ $key }}</b>
</td> </td>
@ -27,7 +27,7 @@
<h2>Incoming cookies</h2> <h2>Incoming cookies</h2>
<table style="width: 100%"> <table style="width: 100%">
<thead> <thead>
<tr style="text-align: left"> <tr>
<th>Name</th> <th>Name</th>
<th>Domain</th> <th>Domain</th>
<th>Path</th> <th>Path</th>
@ -41,7 +41,7 @@
</thead> </thead>
<tbody> <tbody>
{{ range $cookie := .Request.Cookies }} {{ range $cookie := .Request.Cookies }}
<tr style="text-align: left"> <tr>
<td> <td>
<b>{{ $cookie.Name }}</b> <b>{{ $cookie.Name }}</b>
</td> </td>

View File

@ -74,7 +74,7 @@ func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
return return
} }
if err := l.applyRules(ctx, r, options, user); err != nil { if err := l.applyRules(r, options, user); err != nil {
if errors.Is(err, ErrForbidden) { if errors.Is(err, ErrForbidden) {
l.renderForbiddenPage(w, r, layer, options, user) l.renderForbiddenPage(w, r, layer, options, user)
return return

View File

@ -1,7 +1,6 @@
package authn package authn
import ( import (
"context"
"net/http" "net/http"
"forge.cadoles.com/cadoles/bouncer/internal/rule" "forge.cadoles.com/cadoles/bouncer/internal/rule"
@ -10,32 +9,30 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
type Vars struct { type Env struct {
User *User `expr:"user"` User *User `expr:"user"`
} }
func (l *Layer) applyRules(ctx context.Context, r *http.Request, options *LayerOptions, user *User) error { func (l *Layer) applyRules(r *http.Request, options *LayerOptions, user *User) error {
rules := options.Rules rules := options.Rules
if len(rules) == 0 { if len(rules) == 0 {
return nil return nil
} }
engine, err := rule.NewEngine[*Vars]( engine, err := rule.NewEngine[*Env](
rule.WithRules(options.Rules...), rule.WithRules(options.Rules...),
rule.WithExpr(getAuthnAPI()...), rule.WithExpr(getAuthnAPI()...),
ruleHTTP.WithRequestFuncs(), ruleHTTP.WithRequestFuncs(r),
) )
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
vars := &Vars{ env := &Env{
User: user, User: user,
} }
ctx = ruleHTTP.WithRequest(ctx, r) if _, err := engine.Apply(env); err != nil {
if _, err := engine.Apply(ctx, vars); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }

View File

@ -6,9 +6,6 @@ import (
proxy "forge.cadoles.com/Cadoles/go-proxy" proxy "forge.cadoles.com/Cadoles/go-proxy"
"forge.cadoles.com/Cadoles/go-proxy/wildcard" "forge.cadoles.com/Cadoles/go-proxy/wildcard"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director" "forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/util"
"forge.cadoles.com/cadoles/bouncer/internal/rule"
ruleHTTP "forge.cadoles.com/cadoles/bouncer/internal/rule/http"
"forge.cadoles.com/cadoles/bouncer/internal/store" "forge.cadoles.com/cadoles/bouncer/internal/store"
"github.com/pkg/errors" "github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger" "gitlab.com/wpetit/goweb/logger"
@ -16,10 +13,7 @@ import (
const LayerType store.LayerType = "rewriter" const LayerType store.LayerType = "rewriter"
type Layer struct { type Layer struct{}
requestRuleEngine *util.RevisionedRuleEngine[*RequestVars, *LayerOptions]
responseRuleEngine *util.RevisionedRuleEngine[*ResponseVars, *LayerOptions]
}
func (l *Layer) LayerType() store.LayerType { func (l *Layer) LayerType() store.LayerType {
return LayerType return LayerType
@ -45,7 +39,7 @@ func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
return return
} }
if err := l.applyRequestRules(ctx, r, layer.Revision, options); err != nil { if err := l.applyRequestRules(r, options); err != nil {
logger.Error(ctx, "could not apply request rules", logger.E(errors.WithStack(err))) logger.Error(ctx, "could not apply request rules", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -72,9 +66,7 @@ func (l *Layer) ResponseTransformer(layer *store.Layer) proxy.ResponseTransforme
return nil return nil
} }
ctx := r.Request.Context() if err := l.applyResponseRules(r, options); err != nil {
if err := l.applyResponseRules(ctx, r, layer.Revision, options); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
@ -83,30 +75,7 @@ func (l *Layer) ResponseTransformer(layer *store.Layer) proxy.ResponseTransforme
} }
func New(funcs ...OptionFunc) *Layer { func New(funcs ...OptionFunc) *Layer {
return &Layer{ return &Layer{}
requestRuleEngine: util.NewRevisionedRuleEngine(func(options *LayerOptions) (*rule.Engine[*RequestVars], error) {
engine, err := rule.NewEngine[*RequestVars](
rule.WithRules(options.Rules.Request...),
ruleHTTP.WithRequestFuncs(),
)
if err != nil {
return nil, errors.WithStack(err)
}
return engine, nil
}),
responseRuleEngine: util.NewRevisionedRuleEngine(func(options *LayerOptions) (*rule.Engine[*ResponseVars], error) {
engine, err := rule.NewEngine[*ResponseVars](
rule.WithRules(options.Rules.Response...),
ruleHTTP.WithResponseFuncs(),
)
if err != nil {
return nil, errors.WithStack(err)
}
return engine, nil
}),
}
} }
var ( var (

View File

@ -1,93 +1,68 @@
package rewriter package rewriter
import ( import (
"context"
"net/http" "net/http"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/rule" "forge.cadoles.com/cadoles/bouncer/internal/rule"
ruleHTTP "forge.cadoles.com/cadoles/bouncer/internal/rule/http" ruleHTTP "forge.cadoles.com/cadoles/bouncer/internal/rule/http"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
type RequestVars struct { type RequestEnv struct {
Request RequestVar `expr:"request"` Request RequestInfo `expr:"request"`
OriginalURL URLVar `expr:"original_url"`
} }
type URLVar struct { type URLEnv struct {
Scheme string `expr:"scheme"` Scheme string `expr:"scheme"`
Opaque string `expr:"opaque"` Opaque string `expr:"opaque"`
User UserVar `expr:"user"` User UserInfoEnv `expr:"user"`
Host string `expr:"host"` Host string `expr:"host"`
Path string `expr:"path"` Path string `expr:"path"`
RawPath string `expr:"raw_path"` RawPath string `expr:"rawPath"`
RawQuery string `expr:"raw_query"` RawQuery string `expr:"rawQuery"`
Fragment string `expr:"fragment"` Fragment string `expr:"fragment"`
RawFragment string `expr:"raw_fragment"` RawFragment string `expr:"rawFragment"`
} }
type UserVar struct { type UserInfoEnv struct {
Username string `expr:"username"` Username string `expr:"username"`
Password string `expr:"password"` Password string `expr:"password"`
} }
type RequestVar struct { type RequestInfo struct {
Method string `expr:"method"` Method string `expr:"method"`
URL URLVar `expr:"url"` URL URLEnv `expr:"url"`
RawURL string `expr:"raw_url"` RawURL string `expr:"rawUrl"`
Proto string `expr:"proto"` Proto string `expr:"proto"`
ProtoMajor int `expr:"proto_major"` ProtoMajor int `expr:"protoMajor"`
ProtoMinor int `expr:"proto_minor"` ProtoMinor int `expr:"protoMinor"`
Header map[string][]string `expr:"header"` Header map[string][]string `expr:"header"`
ContentLength int64 `expr:"content_length"` ContentLength int64 `expr:"contentLength"`
TransferEncoding []string `expr:"transfer_encoding"` TransferEncoding []string `expr:"transferEncoding"`
Host string `expr:"host"` Host string `expr:"host"`
Trailer map[string][]string `expr:"trailer"` Trailer map[string][]string `expr:"trailer"`
RemoteAddr string `expr:"remote_addr"` RemoteAddr string `expr:"remoteAddr"`
RequestURI string `expr:"request_uri"` RequestURI string `expr:"requestUri"`
} }
func (l *Layer) applyRequestRules(ctx context.Context, r *http.Request, layerRevision int, options *LayerOptions) error { func (l *Layer) applyRequestRules(r *http.Request, options *LayerOptions) error {
rules := options.Rules.Request rules := options.Rules.Request
if len(rules) == 0 { if len(rules) == 0 {
return nil return nil
} }
engine, err := l.getRequestRuleEngine(ctx, layerRevision, options) engine, err := l.getRequestRuleEngine(r, options)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
originalURL, err := director.OriginalURL(ctx) env := &RequestEnv{
if err != nil { Request: RequestInfo{
return errors.WithStack(err)
}
vars := &RequestVars{
OriginalURL: URLVar{
Scheme: originalURL.Scheme,
Opaque: originalURL.Opaque,
User: UserVar{
Username: originalURL.User.Username(),
Password: func() string {
passwd, _ := originalURL.User.Password()
return passwd
}(),
},
Host: originalURL.Host,
Path: originalURL.Path,
RawPath: originalURL.RawPath,
RawQuery: originalURL.RawQuery,
Fragment: originalURL.Fragment,
RawFragment: originalURL.RawFragment,
},
Request: RequestVar{
Method: r.Method, Method: r.Method,
URL: URLVar{ URL: URLEnv{
Scheme: r.URL.Scheme, Scheme: r.URL.Scheme,
Opaque: r.URL.Opaque, Opaque: r.URL.Opaque,
User: UserVar{ User: UserInfoEnv{
Username: r.URL.User.Username(), Username: r.URL.User.Username(),
Password: func() string { Password: func() string {
passwd, _ := r.URL.User.Password() passwd, _ := r.URL.User.Password()
@ -115,17 +90,18 @@ func (l *Layer) applyRequestRules(ctx context.Context, r *http.Request, layerRev
}, },
} }
ctx = ruleHTTP.WithRequest(ctx, r) if _, err := engine.Apply(env); err != nil {
if _, err := engine.Apply(ctx, vars); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
return nil return nil
} }
func (l *Layer) getRequestRuleEngine(ctx context.Context, layerRevision int, options *LayerOptions) (*rule.Engine[*RequestVars], error) { func (l *Layer) getRequestRuleEngine(r *http.Request, options *LayerOptions) (*rule.Engine[*RequestEnv], error) {
engine, err := l.requestRuleEngine.Get(ctx, layerRevision, options) engine, err := rule.NewEngine[*RequestEnv](
rule.WithRules(options.Rules.Request...),
ruleHTTP.WithRequestFuncs(r),
)
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }
@ -133,65 +109,42 @@ func (l *Layer) getRequestRuleEngine(ctx context.Context, layerRevision int, opt
return engine, nil return engine, nil
} }
type ResponseVars struct { type ResponseEnv struct {
OriginalURL URLVar `expr:"original_url"` Request RequestInfo `expr:"request"`
Request RequestVar `expr:"request"` Response ResponseInfo `expr:"response"`
Response ResponseVar `expr:"response"`
} }
type ResponseVar struct { type ResponseInfo struct {
Status string `expr:"status"` Status string `expr:"status"`
StatusCode int `expr:"status_code"` StatusCode int `expr:"statusCode"`
Proto string `expr:"proto"` Proto string `expr:"proto"`
ProtoMajor int `expr:"proto_major"` ProtoMajor int `expr:"protoMajor"`
ProtoMinor int `expr:"proto_minor"` ProtoMinor int `expr:"protoMinor"`
Header map[string][]string `expr:"header"` Header map[string][]string `expr:"header"`
ContentLength int64 `expr:"content_length"` ContentLength int64 `expr:"contentLength"`
TransferEncoding []string `expr:"transfer_encoding"` TransferEncoding []string `expr:"transferEncoding"`
Uncompressed bool `expr:"uncompressed"` Uncompressed bool `expr:"uncompressed"`
Trailer map[string][]string `expr:"trailer"` Trailer map[string][]string `expr:"trailer"`
} }
func (l *Layer) applyResponseRules(ctx context.Context, r *http.Response, layerRevision int, options *LayerOptions) error { func (l *Layer) applyResponseRules(r *http.Response, options *LayerOptions) error {
rules := options.Rules.Response rules := options.Rules.Response
if len(rules) == 0 { if len(rules) == 0 {
return nil return nil
} }
engine, err := l.getResponseRuleEngine(ctx, layerRevision, options) engine, err := l.getResponseRuleEngine(r, options)
if err != nil { if err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
originalURL, err := director.OriginalURL(ctx) env := &ResponseEnv{
if err != nil { Request: RequestInfo{
return errors.WithStack(err)
}
vars := &ResponseVars{
OriginalURL: URLVar{
Scheme: originalURL.Scheme,
Opaque: originalURL.Opaque,
User: UserVar{
Username: originalURL.User.Username(),
Password: func() string {
passwd, _ := originalURL.User.Password()
return passwd
}(),
},
Host: originalURL.Host,
Path: originalURL.Path,
RawPath: originalURL.RawPath,
RawQuery: originalURL.RawQuery,
Fragment: originalURL.Fragment,
RawFragment: originalURL.RawFragment,
},
Request: RequestVar{
Method: r.Request.Method, Method: r.Request.Method,
URL: URLVar{ URL: URLEnv{
Scheme: r.Request.URL.Scheme, Scheme: r.Request.URL.Scheme,
Opaque: r.Request.URL.Opaque, Opaque: r.Request.URL.Opaque,
User: UserVar{ User: UserInfoEnv{
Username: r.Request.URL.User.Username(), Username: r.Request.URL.User.Username(),
Password: func() string { Password: func() string {
passwd, _ := r.Request.URL.User.Password() passwd, _ := r.Request.URL.User.Password()
@ -217,7 +170,7 @@ func (l *Layer) applyResponseRules(ctx context.Context, r *http.Response, layerR
RemoteAddr: r.Request.RemoteAddr, RemoteAddr: r.Request.RemoteAddr,
RequestURI: r.Request.RequestURI, RequestURI: r.Request.RequestURI,
}, },
Response: ResponseVar{ Response: ResponseInfo{
Proto: r.Proto, Proto: r.Proto,
ProtoMajor: r.ProtoMajor, ProtoMajor: r.ProtoMajor,
ProtoMinor: r.ProtoMinor, ProtoMinor: r.ProtoMinor,
@ -230,17 +183,18 @@ func (l *Layer) applyResponseRules(ctx context.Context, r *http.Response, layerR
}, },
} }
ctx = ruleHTTP.WithResponse(ctx, r) if _, err := engine.Apply(env); err != nil {
if _, err := engine.Apply(ctx, vars); err != nil {
return errors.WithStack(err) return errors.WithStack(err)
} }
return nil return nil
} }
func (l *Layer) getResponseRuleEngine(ctx context.Context, layerRevision int, options *LayerOptions) (*rule.Engine[*ResponseVars], error) { func (l *Layer) getResponseRuleEngine(r *http.Response, options *LayerOptions) (*rule.Engine[*ResponseEnv], error) {
engine, err := l.responseRuleEngine.Get(ctx, layerRevision, options) engine, err := rule.NewEngine[*ResponseEnv](
rule.WithRules(options.Rules.Response...),
ruleHTTP.WithResponseFuncs(r),
)
if err != nil { if err != nil {
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }

View File

@ -1,51 +0,0 @@
package util
import (
"context"
"sync"
"forge.cadoles.com/cadoles/bouncer/internal/rule"
"github.com/pkg/errors"
"gitlab.com/wpetit/goweb/logger"
)
type RuleEngineFactoryFunc[V any, O any] func(ops O) (*rule.Engine[V], error)
type RevisionedRuleEngine[V any, O any] struct {
mutex sync.RWMutex
revision int
engine *rule.Engine[V]
factory RuleEngineFactoryFunc[V, O]
}
func (e *RevisionedRuleEngine[V, O]) Get(ctx context.Context, revision int, opts O) (*rule.Engine[V], error) {
e.mutex.RLock()
if revision == e.revision {
logger.Debug(ctx, "using cached rule engine", logger.F("layerRevision", revision))
defer e.mutex.RUnlock()
return e.engine, nil
}
e.mutex.RUnlock()
e.mutex.Lock()
defer e.mutex.Unlock()
logger.Debug(ctx, "creating rule engine", logger.F("layerRevision", revision))
engine, err := e.factory(opts)
if err != nil {
return nil, errors.WithStack(err)
}
e.engine = engine
e.revision = revision
return engine, nil
}
func NewRevisionedRuleEngine[V any, O any](factory RuleEngineFactoryFunc[V, O]) *RevisionedRuleEngine[V, O] {
return &RevisionedRuleEngine[V, O]{
factory: factory,
}
}

View File

@ -1,28 +1,16 @@
package rule package rule
import ( import (
"context"
"github.com/expr-lang/expr" "github.com/expr-lang/expr"
"github.com/expr-lang/expr/vm" "github.com/expr-lang/expr/vm"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
type Engine[V any] struct { type Engine[E any] struct {
rules []*vm.Program rules []*vm.Program
} }
func (e *Engine[V]) Apply(ctx context.Context, vars V) ([]any, error) { func (e *Engine[E]) Apply(env E) ([]any, error) {
type Env[V any] struct {
Context context.Context `expr:"ctx"`
Vars V `expr:"vars"`
}
env := Env[V]{
Context: ctx,
Vars: vars,
}
results := make([]any, 0, len(e.rules)) results := make([]any, 0, len(e.rules))
for i, r := range e.rules { for i, r := range e.rules {
result, err := expr.Run(r, env) result, err := expr.Run(r, env)
@ -54,26 +42,3 @@ func NewEngine[E any](funcs ...OptionFunc) (*Engine[E], error) {
return engine, nil return engine, nil
} }
func Context[T any](ctx context.Context, key any) (T, bool) {
raw := ctx.Value(key)
if raw == nil {
return *new(T), false
}
value, err := Assert[T](raw)
if err != nil {
return *new(T), false
}
return value, true
}
func Assert[T any](raw any) (T, error) {
value, ok := raw.(T)
if !ok {
return *new(T), errors.Errorf("unexpected value '%T'", value)
}
return value, nil
}

View File

@ -1,31 +0,0 @@
package http
import (
"context"
"net/http"
"forge.cadoles.com/cadoles/bouncer/internal/rule"
)
type contextKey string
const (
contextKeyRequest contextKey = "request"
contextKeyResponse contextKey = "response"
)
func WithRequest(ctx context.Context, r *http.Request) context.Context {
return context.WithValue(ctx, contextKeyRequest, r)
}
func WithResponse(ctx context.Context, r *http.Response) context.Context {
return context.WithValue(ctx, contextKeyResponse, r)
}
func ctxRequest(ctx context.Context) (*http.Request, bool) {
return rule.Context[*http.Request](ctx, contextKeyRequest)
}
func ctxResponse(ctx context.Context) (*http.Response, bool) {
return rule.Context[*http.Response](ctx, contextKeyResponse)
}

View File

@ -1,18 +1,20 @@
package http package http
import ( import (
"net/http"
"forge.cadoles.com/cadoles/bouncer/internal/rule" "forge.cadoles.com/cadoles/bouncer/internal/rule"
"github.com/expr-lang/expr" "github.com/expr-lang/expr"
) )
func WithRequestFuncs() rule.OptionFunc { func WithRequestFuncs(r *http.Request) rule.OptionFunc {
return func(opts *rule.Options) { return func(opts *rule.Options) {
funcs := []expr.Option{ funcs := []expr.Option{
setRequestURLFunc(), setRequestURL(r),
setRequestHeaderFunc(), setRequestHeaderFunc(r),
addRequestHeaderFunc(), addRequestHeaderFunc(r),
delRequestHeadersFunc(), delRequestHeadersFunc(r),
setRequestHostFunc(), setRequestHostFunc(r),
} }
if len(opts.Expr) == 0 { if len(opts.Expr) == 0 {
@ -23,12 +25,12 @@ func WithRequestFuncs() rule.OptionFunc {
} }
} }
func WithResponseFuncs() rule.OptionFunc { func WithResponseFuncs(r *http.Response) rule.OptionFunc {
return func(opts *rule.Options) { return func(opts *rule.Options) {
funcs := []expr.Option{ funcs := []expr.Option{
setResponseHeaderFunc(), setResponseHeaderFunc(r),
addResponseHeaderFunc(), addResponseHeaderFunc(r),
delResponseHeadersFunc(), delResponseHeadersFunc(r),
} }
if len(opts.Expr) == 0 { if len(opts.Expr) == 0 {

View File

@ -1,155 +1,109 @@
package http package http
import ( import (
"context"
"fmt" "fmt"
"net/http"
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"forge.cadoles.com/Cadoles/go-proxy/wildcard" "forge.cadoles.com/Cadoles/go-proxy/wildcard"
"forge.cadoles.com/cadoles/bouncer/internal/rule"
"github.com/expr-lang/expr" "github.com/expr-lang/expr"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
func setRequestHostFunc() expr.Option { func setRequestHostFunc(r *http.Request) expr.Option {
return expr.Function( return expr.Function(
"set_host", "set_host",
func(params ...any) (any, error) { func(params ...any) (any, error) {
ctx, err := rule.Assert[context.Context](params[0]) host := params[0].(string)
if err != nil {
return nil, errors.WithStack(err)
}
host, err := rule.Assert[string](params[1])
if err != nil {
return nil, errors.WithStack(err)
}
r, ok := ctxRequest(ctx)
if !ok {
return nil, errors.New("could not find http request in context")
}
r.Host = host r.Host = host
return true, nil return true, nil
}, },
new(func(context.Context, string) bool), new(func(string) bool),
) )
} }
func setRequestURLFunc() expr.Option { func setRequestURL(r *http.Request) expr.Option {
return expr.Function( return expr.Function(
"set_url", "set_url",
func(params ...any) (any, error) { func(params ...any) (any, error) {
ctx, err := rule.Assert[context.Context](params[0]) rawURL := params[0].(string)
if err != nil {
return nil, errors.WithStack(err)
}
rawURL, err := rule.Assert[string](params[1])
if err != nil {
return nil, errors.WithStack(err)
}
url, err := url.Parse(rawURL) url, err := url.Parse(rawURL)
if err != nil { if err != nil {
return false, errors.WithStack(err) return false, errors.WithStack(err)
} }
r, ok := ctxRequest(ctx)
if !ok {
return nil, errors.New("could not find http request in context")
}
r.URL = url r.URL = url
return true, nil return true, nil
}, },
new(func(context.Context, string) bool), new(func(string) bool),
) )
} }
func addRequestHeaderFunc() expr.Option { func addRequestHeaderFunc(r *http.Request) expr.Option {
return expr.Function( return expr.Function(
"add_header", "add_header",
func(params ...any) (any, error) { func(params ...any) (any, error) {
ctx, err := rule.Assert[context.Context](params[0]) name := params[0].(string)
if err != nil { rawValue := params[1]
return nil, errors.WithStack(err)
}
name, err := rule.Assert[string](params[1]) var value string
if err != nil { switch v := rawValue.(type) {
return nil, errors.WithStack(err) case []string:
} value = strings.Join(v, ",")
case time.Time:
value := formatValue(params[2]) value = strconv.FormatInt(v.UTC().Unix(), 10)
case time.Duration:
r, ok := ctxRequest(ctx) value = strconv.FormatInt(int64(v.Seconds()), 10)
if !ok { default:
return nil, errors.New("could not find http request in context") value = fmt.Sprintf("%v", rawValue)
} }
r.Header.Add(name, value) r.Header.Add(name, value)
return true, nil return true, nil
}, },
new(func(context.Context, string, string) bool), new(func(string, string) bool),
) )
} }
func setRequestHeaderFunc() expr.Option { func setRequestHeaderFunc(r *http.Request) expr.Option {
return expr.Function( return expr.Function(
"set_header", "set_header",
func(params ...any) (any, error) { func(params ...any) (any, error) {
ctx, err := rule.Assert[context.Context](params[0]) name := params[0].(string)
if err != nil { rawValue := params[1]
return nil, errors.WithStack(err)
}
name, err := rule.Assert[string](params[1]) var value string
if err != nil { switch v := rawValue.(type) {
return nil, errors.WithStack(err) case []string:
} value = strings.Join(v, ",")
case time.Time:
value := formatValue(params[2]) value = strconv.FormatInt(v.UTC().Unix(), 10)
case time.Duration:
r, ok := ctxRequest(ctx) value = strconv.FormatInt(int64(v.Seconds()), 10)
if !ok { default:
return nil, errors.New("could not find http request in context") value = fmt.Sprintf("%v", rawValue)
} }
r.Header.Set(name, value) r.Header.Set(name, value)
return true, nil return true, nil
}, },
new(func(context.Context, string, string) bool), new(func(string, string) bool),
) )
} }
func delRequestHeadersFunc() expr.Option { func delRequestHeadersFunc(r *http.Request) expr.Option {
return expr.Function( return expr.Function(
"del_headers", "del_headers",
func(params ...any) (any, error) { func(params ...any) (any, error) {
ctx, err := rule.Assert[context.Context](params[0]) pattern := params[0].(string)
if err != nil {
return nil, errors.WithStack(err)
}
pattern, err := rule.Assert[string](params[1])
if err != nil {
return nil, errors.WithStack(err)
}
r, ok := ctxRequest(ctx)
if !ok {
return nil, errors.New("could not find http request in context")
}
deleted := false deleted := false
for key := range r.Header { for key := range r.Header {
@ -163,21 +117,6 @@ func delRequestHeadersFunc() expr.Option {
return deleted, nil return deleted, nil
}, },
new(func(context.Context, string) bool), new(func(string) bool),
) )
} }
func formatValue(v any) string {
var value string
switch v := v.(type) {
case []string:
value = strings.Join(v, ",")
case time.Time:
value = strconv.FormatInt(v.UTC().Unix(), 10)
case time.Duration:
value = strconv.FormatInt(int64(v.Seconds()), 10)
default:
value = fmt.Sprintf("%v", v)
}
return value
}

View File

@ -1,195 +0,0 @@
package http
import (
"context"
"net/http"
"testing"
"forge.cadoles.com/cadoles/bouncer/internal/rule"
"github.com/pkg/errors"
)
func TestSetRequestHost(t *testing.T) {
type Vars struct {
NewHost string `expr:"newHost"`
}
engine := createRuleEngine[Vars](t,
rule.WithExpr(setRequestHostFunc()),
rule.WithRules(
"set_host(ctx, vars.newHost)",
),
)
req, err := http.NewRequest("GET", "http://example.net", nil)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
ctx := context.Background()
ctx = WithRequest(ctx, req)
vars := Vars{
NewHost: "foobar",
}
if _, err := engine.Apply(ctx, vars); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if e, g := vars.NewHost, req.Host; e != g {
t.Errorf("req.Host: expected '%v', got '%v'", e, g)
}
}
func TestSetRequestURL(t *testing.T) {
type Vars struct {
NewURL string `expr:"newURL"`
}
engine := createRuleEngine[Vars](t,
rule.WithExpr(setRequestURLFunc()),
rule.WithRules(
"set_url(ctx, vars.newURL)",
),
)
req, err := http.NewRequest("GET", "http://example.net", nil)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
ctx := context.Background()
ctx = WithRequest(ctx, req)
vars := Vars{
NewURL: "http://localhost",
}
if _, err := engine.Apply(ctx, vars); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if e, g := vars.NewURL, req.URL.String(); e != g {
t.Errorf("req.URL.String(): expected '%v', got '%v'", e, g)
}
}
func TestAddRequestHeader(t *testing.T) {
type Vars struct {
NewHeaderKey string `expr:"newHeaderKey"`
NewHeaderValue string `expr:"newHeaderValue"`
}
engine := createRuleEngine[Vars](t,
rule.WithExpr(addRequestHeaderFunc()),
rule.WithRules(
"add_header(ctx, vars.newHeaderKey, vars.newHeaderValue)",
),
)
req, err := http.NewRequest("GET", "http://example.net", nil)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
ctx := context.Background()
ctx = WithRequest(ctx, req)
vars := Vars{
NewHeaderKey: "X-My-Header",
NewHeaderValue: "foobar",
}
if _, err := engine.Apply(ctx, vars); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if e, g := vars.NewHeaderValue, req.Header.Get(vars.NewHeaderKey); e != g {
t.Errorf("req.Header.Get(vars.NewHeaderKey): expected '%v', got '%v'", e, g)
}
}
func TestSetRequestHeader(t *testing.T) {
type Vars struct {
HeaderKey string `expr:"headerKey"`
HeaderValue string `expr:"headerValue"`
}
engine := createRuleEngine[Vars](t,
rule.WithExpr(setRequestHeaderFunc()),
rule.WithRules(
"set_header(ctx, vars.headerKey, vars.headerValue)",
),
)
req, err := http.NewRequest("GET", "http://example.net", nil)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
vars := Vars{
HeaderKey: "X-My-Header",
HeaderValue: "foobar",
}
req.Header.Set(vars.HeaderKey, "test")
ctx := context.Background()
ctx = WithRequest(ctx, req)
if _, err := engine.Apply(ctx, vars); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if e, g := vars.HeaderValue, req.Header.Get(vars.HeaderKey); e != g {
t.Errorf("req.Header.Get(vars.HeaderKey): expected '%v', got '%v'", e, g)
}
}
func TestDelRequestHeaders(t *testing.T) {
type Vars struct {
HeaderPattern string `expr:"headerPattern"`
}
engine := createRuleEngine[Vars](t,
rule.WithExpr(delRequestHeadersFunc()),
rule.WithRules(
"del_headers(ctx, vars.headerPattern)",
),
)
req, err := http.NewRequest("GET", "http://example.net", nil)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
vars := Vars{
HeaderPattern: "X-My-*",
}
req.Header.Set("X-My-Header", "test")
ctx := context.Background()
ctx = WithRequest(ctx, req)
if _, err := engine.Apply(ctx, vars); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if val := req.Header.Get("X-My-Header"); val != "" {
t.Errorf("req.Header.Get(\"X-My-Header\") should be empty, got '%v'", val)
}
}
func createRuleEngine[V any](t *testing.T, funcs ...rule.OptionFunc) *rule.Engine[V] {
engine, err := rule.NewEngine[V](funcs...)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
return engine
}

View File

@ -1,33 +1,22 @@
package http package http
import ( import (
"context"
"fmt" "fmt"
"net/http"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"forge.cadoles.com/Cadoles/go-proxy/wildcard" "forge.cadoles.com/Cadoles/go-proxy/wildcard"
"forge.cadoles.com/cadoles/bouncer/internal/rule"
"github.com/expr-lang/expr" "github.com/expr-lang/expr"
"github.com/pkg/errors"
) )
func addResponseHeaderFunc() expr.Option { func addResponseHeaderFunc(r *http.Response) expr.Option {
return expr.Function( return expr.Function(
"add_header", "add_header",
func(params ...any) (any, error) { func(params ...any) (any, error) {
ctx, err := rule.Assert[context.Context](params[0]) name := params[0].(string)
if err != nil { rawValue := params[1]
return nil, errors.WithStack(err)
}
name, err := rule.Assert[string](params[1])
if err != nil {
return nil, errors.WithStack(err)
}
rawValue := params[2]
var value string var value string
switch v := rawValue.(type) { switch v := rawValue.(type) {
@ -41,34 +30,20 @@ func addResponseHeaderFunc() expr.Option {
value = fmt.Sprintf("%v", rawValue) value = fmt.Sprintf("%v", rawValue)
} }
r, ok := ctxResponse(ctx)
if !ok {
return nil, errors.New("could not find http response in context")
}
r.Header.Add(name, value) r.Header.Add(name, value)
return true, nil return true, nil
}, },
new(func(context.Context, string, string) bool), new(func(string, string) bool),
) )
} }
func setResponseHeaderFunc() expr.Option { func setResponseHeaderFunc(r *http.Response) expr.Option {
return expr.Function( return expr.Function(
"set_header", "set_header",
func(params ...any) (any, error) { func(params ...any) (any, error) {
ctx, err := rule.Assert[context.Context](params[0]) name := params[0].(string)
if err != nil { rawValue := params[1]
return nil, errors.WithStack(err)
}
name, err := rule.Assert[string](params[1])
if err != nil {
return nil, errors.WithStack(err)
}
rawValue := params[2]
var value string var value string
switch v := rawValue.(type) { switch v := rawValue.(type) {
@ -82,38 +57,19 @@ func setResponseHeaderFunc() expr.Option {
value = fmt.Sprintf("%v", rawValue) value = fmt.Sprintf("%v", rawValue)
} }
r, ok := ctxResponse(ctx)
if !ok {
return nil, errors.New("could not find http response in context")
}
r.Header.Set(name, value) r.Header.Set(name, value)
return true, nil return true, nil
}, },
new(func(context.Context, string, string) bool), new(func(string, string) bool),
) )
} }
func delResponseHeadersFunc() expr.Option { func delResponseHeadersFunc(r *http.Response) expr.Option {
return expr.Function( return expr.Function(
"del_headers", "del_headers",
func(params ...any) (any, error) { func(params ...any) (any, error) {
ctx, err := rule.Assert[context.Context](params[0]) pattern := params[0].(string)
if err != nil {
return nil, errors.WithStack(err)
}
pattern, err := rule.Assert[string](params[1])
if err != nil {
return nil, errors.WithStack(err)
}
r, ok := ctxResponse(ctx)
if !ok {
return nil, errors.New("could not find http response in context")
}
deleted := false deleted := false
for key := range r.Header { for key := range r.Header {
@ -127,6 +83,6 @@ func delResponseHeadersFunc() expr.Option {
return deleted, nil return deleted, nil
}, },
new(func(context.Context, string) bool), new(func(string) bool),
) )
} }

View File

@ -1,139 +0,0 @@
package http
import (
"context"
"io"
"net/http"
"testing"
"forge.cadoles.com/cadoles/bouncer/internal/rule"
"github.com/pkg/errors"
)
func TestAddResponseHeader(t *testing.T) {
type Vars struct {
NewHeaderKey string `expr:"newHeaderKey"`
NewHeaderValue string `expr:"newHeaderValue"`
}
engine := createRuleEngine[Vars](t,
rule.WithExpr(addResponseHeaderFunc()),
rule.WithRules(
"add_header(ctx, vars.newHeaderKey, vars.newHeaderValue)",
),
)
req, err := http.NewRequest("GET", "http://example.net", nil)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
resp := createResponse(req, http.StatusOK, nil)
ctx := context.Background()
ctx = WithResponse(ctx, resp)
vars := Vars{
NewHeaderKey: "X-My-Header",
NewHeaderValue: "foobar",
}
if _, err := engine.Apply(ctx, vars); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if e, g := vars.NewHeaderValue, resp.Header.Get(vars.NewHeaderKey); e != g {
t.Errorf("resp.Header.Get(vars.NewHeaderKey): expected '%v', got '%v'", e, g)
}
}
func TestResponseSetHeader(t *testing.T) {
type Vars struct {
HeaderKey string `expr:"headerKey"`
HeaderValue string `expr:"headerValue"`
}
engine := createRuleEngine[Vars](t,
rule.WithExpr(setResponseHeaderFunc()),
rule.WithRules(
"set_header(ctx, vars.headerKey, vars.headerValue)",
),
)
req, err := http.NewRequest("GET", "http://example.net", nil)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
resp := createResponse(req, http.StatusOK, nil)
vars := Vars{
HeaderKey: "X-My-Header",
HeaderValue: "foobar",
}
resp.Header.Set(vars.HeaderKey, "test")
ctx := context.Background()
ctx = WithResponse(ctx, resp)
if _, err := engine.Apply(ctx, vars); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if e, g := vars.HeaderValue, resp.Header.Get(vars.HeaderKey); e != g {
t.Errorf("resp.Header.Get(vars.HeaderKey): expected '%v', got '%v'", e, g)
}
}
func TestResponseDelHeaders(t *testing.T) {
type Vars struct {
HeaderPattern string `expr:"headerPattern"`
}
engine := createRuleEngine[Vars](t,
rule.WithExpr(delResponseHeadersFunc()),
rule.WithRules(
"del_headers(ctx, vars.headerPattern)",
),
)
req, err := http.NewRequest("GET", "http://example.net", nil)
if err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
resp := createResponse(req, http.StatusOK, nil)
vars := Vars{
HeaderPattern: "X-My-*",
}
resp.Header.Set("X-My-Header", "test")
ctx := context.Background()
ctx = WithResponse(ctx, resp)
if _, err := engine.Apply(ctx, vars); err != nil {
t.Fatalf("%+v", errors.WithStack(err))
}
if val := resp.Header.Get("X-My-Header"); val != "" {
t.Errorf("resp.Header.Get(\"X-My-Header\") should be empty, got '%v'", val)
}
}
func createResponse(req *http.Request, statusCode int, body io.Reader) *http.Response {
return &http.Response{
Status: http.StatusText(statusCode),
StatusCode: statusCode,
Proto: "HTTP/1.1",
ProtoMajor: 1,
ProtoMinor: 1,
Body: io.NopCloser(body),
ContentLength: -1,
Request: req,
Header: make(http.Header, 0),
}
}