Layer rewriter
: réécriture dynamiques des attributs de requêtes/réponses
#29
@ -4,3 +4,4 @@ Vous trouverez ci-dessous la liste des entités "Layer" activables sur vos entit
|
|||||||
|
|
||||||
- [Authn (`authn-*`)](./authn/README.md) - Authentification des accès (SSO)
|
- [Authn (`authn-*`)](./authn/README.md) - Authentification des accès (SSO)
|
||||||
- [Queue](./queue.md) - File d'attente dynamique
|
- [Queue](./queue.md) - File d'attente dynamique
|
||||||
|
- [Rewriter](./rewriter.md) - Réécriture dynamiques des attributs des requêtes/réponses
|
||||||
|
116
doc/fr/references/layers/rewriter.md
Normal file
116
doc/fr/references/layers/rewriter.md
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
# Layer "Rewriter"
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
Ce layer permet de modifier dynamiquement certains attributs de requêtes/réponses transitant par le proxy.
|
||||||
|
|
||||||
|
## Type
|
||||||
|
|
||||||
|
`rewriter`
|
||||||
|
|
||||||
|
## Schéma des options
|
||||||
|
|
||||||
|
Les options disponibles pour le layer sont décrites via un [schéma JSON](https://json-schema.org/specification). Elles sont documentées dans le [schéma visible ici](../../../../internal/proxy/director/layer/rewriter/layer-options.json).
|
||||||
|
|
||||||
|
## Moteur de règles
|
||||||
|
|
||||||
|
Les options `rules.request` et `rules.response` permettent de définir des listes de règles utilisant un DSL modifiant de manière dynamique les attributs des requêtes/réponses transitant par le proxy.
|
||||||
|
|
||||||
|
Les listes d'instructions sont exécutées séquentiellement.
|
||||||
|
|
||||||
|
Bouncer utilise le projet [`expr`](https://expr-lang.org/) comme DSL. En plus des fonctionnalités natives du langage, Bouncer ajoute un certain nombre de fonctions spécifiques au contexte d'utilisation.
|
||||||
|
|
||||||
|
### Fonctions
|
||||||
|
|
||||||
|
#### Communes
|
||||||
|
|
||||||
|
##### `add_header(name string, value string)`
|
||||||
|
|
||||||
|
Ajouter une valeur à un entête HTTP via son nom `name` et sa valeur `value`.
|
||||||
|
|
||||||
|
##### `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.
|
||||||
|
|
||||||
|
##### `del_headers(pattern string)`
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
#### Requête
|
||||||
|
|
||||||
|
##### `set_host(host string)`
|
||||||
|
|
||||||
|
Modifier la valeur de l'entête `Host` de la requête.
|
||||||
|
|
||||||
|
##### `set_url(url string)`
|
||||||
|
|
||||||
|
Modifier l'URL du serveur cible.
|
||||||
|
|
||||||
|
#### Réponse
|
||||||
|
|
||||||
|
_Pas de fonctions spécifiques._
|
||||||
|
|
||||||
|
### Environnement
|
||||||
|
|
||||||
|
Les règles ont accès aux variables suivantes pendant leur exécution. **Ces données sont en lecture seule.**
|
||||||
|
|
||||||
|
#### Requête
|
||||||
|
|
||||||
|
##### `request`
|
||||||
|
|
||||||
|
La requête en cours de traitement.
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
method: "string", // Méthode HTTP
|
||||||
|
host: "string", // Nom d'hôte (`Host`) associé à la requête
|
||||||
|
url: "string", // URL associée à la requête
|
||||||
|
proto: "string", // Numéro de version du protocole utilisé
|
||||||
|
protoMajor: "int", // Numéro de version majeure 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
|
||||||
|
"string": ["string"]
|
||||||
|
},
|
||||||
|
contentLength: "int", // Taille 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
|
||||||
|
"string": ["string"]
|
||||||
|
},
|
||||||
|
remoteAddr: "string", // Adresse du client HTTP à l'origine de la requête
|
||||||
|
requestUri: "string" // URL "brute" associée à la requêtes (avant opérations d'assainissement, utiliser "url" plutôt)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Réponse
|
||||||
|
|
||||||
|
##### `response`
|
||||||
|
|
||||||
|
La réponse en cours de traitement.
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
statusCode: "int", // Code de statut de la réponse
|
||||||
|
status: "string", // Message associé au code de statut
|
||||||
|
proto: "string", // Numéro de version du protocole utilisé
|
||||||
|
protoMajor: "int", // Numéro de version majeure 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
|
||||||
|
"string": ["string"]
|
||||||
|
},
|
||||||
|
contentLength: "int", // Taille du corps de la réponse
|
||||||
|
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
|
||||||
|
"string": ["string"]
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### `request`
|
||||||
|
|
||||||
|
_Voir section précédente._
|
||||||
|
|
||||||
|
## Métriques
|
||||||
|
|
||||||
|
_Pas de métriques spécifiques._
|
38
internal/proxy/director/layer/rewriter/layer-options.json
Normal file
38
internal/proxy/director/layer/rewriter/layer-options.json
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"rules": {
|
||||||
|
"title": "Règles appliquées aux requêtes/réponses transitant par le proxy",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"request": {
|
||||||
|
"title": "Règles appliquées aux requêtes transitant par le proxy",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"response": {
|
||||||
|
"title": "Règles appliquées aux réponses transitant par le proxy",
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
},
|
||||||
|
"matchURLs": {
|
||||||
|
"title": "Liste de filtrage des URLs sur lesquelles le layer est actif",
|
||||||
|
"description": "Par exemple, si vous souhaitez limiter votre layer à l'ensemble d'une section '`/blog`' d'un site, vous pouvez déclarer la valeur `['*/blog*']`. Les autres URLs du site ne seront pas affectées par ce layer.",
|
||||||
|
"default": [
|
||||||
|
"*"
|
||||||
|
],
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"additionalProperties": false
|
||||||
|
}
|
84
internal/proxy/director/layer/rewriter/layer.go
Normal file
84
internal/proxy/director/layer/rewriter/layer.go
Normal file
@ -0,0 +1,84 @@
|
|||||||
|
package rewriter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
proxy "forge.cadoles.com/Cadoles/go-proxy"
|
||||||
|
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
"gitlab.com/wpetit/goweb/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
const LayerType store.LayerType = "rewriter"
|
||||||
|
|
||||||
|
type Layer struct{}
|
||||||
|
|
||||||
|
func (l *Layer) LayerType() store.LayerType {
|
||||||
|
return LayerType
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
|
||||||
|
return func(next http.Handler) http.Handler {
|
||||||
|
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
options, err := fromStoreOptions(layer.Options)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(ctx, "could not parse layer options", logger.E(errors.WithStack(err)))
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
matches := wildcard.MatchAny(r.URL.String(), options.MatchURLs...)
|
||||||
|
if !matches {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := l.applyRequestRules(r, options); err != nil {
|
||||||
|
logger.Error(ctx, "could not apply request rules", logger.E(errors.WithStack(err)))
|
||||||
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
return http.HandlerFunc(fn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponseTransformer implements director.ResponseTransformerLayer.
|
||||||
|
func (l *Layer) ResponseTransformer(layer *store.Layer) proxy.ResponseTransformer {
|
||||||
|
return func(r *http.Response) error {
|
||||||
|
options, err := fromStoreOptions(layer.Options)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
matches := wildcard.MatchAny(r.Request.URL.String(), options.MatchURLs...)
|
||||||
|
if !matches {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := l.applyResponseRules(r, options); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func New() *Layer {
|
||||||
|
return &Layer{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ director.MiddlewareLayer = &Layer{}
|
||||||
|
_ director.ResponseTransformerLayer = &Layer{}
|
||||||
|
)
|
56
internal/proxy/director/layer/rewriter/layer_options.go
Normal file
56
internal/proxy/director/layer/rewriter/layer_options.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
package rewriter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/store"
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LayerOptions struct {
|
||||||
|
MatchURLs []string `mapstructure:"matchURLs"`
|
||||||
|
Rules Rules `mapstructure:"rules"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Rules struct {
|
||||||
|
Request []string `mapstructure:"request"`
|
||||||
|
Response []string `mapstructure:"response"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultLayerOptions() LayerOptions {
|
||||||
|
return LayerOptions{
|
||||||
|
MatchURLs: []string{"*"},
|
||||||
|
Rules: Rules{
|
||||||
|
Request: []string{},
|
||||||
|
Response: []string{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromStoreOptions(storeOptions store.LayerOptions) (*LayerOptions, error) {
|
||||||
|
layerOptions := DefaultLayerOptions()
|
||||||
|
|
||||||
|
if err := FromStoreOptions(storeOptions, &layerOptions); err != nil {
|
||||||
|
return nil, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &layerOptions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromStoreOptions(storeOptions store.LayerOptions, dest any) error {
|
||||||
|
config := mapstructure.DecoderConfig{
|
||||||
|
Result: dest,
|
||||||
|
ZeroFields: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
decoder, err := mapstructure.NewDecoder(&config)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := decoder.Decode(storeOptions); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
133
internal/proxy/director/layer/rewriter/rules.go
Normal file
133
internal/proxy/director/layer/rewriter/rules.go
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
package rewriter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/rule"
|
||||||
|
ruleHTTP "forge.cadoles.com/cadoles/bouncer/internal/rule/http"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RequestEnv struct {
|
||||||
|
Request RequestInfo `expr:"request"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RequestInfo struct {
|
||||||
|
Method string `expr:"method"`
|
||||||
|
URL string `expr:"url"`
|
||||||
|
Proto string `expr:"proto"`
|
||||||
|
ProtoMajor int `expr:"protoMajor"`
|
||||||
|
ProtoMinor int `expr:"protoMinor"`
|
||||||
|
Header map[string][]string `expr:"header"`
|
||||||
|
ContentLength int64 `expr:"contentLength"`
|
||||||
|
TransferEncoding []string `expr:"transferEncoding"`
|
||||||
|
Host string `expr:"host"`
|
||||||
|
Trailer map[string][]string `expr:"trailer"`
|
||||||
|
RemoteAddr string `expr:"remoteAddr"`
|
||||||
|
RequestURI string `expr:"requestUri"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Layer) applyRequestRules(r *http.Request, options *LayerOptions) error {
|
||||||
|
rules := options.Rules.Request
|
||||||
|
if len(rules) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
engine, err := rule.NewEngine[*RequestEnv](
|
||||||
|
ruleHTTP.WithRequestFuncs(r),
|
||||||
|
rule.WithRules(options.Rules.Request...),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
env := &RequestEnv{
|
||||||
|
Request: RequestInfo{
|
||||||
|
Method: r.Method,
|
||||||
|
URL: r.URL.String(),
|
||||||
|
Proto: r.Proto,
|
||||||
|
ProtoMajor: r.ProtoMajor,
|
||||||
|
ProtoMinor: r.ProtoMinor,
|
||||||
|
Header: r.Header,
|
||||||
|
ContentLength: r.ContentLength,
|
||||||
|
TransferEncoding: r.TransferEncoding,
|
||||||
|
Host: r.Host,
|
||||||
|
Trailer: r.Trailer,
|
||||||
|
RemoteAddr: r.RemoteAddr,
|
||||||
|
RequestURI: r.RequestURI,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := engine.Apply(env); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResponseEnv struct {
|
||||||
|
Request RequestInfo `expr:"request"`
|
||||||
|
Response ResponseInfo `expr:"response"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResponseInfo struct {
|
||||||
|
Status string `expr:"status"`
|
||||||
|
StatusCode int `expr:"statusCode"`
|
||||||
|
Proto string `expr:"proto"`
|
||||||
|
ProtoMajor int `expr:"protoMajor"`
|
||||||
|
ProtoMinor int `expr:"protoMinor"`
|
||||||
|
Header map[string][]string `expr:"header"`
|
||||||
|
ContentLength int64 `expr:"contentLength"`
|
||||||
|
TransferEncoding []string `expr:"transferEncoding"`
|
||||||
|
Uncompressed bool `expr:"uncompressed"`
|
||||||
|
Trailer map[string][]string `expr:"trailer"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Layer) applyResponseRules(r *http.Response, options *LayerOptions) error {
|
||||||
|
rules := options.Rules.Request
|
||||||
|
if len(rules) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
engine, err := rule.NewEngine[*ResponseEnv](
|
||||||
|
rule.WithRules(options.Rules.Response...),
|
||||||
|
ruleHTTP.WithResponseFuncs(r),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
env := &ResponseEnv{
|
||||||
|
Request: RequestInfo{
|
||||||
|
Method: r.Request.Method,
|
||||||
|
URL: r.Request.URL.String(),
|
||||||
|
Proto: r.Request.Proto,
|
||||||
|
ProtoMajor: r.Request.ProtoMajor,
|
||||||
|
ProtoMinor: r.Request.ProtoMinor,
|
||||||
|
Header: r.Request.Header,
|
||||||
|
ContentLength: r.Request.ContentLength,
|
||||||
|
TransferEncoding: r.Request.TransferEncoding,
|
||||||
|
Host: r.Request.Host,
|
||||||
|
Trailer: r.Request.Trailer,
|
||||||
|
RemoteAddr: r.Request.RemoteAddr,
|
||||||
|
RequestURI: r.Request.RequestURI,
|
||||||
|
},
|
||||||
|
Response: ResponseInfo{
|
||||||
|
Proto: r.Proto,
|
||||||
|
ProtoMajor: r.ProtoMajor,
|
||||||
|
ProtoMinor: r.ProtoMinor,
|
||||||
|
Header: r.Header,
|
||||||
|
ContentLength: r.ContentLength,
|
||||||
|
TransferEncoding: r.TransferEncoding,
|
||||||
|
Trailer: r.Trailer,
|
||||||
|
Status: r.Status,
|
||||||
|
StatusCode: r.StatusCode,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := engine.Apply(env); err != nil {
|
||||||
|
return errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
8
internal/proxy/director/layer/rewriter/schema.go
Normal file
8
internal/proxy/director/layer/rewriter/schema.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package rewriter
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed layer-options.json
|
||||||
|
var RawLayerOptionsSchema []byte
|
45
internal/rule/engine.go
Normal file
45
internal/rule/engine.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
package rule
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/expr-lang/expr"
|
||||||
|
"github.com/expr-lang/expr/vm"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Engine[E any] struct {
|
||||||
|
rules []*vm.Program
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *Engine[E]) Apply(env E) ([]any, error) {
|
||||||
|
results := make([]any, 0, len(e.rules))
|
||||||
|
for i, r := range e.rules {
|
||||||
|
result, err := expr.Run(r, env)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "could not run rule #%d", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
results = append(results, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
return results, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewEngine[E any](funcs ...OptionFunc) (*Engine[E], error) {
|
||||||
|
opts := NewOptions(funcs...)
|
||||||
|
|
||||||
|
engine := &Engine[E]{
|
||||||
|
rules: make([]*vm.Program, 0, len(opts.Rules)),
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, r := range opts.Rules {
|
||||||
|
|
||||||
|
program, err := expr.Compile(r, opts.Expr...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, errors.Wrapf(err, "could not compile rule #%d", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
engine.rules = append(engine.rules, program)
|
||||||
|
}
|
||||||
|
|
||||||
|
return engine, nil
|
||||||
|
}
|
42
internal/rule/http/option.go
Normal file
42
internal/rule/http/option.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/rule"
|
||||||
|
"github.com/expr-lang/expr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func WithRequestFuncs(r *http.Request) rule.OptionFunc {
|
||||||
|
return func(opts *rule.Options) {
|
||||||
|
funcs := []expr.Option{
|
||||||
|
setRequestURL(r),
|
||||||
|
setRequestHeaderFunc(r),
|
||||||
|
addRequestHeaderFunc(r),
|
||||||
|
delRequestHeadersFunc(r),
|
||||||
|
setRequestHostFunc(r),
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(opts.Expr) == 0 {
|
||||||
|
opts.Expr = make([]expr.Option, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.Expr = append(opts.Expr, funcs...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithResponseFuncs(r *http.Response) rule.OptionFunc {
|
||||||
|
return func(opts *rule.Options) {
|
||||||
|
funcs := []expr.Option{
|
||||||
|
setResponseHeaderFunc(r),
|
||||||
|
addResponseHeaderFunc(r),
|
||||||
|
delResponseHeadersFunc(r),
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(opts.Expr) == 0 {
|
||||||
|
opts.Expr = make([]expr.Option, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts.Expr = append(opts.Expr, funcs...)
|
||||||
|
}
|
||||||
|
}
|
122
internal/rule/http/request.go
Normal file
122
internal/rule/http/request.go
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
|
||||||
|
"github.com/expr-lang/expr"
|
||||||
|
"github.com/pkg/errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setRequestHostFunc(r *http.Request) expr.Option {
|
||||||
|
return expr.Function(
|
||||||
|
"set_host",
|
||||||
|
func(params ...any) (any, error) {
|
||||||
|
host := params[0].(string)
|
||||||
|
r.Host = host
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
|
new(func(string) bool),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setRequestURL(r *http.Request) expr.Option {
|
||||||
|
return expr.Function(
|
||||||
|
"set_url",
|
||||||
|
func(params ...any) (any, error) {
|
||||||
|
rawURL := params[0].(string)
|
||||||
|
|
||||||
|
url, err := url.Parse(rawURL)
|
||||||
|
if err != nil {
|
||||||
|
return false, errors.WithStack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.URL = url
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
|
new(func(string) bool),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addRequestHeaderFunc(r *http.Request) expr.Option {
|
||||||
|
return expr.Function(
|
||||||
|
"add_header",
|
||||||
|
func(params ...any) (any, error) {
|
||||||
|
name := params[0].(string)
|
||||||
|
rawValue := params[1]
|
||||||
|
|
||||||
|
var value string
|
||||||
|
switch v := rawValue.(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", rawValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Header.Add(name, value)
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
|
new(func(string, string) bool),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setRequestHeaderFunc(r *http.Request) expr.Option {
|
||||||
|
return expr.Function(
|
||||||
|
"set_header",
|
||||||
|
func(params ...any) (any, error) {
|
||||||
|
name := params[0].(string)
|
||||||
|
rawValue := params[1]
|
||||||
|
|
||||||
|
var value string
|
||||||
|
switch v := rawValue.(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", rawValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Header.Set(name, value)
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
|
new(func(string, string) bool),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func delRequestHeadersFunc(r *http.Request) expr.Option {
|
||||||
|
return expr.Function(
|
||||||
|
"del_headers",
|
||||||
|
func(params ...any) (any, error) {
|
||||||
|
pattern := params[0].(string)
|
||||||
|
deleted := false
|
||||||
|
|
||||||
|
for key := range r.Header {
|
||||||
|
if !wildcard.Match(key, pattern) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Header.Del(key)
|
||||||
|
deleted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return deleted, nil
|
||||||
|
},
|
||||||
|
new(func(string) bool),
|
||||||
|
)
|
||||||
|
}
|
88
internal/rule/http/response.go
Normal file
88
internal/rule/http/response.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"forge.cadoles.com/Cadoles/go-proxy/wildcard"
|
||||||
|
"github.com/expr-lang/expr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addResponseHeaderFunc(r *http.Response) expr.Option {
|
||||||
|
return expr.Function(
|
||||||
|
"add_header",
|
||||||
|
func(params ...any) (any, error) {
|
||||||
|
name := params[0].(string)
|
||||||
|
rawValue := params[1]
|
||||||
|
|
||||||
|
var value string
|
||||||
|
switch v := rawValue.(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", rawValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Header.Add(name, value)
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
|
new(func(string, string) bool),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setResponseHeaderFunc(r *http.Response) expr.Option {
|
||||||
|
return expr.Function(
|
||||||
|
"set_header",
|
||||||
|
func(params ...any) (any, error) {
|
||||||
|
name := params[0].(string)
|
||||||
|
rawValue := params[1]
|
||||||
|
|
||||||
|
var value string
|
||||||
|
switch v := rawValue.(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", rawValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Header.Set(name, value)
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
},
|
||||||
|
new(func(string, string) bool),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func delResponseHeadersFunc(r *http.Response) expr.Option {
|
||||||
|
return expr.Function(
|
||||||
|
"del_headers",
|
||||||
|
func(params ...any) (any, error) {
|
||||||
|
pattern := params[0].(string)
|
||||||
|
deleted := false
|
||||||
|
|
||||||
|
for key := range r.Header {
|
||||||
|
if !wildcard.Match(key, pattern) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Header.Del(key)
|
||||||
|
deleted = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return deleted, nil
|
||||||
|
},
|
||||||
|
new(func(string) bool),
|
||||||
|
)
|
||||||
|
}
|
35
internal/rule/options.go
Normal file
35
internal/rule/options.go
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
package rule
|
||||||
|
|
||||||
|
import "github.com/expr-lang/expr"
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
Rules []string
|
||||||
|
Expr []expr.Option
|
||||||
|
}
|
||||||
|
|
||||||
|
type OptionFunc func(opts *Options)
|
||||||
|
|
||||||
|
func NewOptions(funcs ...OptionFunc) *Options {
|
||||||
|
opts := &Options{
|
||||||
|
Expr: make([]expr.Option, 0),
|
||||||
|
Rules: make([]string, 0),
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fn := range funcs {
|
||||||
|
fn(opts)
|
||||||
|
}
|
||||||
|
|
||||||
|
return opts
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithRules(rules ...string) OptionFunc {
|
||||||
|
return func(opts *Options) {
|
||||||
|
opts.Rules = rules
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithExpr(options ...expr.Option) OptionFunc {
|
||||||
|
return func(opts *Options) {
|
||||||
|
opts.Expr = options
|
||||||
|
}
|
||||||
|
}
|
15
internal/setup/rewriter_layer.go
Normal file
15
internal/setup/rewriter_layer.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package setup
|
||||||
|
|
||||||
|
import (
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/config"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||||
|
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director/layer/rewriter"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
RegisterLayer(rewriter.LayerType, setupRewriterLayer, rewriter.RawLayerOptionsSchema)
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupRewriterLayer(conf *config.Config) (director.Layer, error) {
|
||||||
|
return rewriter.New(), nil
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user