Merge pull request 'Layer rewriter
: réécriture dynamiques des attributs de requêtes/réponses' (#29) from rewriter-layer into develop
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
All checks were successful
Cadoles/bouncer/pipeline/head This commit looks good
Reviewed-on: #29
This commit is contained in:
commit
114608931b
@ -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)
|
||||
- [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