feat: rewriter layer
This commit is contained in:
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
|
Reference in New Issue
Block a user