feat: reusable rule engine to prevent memory reallocation
All checks were successful
Cadoles/bouncer/pipeline/pr-develop This commit looks good
All checks were successful
Cadoles/bouncer/pipeline/pr-develop This commit looks good
This commit is contained in:
@ -74,7 +74,7 @@ func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
|
||||
return
|
||||
}
|
||||
|
||||
if err := l.applyRules(r, options, user); err != nil {
|
||||
if err := l.applyRules(ctx, r, options, user); err != nil {
|
||||
if errors.Is(err, ErrForbidden) {
|
||||
l.renderForbiddenPage(w, r, layer, options, user)
|
||||
return
|
||||
|
@ -1,6 +1,7 @@
|
||||
package authn
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/rule"
|
||||
@ -9,30 +10,32 @@ import (
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type Env struct {
|
||||
type Vars struct {
|
||||
User *User `expr:"user"`
|
||||
}
|
||||
|
||||
func (l *Layer) applyRules(r *http.Request, options *LayerOptions, user *User) error {
|
||||
func (l *Layer) applyRules(ctx context.Context, r *http.Request, options *LayerOptions, user *User) error {
|
||||
rules := options.Rules
|
||||
if len(rules) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
engine, err := rule.NewEngine[*Env](
|
||||
engine, err := rule.NewEngine[*Vars](
|
||||
rule.WithRules(options.Rules...),
|
||||
rule.WithExpr(getAuthnAPI()...),
|
||||
ruleHTTP.WithRequestFuncs(r),
|
||||
ruleHTTP.WithRequestFuncs(),
|
||||
)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
env := &Env{
|
||||
vars := &Vars{
|
||||
User: user,
|
||||
}
|
||||
|
||||
if _, err := engine.Apply(env); err != nil {
|
||||
ctx = ruleHTTP.WithRequest(ctx, r)
|
||||
|
||||
if _, err := engine.Apply(ctx, vars); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
|
@ -6,6 +6,9 @@ import (
|
||||
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/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"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
@ -13,7 +16,10 @@ import (
|
||||
|
||||
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 {
|
||||
return LayerType
|
||||
@ -39,7 +45,7 @@ func (l *Layer) Middleware(layer *store.Layer) proxy.Middleware {
|
||||
return
|
||||
}
|
||||
|
||||
if err := l.applyRequestRules(r, options); err != nil {
|
||||
if err := l.applyRequestRules(ctx, r, layer.Revision, 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)
|
||||
|
||||
@ -66,7 +72,9 @@ func (l *Layer) ResponseTransformer(layer *store.Layer) proxy.ResponseTransforme
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := l.applyResponseRules(r, options); err != nil {
|
||||
ctx := r.Request.Context()
|
||||
|
||||
if err := l.applyResponseRules(ctx, r, layer.Revision, options); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
@ -75,7 +83,30 @@ func (l *Layer) ResponseTransformer(layer *store.Layer) proxy.ResponseTransforme
|
||||
}
|
||||
|
||||
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 (
|
||||
|
@ -1,68 +1,93 @@
|
||||
package rewriter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"forge.cadoles.com/cadoles/bouncer/internal/proxy/director"
|
||||
"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 RequestVars struct {
|
||||
Request RequestVar `expr:"request"`
|
||||
OriginalURL URLVar `expr:"original_url"`
|
||||
}
|
||||
|
||||
type URLEnv struct {
|
||||
Scheme string `expr:"scheme"`
|
||||
Opaque string `expr:"opaque"`
|
||||
User UserInfoEnv `expr:"user"`
|
||||
Host string `expr:"host"`
|
||||
Path string `expr:"path"`
|
||||
RawPath string `expr:"rawPath"`
|
||||
RawQuery string `expr:"rawQuery"`
|
||||
Fragment string `expr:"fragment"`
|
||||
RawFragment string `expr:"rawFragment"`
|
||||
type URLVar struct {
|
||||
Scheme string `expr:"scheme"`
|
||||
Opaque string `expr:"opaque"`
|
||||
User UserVar `expr:"user"`
|
||||
Host string `expr:"host"`
|
||||
Path string `expr:"path"`
|
||||
RawPath string `expr:"raw_path"`
|
||||
RawQuery string `expr:"raw_query"`
|
||||
Fragment string `expr:"fragment"`
|
||||
RawFragment string `expr:"raw_fragment"`
|
||||
}
|
||||
|
||||
type UserInfoEnv struct {
|
||||
type UserVar struct {
|
||||
Username string `expr:"username"`
|
||||
Password string `expr:"password"`
|
||||
}
|
||||
|
||||
type RequestInfo struct {
|
||||
type RequestVar struct {
|
||||
Method string `expr:"method"`
|
||||
URL URLEnv `expr:"url"`
|
||||
RawURL string `expr:"rawUrl"`
|
||||
URL URLVar `expr:"url"`
|
||||
RawURL string `expr:"raw_url"`
|
||||
Proto string `expr:"proto"`
|
||||
ProtoMajor int `expr:"protoMajor"`
|
||||
ProtoMinor int `expr:"protoMinor"`
|
||||
ProtoMajor int `expr:"proto_major"`
|
||||
ProtoMinor int `expr:"proto_minor"`
|
||||
Header map[string][]string `expr:"header"`
|
||||
ContentLength int64 `expr:"contentLength"`
|
||||
TransferEncoding []string `expr:"transferEncoding"`
|
||||
ContentLength int64 `expr:"content_length"`
|
||||
TransferEncoding []string `expr:"transfer_encoding"`
|
||||
Host string `expr:"host"`
|
||||
Trailer map[string][]string `expr:"trailer"`
|
||||
RemoteAddr string `expr:"remoteAddr"`
|
||||
RequestURI string `expr:"requestUri"`
|
||||
RemoteAddr string `expr:"remote_addr"`
|
||||
RequestURI string `expr:"request_uri"`
|
||||
}
|
||||
|
||||
func (l *Layer) applyRequestRules(r *http.Request, options *LayerOptions) error {
|
||||
func (l *Layer) applyRequestRules(ctx context.Context, r *http.Request, layerRevision int, options *LayerOptions) error {
|
||||
rules := options.Rules.Request
|
||||
if len(rules) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
engine, err := l.getRequestRuleEngine(r, options)
|
||||
engine, err := l.getRequestRuleEngine(ctx, layerRevision, options)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
env := &RequestEnv{
|
||||
Request: RequestInfo{
|
||||
originalURL, err := director.OriginalURL(ctx)
|
||||
if err != nil {
|
||||
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,
|
||||
URL: URLEnv{
|
||||
URL: URLVar{
|
||||
Scheme: r.URL.Scheme,
|
||||
Opaque: r.URL.Opaque,
|
||||
User: UserInfoEnv{
|
||||
User: UserVar{
|
||||
Username: r.URL.User.Username(),
|
||||
Password: func() string {
|
||||
passwd, _ := r.URL.User.Password()
|
||||
@ -90,18 +115,17 @@ func (l *Layer) applyRequestRules(r *http.Request, options *LayerOptions) error
|
||||
},
|
||||
}
|
||||
|
||||
if _, err := engine.Apply(env); err != nil {
|
||||
ctx = ruleHTTP.WithRequest(ctx, r)
|
||||
|
||||
if _, err := engine.Apply(ctx, vars); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Layer) getRequestRuleEngine(r *http.Request, options *LayerOptions) (*rule.Engine[*RequestEnv], error) {
|
||||
engine, err := rule.NewEngine[*RequestEnv](
|
||||
rule.WithRules(options.Rules.Request...),
|
||||
ruleHTTP.WithRequestFuncs(r),
|
||||
)
|
||||
func (l *Layer) getRequestRuleEngine(ctx context.Context, layerRevision int, options *LayerOptions) (*rule.Engine[*RequestVars], error) {
|
||||
engine, err := l.requestRuleEngine.Get(ctx, layerRevision, options)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
@ -109,42 +133,65 @@ func (l *Layer) getRequestRuleEngine(r *http.Request, options *LayerOptions) (*r
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
type ResponseEnv struct {
|
||||
Request RequestInfo `expr:"request"`
|
||||
Response ResponseInfo `expr:"response"`
|
||||
type ResponseVars struct {
|
||||
OriginalURL URLVar `expr:"original_url"`
|
||||
Request RequestVar `expr:"request"`
|
||||
Response ResponseVar `expr:"response"`
|
||||
}
|
||||
|
||||
type ResponseInfo struct {
|
||||
type ResponseVar struct {
|
||||
Status string `expr:"status"`
|
||||
StatusCode int `expr:"statusCode"`
|
||||
StatusCode int `expr:"status_code"`
|
||||
Proto string `expr:"proto"`
|
||||
ProtoMajor int `expr:"protoMajor"`
|
||||
ProtoMinor int `expr:"protoMinor"`
|
||||
ProtoMajor int `expr:"proto_major"`
|
||||
ProtoMinor int `expr:"proto_minor"`
|
||||
Header map[string][]string `expr:"header"`
|
||||
ContentLength int64 `expr:"contentLength"`
|
||||
TransferEncoding []string `expr:"transferEncoding"`
|
||||
ContentLength int64 `expr:"content_length"`
|
||||
TransferEncoding []string `expr:"transfer_encoding"`
|
||||
Uncompressed bool `expr:"uncompressed"`
|
||||
Trailer map[string][]string `expr:"trailer"`
|
||||
}
|
||||
|
||||
func (l *Layer) applyResponseRules(r *http.Response, options *LayerOptions) error {
|
||||
func (l *Layer) applyResponseRules(ctx context.Context, r *http.Response, layerRevision int, options *LayerOptions) error {
|
||||
rules := options.Rules.Response
|
||||
if len(rules) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
engine, err := l.getResponseRuleEngine(r, options)
|
||||
engine, err := l.getResponseRuleEngine(ctx, layerRevision, options)
|
||||
if err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
env := &ResponseEnv{
|
||||
Request: RequestInfo{
|
||||
originalURL, err := director.OriginalURL(ctx)
|
||||
if err != nil {
|
||||
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,
|
||||
URL: URLEnv{
|
||||
URL: URLVar{
|
||||
Scheme: r.Request.URL.Scheme,
|
||||
Opaque: r.Request.URL.Opaque,
|
||||
User: UserInfoEnv{
|
||||
User: UserVar{
|
||||
Username: r.Request.URL.User.Username(),
|
||||
Password: func() string {
|
||||
passwd, _ := r.Request.URL.User.Password()
|
||||
@ -170,7 +217,7 @@ func (l *Layer) applyResponseRules(r *http.Response, options *LayerOptions) erro
|
||||
RemoteAddr: r.Request.RemoteAddr,
|
||||
RequestURI: r.Request.RequestURI,
|
||||
},
|
||||
Response: ResponseInfo{
|
||||
Response: ResponseVar{
|
||||
Proto: r.Proto,
|
||||
ProtoMajor: r.ProtoMajor,
|
||||
ProtoMinor: r.ProtoMinor,
|
||||
@ -183,18 +230,17 @@ func (l *Layer) applyResponseRules(r *http.Response, options *LayerOptions) erro
|
||||
},
|
||||
}
|
||||
|
||||
if _, err := engine.Apply(env); err != nil {
|
||||
ctx = ruleHTTP.WithResponse(ctx, r)
|
||||
|
||||
if _, err := engine.Apply(ctx, vars); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *Layer) getResponseRuleEngine(r *http.Response, options *LayerOptions) (*rule.Engine[*ResponseEnv], error) {
|
||||
engine, err := rule.NewEngine[*ResponseEnv](
|
||||
rule.WithRules(options.Rules.Response...),
|
||||
ruleHTTP.WithResponseFuncs(r),
|
||||
)
|
||||
func (l *Layer) getResponseRuleEngine(ctx context.Context, layerRevision int, options *LayerOptions) (*rule.Engine[*ResponseVars], error) {
|
||||
engine, err := l.responseRuleEngine.Get(ctx, layerRevision, options)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
51
internal/proxy/director/layer/util/revisioned_rule_engine.go
Normal file
51
internal/proxy/director/layer/util/revisioned_rule_engine.go
Normal file
@ -0,0 +1,51 @@
|
||||
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,
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user