feat(rewriter): add redirect(), get_cookie(), add_cookie() methods to rule engine (#36)
Some checks are pending
Cadoles/bouncer/pipeline/pr-develop Build started...

This commit is contained in:
2024-09-25 15:50:13 +02:00
committed by wpetit
parent cb9260ac2b
commit 04b41baea3
11 changed files with 710 additions and 110 deletions

View File

@ -0,0 +1,79 @@
package rewriter
import (
"context"
"fmt"
"forge.cadoles.com/cadoles/bouncer/internal/rule"
"github.com/expr-lang/expr"
"github.com/pkg/errors"
)
type errRedirect struct {
statusCode int
url string
}
func (e *errRedirect) StatusCode() int {
return e.statusCode
}
func (e *errRedirect) URL() string {
return e.url
}
func (e *errRedirect) Error() string {
return fmt.Sprintf("redirect %d %s", e.statusCode, e.url)
}
func newErrRedirect(statusCode int, url string) *errRedirect {
return &errRedirect{
url: url,
statusCode: statusCode,
}
}
var _ error = &errRedirect{}
func redirectFunc() expr.Option {
return expr.Function(
"redirect",
func(params ...any) (any, error) {
_, err := rule.Assert[context.Context](params[0])
if err != nil {
return nil, errors.WithStack(err)
}
statusCode, err := rule.Assert[int](params[1])
if err != nil {
return nil, errors.WithStack(err)
}
if statusCode < 300 || statusCode >= 400 {
return nil, errors.Errorf("unexpected redirect status code '%d'", statusCode)
}
url, err := rule.Assert[string](params[2])
if err != nil {
return nil, errors.WithStack(err)
}
return nil, newErrRedirect(statusCode, url)
},
new(func(context.Context, int, string) bool),
)
}
func WithRewriterFuncs() rule.OptionFunc {
return func(opts *rule.Options) {
funcs := []expr.Option{
redirectFunc(),
}
if len(opts.Expr) == 0 {
opts.Expr = make([]expr.Option, 0)
}
opts.Expr = append(opts.Expr, funcs...)
}
}

View File

@ -46,6 +46,12 @@ func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
}
if err := l.applyRequestRules(ctx, r, layer.Revision, options); err != nil {
var redirect *errRedirect
if errors.As(err, &redirect) {
http.Redirect(w, r, redirect.URL(), redirect.StatusCode())
return
}
logger.Error(ctx, "could not apply request rules", logger.E(errors.WithStack(err)))
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
@ -88,6 +94,7 @@ func New(funcs ...OptionFunc) *Layer {
engine, err := rule.NewEngine[*RequestVars](
rule.WithRules(options.Rules.Request...),
ruleHTTP.WithRequestFuncs(),
WithRewriterFuncs(),
)
if err != nil {
return nil, errors.WithStack(err)

View File

@ -3,6 +3,7 @@ package rewriter
import (
"context"
"net/http"
"net/url"
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
"forge.cadoles.com/cadoles/bouncer/internal/rule"
@ -27,6 +28,26 @@ type URLVar struct {
RawFragment string `expr:"raw_fragment"`
}
func fromURL(url *url.URL) URLVar {
return URLVar{
Scheme: url.Scheme,
Opaque: url.Opaque,
User: UserVar{
Username: url.User.Username(),
Password: func() string {
passwd, _ := url.User.Password()
return passwd
}(),
},
Host: url.Host,
Path: url.Path,
RawPath: url.RawPath,
RawQuery: url.RawQuery,
Fragment: url.Fragment,
RawFragment: url.RawFragment,
}
}
type UserVar struct {
Username string `expr:"username"`
Password string `expr:"password"`
@ -48,6 +69,24 @@ type RequestVar struct {
RequestURI string `expr:"request_uri"`
}
func fromRequest(r *http.Request) RequestVar {
return RequestVar{
Method: r.Method,
URL: fromURL(r.URL),
RawURL: 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,
}
}
func (l *Layer) applyRequestRules(ctx context.Context, r *http.Request, layerRevision int, options *LayerOptions) error {
rules := options.Rules.Request
if len(rules) == 0 {
@ -65,54 +104,8 @@ func (l *Layer) applyRequestRules(ctx context.Context, r *http.Request, layerRev
}
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,
URL: URLVar{
Scheme: r.URL.Scheme,
Opaque: r.URL.Opaque,
User: UserVar{
Username: r.URL.User.Username(),
Password: func() string {
passwd, _ := r.URL.User.Password()
return passwd
}(),
},
Host: r.URL.Host,
Path: r.URL.Path,
RawPath: r.URL.RawPath,
RawQuery: r.URL.RawQuery,
Fragment: r.URL.Fragment,
RawFragment: r.URL.RawFragment,
},
RawURL: 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,
},
OriginalURL: fromURL(originalURL),
Request: fromRequest(r),
}
ctx = ruleHTTP.WithRequest(ctx, r)
@ -169,54 +162,8 @@ func (l *Layer) applyResponseRules(ctx context.Context, r *http.Response, layerR
}
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,
URL: URLVar{
Scheme: r.Request.URL.Scheme,
Opaque: r.Request.URL.Opaque,
User: UserVar{
Username: r.Request.URL.User.Username(),
Password: func() string {
passwd, _ := r.Request.URL.User.Password()
return passwd
}(),
},
Host: r.Request.URL.Host,
Path: r.Request.URL.Path,
RawPath: r.Request.URL.RawPath,
RawQuery: r.Request.URL.RawQuery,
Fragment: r.Request.URL.Fragment,
RawFragment: r.Request.URL.RawFragment,
},
RawURL: 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,
},
OriginalURL: fromURL(originalURL),
Request: fromRequest(r.Request),
Response: ResponseVar{
Proto: r.Proto,
ProtoMajor: r.ProtoMajor,
@ -231,6 +178,7 @@ func (l *Layer) applyResponseRules(ctx context.Context, r *http.Response, layerR
}
ctx = ruleHTTP.WithResponse(ctx, r)
ctx = ruleHTTP.WithRequest(ctx, r.Request)
if _, err := engine.Apply(ctx, vars); err != nil {
return errors.WithStack(err)