mirror of
https://github.com/hanzoai/ingress.git
synced 2026-10-26 02:47:15 +00:00
OWASP Coraza with the Core Rule Set v4 embedded, declared as `waf` on every provider: file, labels and the Middleware CR. Rules load Core Rule Set, then files, then inline directives. The engine state is written after every rule. The Core Rule Set's own recommended configuration opens with SecRuleEngine DetectionOnly, so a state written first is overridden by the rules it governs and the firewall loads all of CRS and refuses nothing; detectionOnly on the middleware is the one place the mode is decided. geoBlock rode along: it was declared in dynamic config only, absent from MiddlewareSpec, both deep copies and the CRD schema, so a Middleware CR carrying it produced a middleware without it. The +groupName marker now matches the registered group, which is what makes controller-gen's output land where code-gen.sh reads it; the stale traefik.io_*.yaml dumps go. CI asks for the arm64 runner git.hanzo.ai actually has and builds both platforms.
476 lines
14 KiB
Go
476 lines
14 KiB
Go
package middleware
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"reflect"
|
|
|
|
"github.com/containous/alice"
|
|
"github.com/hanzoai/ingress/pkg/config/runtime"
|
|
"github.com/hanzoai/ingress/pkg/middlewares/addprefix"
|
|
"github.com/hanzoai/ingress/pkg/middlewares/auth"
|
|
"github.com/hanzoai/ingress/pkg/middlewares/buffering"
|
|
"github.com/hanzoai/ingress/pkg/middlewares/chain"
|
|
"github.com/hanzoai/ingress/pkg/middlewares/circuitbreaker"
|
|
"github.com/hanzoai/ingress/pkg/middlewares/compress"
|
|
"github.com/hanzoai/ingress/pkg/middlewares/contenttype"
|
|
"github.com/hanzoai/ingress/pkg/middlewares/customerrors"
|
|
"github.com/hanzoai/ingress/pkg/middlewares/encodedcharacters"
|
|
"github.com/hanzoai/ingress/pkg/middlewares/gatewayapi/headermodifier"
|
|
gapiredirect "github.com/hanzoai/ingress/pkg/middlewares/gatewayapi/redirect"
|
|
"github.com/hanzoai/ingress/pkg/middlewares/gatewayapi/urlrewrite"
|
|
"github.com/hanzoai/ingress/pkg/middlewares/geoblock"
|
|
"github.com/hanzoai/ingress/pkg/middlewares/grpcweb"
|
|
"github.com/hanzoai/ingress/pkg/middlewares/headers"
|
|
"github.com/hanzoai/ingress/pkg/middlewares/inflightreq"
|
|
"github.com/hanzoai/ingress/pkg/middlewares/ingressnginx/authtlspasscertificatetoupstream"
|
|
"github.com/hanzoai/ingress/pkg/middlewares/ipallowlist"
|
|
"github.com/hanzoai/ingress/pkg/middlewares/ipwhitelist"
|
|
"github.com/hanzoai/ingress/pkg/middlewares/observability"
|
|
"github.com/hanzoai/ingress/pkg/middlewares/passtlsclientcert"
|
|
"github.com/hanzoai/ingress/pkg/middlewares/ratelimiter"
|
|
"github.com/hanzoai/ingress/pkg/middlewares/redirect"
|
|
"github.com/hanzoai/ingress/pkg/middlewares/replacepath"
|
|
"github.com/hanzoai/ingress/pkg/middlewares/replacepathregex"
|
|
"github.com/hanzoai/ingress/pkg/middlewares/retry"
|
|
"github.com/hanzoai/ingress/pkg/middlewares/staticfiles"
|
|
"github.com/hanzoai/ingress/pkg/middlewares/stripprefix"
|
|
"github.com/hanzoai/ingress/pkg/middlewares/stripprefixregex"
|
|
"github.com/hanzoai/ingress/pkg/middlewares/waf"
|
|
"github.com/hanzoai/ingress/pkg/server/provider"
|
|
"github.com/hanzoai/ingress/pkg/server/recursion"
|
|
"github.com/rs/zerolog/log"
|
|
)
|
|
|
|
// Builder the middleware builder.
|
|
type Builder struct {
|
|
configs map[string]*runtime.MiddlewareInfo
|
|
pluginBuilder PluginsBuilder
|
|
serviceBuilder serviceBuilder
|
|
}
|
|
|
|
type serviceBuilder interface {
|
|
BuildHTTP(ctx context.Context, serviceName string) (http.Handler, error)
|
|
}
|
|
|
|
// NewBuilder creates a new Builder.
|
|
func NewBuilder(configs map[string]*runtime.MiddlewareInfo, serviceBuilder serviceBuilder, pluginBuilder PluginsBuilder) *Builder {
|
|
return &Builder{configs: configs, serviceBuilder: serviceBuilder, pluginBuilder: pluginBuilder}
|
|
}
|
|
|
|
// BuildMiddlewareChain creates a middleware chain.
|
|
func (b *Builder) BuildMiddlewareChain(ctx context.Context, middlewares []string) *alice.Chain {
|
|
chain := alice.New()
|
|
for _, name := range middlewares {
|
|
middlewareName := provider.GetQualifiedName(ctx, name)
|
|
|
|
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
|
|
constructorContext := provider.AddInContext(ctx, middlewareName)
|
|
if midInf, ok := b.configs[middlewareName]; !ok || midInf.Middleware == nil {
|
|
return nil, fmt.Errorf("middleware %q does not exist", middlewareName)
|
|
}
|
|
|
|
var err error
|
|
if constructorContext, err = recursion.CheckRecursion(constructorContext, "middleware", middlewareName); err != nil {
|
|
b.configs[middlewareName].AddError(err, true)
|
|
return nil, err
|
|
}
|
|
|
|
constructor, err := b.buildConstructor(constructorContext, middlewareName)
|
|
if err != nil {
|
|
b.configs[middlewareName].AddError(err, true)
|
|
return nil, err
|
|
}
|
|
|
|
handler, err := constructor(next)
|
|
if err != nil {
|
|
b.configs[middlewareName].AddError(err, true)
|
|
return nil, err
|
|
}
|
|
|
|
return handler, nil
|
|
})
|
|
}
|
|
return &chain
|
|
}
|
|
|
|
// it is the responsibility of the caller to make sure that b.configs[middlewareName].Middleware exists.
|
|
func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) (alice.Constructor, error) {
|
|
config := b.configs[middlewareName]
|
|
if config == nil || config.Middleware == nil {
|
|
return nil, fmt.Errorf("invalid middleware %q configuration", middlewareName)
|
|
}
|
|
|
|
var middleware alice.Constructor
|
|
badConf := errors.New("cannot create middleware: multi-types middleware not supported, consider declaring two different pieces of middleware instead")
|
|
|
|
// AddPrefix
|
|
if config.AddPrefix != nil {
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return addprefix.New(ctx, next, *config.AddPrefix, middlewareName)
|
|
}
|
|
}
|
|
|
|
// BasicAuth
|
|
if config.BasicAuth != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return auth.NewBasic(ctx, next, *config.BasicAuth, middlewareName)
|
|
}
|
|
}
|
|
|
|
// Buffering
|
|
if config.Buffering != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return buffering.New(ctx, next, *config.Buffering, middlewareName)
|
|
}
|
|
}
|
|
|
|
// Chain
|
|
if config.Chain != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
|
|
var qualifiedNames []string
|
|
for _, name := range config.Chain.Middlewares {
|
|
qualifiedNames = append(qualifiedNames, provider.GetQualifiedName(ctx, name))
|
|
}
|
|
config.Chain.Middlewares = qualifiedNames
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return chain.New(ctx, next, *config.Chain, b, middlewareName)
|
|
}
|
|
}
|
|
|
|
// CircuitBreaker
|
|
if config.CircuitBreaker != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return circuitbreaker.New(ctx, next, *config.CircuitBreaker, middlewareName)
|
|
}
|
|
}
|
|
|
|
// Compress
|
|
if config.Compress != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return compress.New(ctx, next, *config.Compress, middlewareName)
|
|
}
|
|
}
|
|
|
|
// ContentType
|
|
if config.ContentType != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return contenttype.New(ctx, next, *config.ContentType, middlewareName)
|
|
}
|
|
}
|
|
|
|
// EncodedCharacters
|
|
if config.EncodedCharacters != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return encodedcharacters.NewEncodedCharacters(ctx, next, *config.EncodedCharacters, middlewareName), nil
|
|
}
|
|
}
|
|
|
|
// CustomErrors
|
|
if config.Errors != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return customerrors.New(ctx, next, *config.Errors, b.serviceBuilder, middlewareName)
|
|
}
|
|
}
|
|
|
|
// DigestAuth
|
|
if config.DigestAuth != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return auth.NewDigest(ctx, next, *config.DigestAuth, middlewareName)
|
|
}
|
|
}
|
|
|
|
// ForwardAuth
|
|
if config.ForwardAuth != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return auth.NewForward(ctx, next, *config.ForwardAuth, middlewareName)
|
|
}
|
|
}
|
|
|
|
// GrpcWeb
|
|
if config.GrpcWeb != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return grpcweb.New(ctx, next, *config.GrpcWeb, middlewareName), nil
|
|
}
|
|
}
|
|
|
|
// Headers
|
|
if config.Headers != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return headers.New(ctx, next, *config.Headers, middlewareName)
|
|
}
|
|
}
|
|
|
|
// IPWhiteList
|
|
if config.IPWhiteList != nil {
|
|
qualifiedName := provider.GetQualifiedName(ctx, middlewareName)
|
|
log.Warn().Msgf("Middleware %q of type IPWhiteList is deprecated, please use IPAllowList instead.", qualifiedName)
|
|
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return ipwhitelist.New(ctx, next, *config.IPWhiteList, middlewareName)
|
|
}
|
|
}
|
|
|
|
// IPAllowList
|
|
if config.IPAllowList != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return ipallowlist.New(ctx, next, *config.IPAllowList, middlewareName)
|
|
}
|
|
}
|
|
|
|
// GeoBlock — country-level header injection + access control.
|
|
// Single source of truth across all brands (Hanzo / Lux / Zoo).
|
|
// See pkg/middlewares/geoblock for the resolver chain
|
|
// (Cloudflare/X-Forwarded-Country upstream → MaxMind MMDB fallback)
|
|
// and rejection semantics (RFC 7725 451 by default).
|
|
if config.GeoBlock != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return geoblock.New(ctx, next, *config.GeoBlock, middlewareName)
|
|
}
|
|
}
|
|
|
|
// WAF (OWASP Coraza, SecLang / OWASP Core Rule Set).
|
|
// See pkg/middlewares/waf: a configuration that loads no rule is refused
|
|
// here rather than serving traffic through an empty ruleset.
|
|
if config.WAF != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return waf.New(ctx, next, *config.WAF, middlewareName)
|
|
}
|
|
}
|
|
|
|
// InFlightReq
|
|
if config.InFlightReq != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return inflightreq.New(ctx, next, *config.InFlightReq, middlewareName)
|
|
}
|
|
}
|
|
|
|
// PassTLSClientCert
|
|
if config.PassTLSClientCert != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return passtlsclientcert.New(ctx, next, *config.PassTLSClientCert, middlewareName)
|
|
}
|
|
}
|
|
|
|
// AuthTLSPassCertificateToUpstream
|
|
if config.AuthTLSPassCertificateToUpstream != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return authtlspasscertificatetoupstream.NewAuthTLSPassCertificateToUpstream(ctx, next, *config.AuthTLSPassCertificateToUpstream, middlewareName)
|
|
}
|
|
}
|
|
|
|
// RateLimit
|
|
if config.RateLimit != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return ratelimiter.New(ctx, next, *config.RateLimit, middlewareName)
|
|
}
|
|
}
|
|
|
|
// RedirectRegex
|
|
if config.RedirectRegex != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return redirect.NewRedirectRegex(ctx, next, *config.RedirectRegex, middlewareName)
|
|
}
|
|
}
|
|
|
|
// RedirectScheme
|
|
if config.RedirectScheme != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return redirect.NewRedirectScheme(ctx, next, *config.RedirectScheme, middlewareName)
|
|
}
|
|
}
|
|
|
|
// ReplacePath
|
|
if config.ReplacePath != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return replacepath.New(ctx, next, *config.ReplacePath, middlewareName)
|
|
}
|
|
}
|
|
|
|
// ReplacePathRegex
|
|
if config.ReplacePathRegex != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return replacepathregex.New(ctx, next, *config.ReplacePathRegex, middlewareName)
|
|
}
|
|
}
|
|
|
|
// Retry
|
|
if config.Retry != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
// TODO missing metrics / accessLog
|
|
return retry.New(ctx, next, *config.Retry, retry.Listeners{}, middlewareName)
|
|
}
|
|
}
|
|
|
|
// StripPrefix
|
|
if config.StripPrefix != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return stripprefix.New(ctx, next, *config.StripPrefix, middlewareName)
|
|
}
|
|
}
|
|
|
|
// StripPrefixRegex
|
|
if config.StripPrefixRegex != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return stripprefixregex.New(ctx, next, *config.StripPrefixRegex, middlewareName)
|
|
}
|
|
}
|
|
|
|
// StaticFiles
|
|
if config.StaticFiles != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return staticfiles.New(ctx, next, *config.StaticFiles, middlewareName)
|
|
}
|
|
}
|
|
|
|
// Plugin
|
|
if config.Plugin != nil && !reflect.ValueOf(b.pluginBuilder).IsNil() { // Using "reflect" because "b.pluginBuilder" is an interface.
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
|
|
pluginType, rawPluginConfig, err := findPluginConfig(config.Plugin)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("plugin: %w", err)
|
|
}
|
|
|
|
plug, err := b.pluginBuilder.Build(pluginType, rawPluginConfig, middlewareName)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("plugin: %w", err)
|
|
}
|
|
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return newTraceablePlugin(ctx, middlewareName, plug, next)
|
|
}
|
|
}
|
|
|
|
// Gateway API HTTPRoute filters middlewares.
|
|
if config.RequestHeaderModifier != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return headermodifier.NewRequestHeaderModifier(ctx, next, *config.RequestHeaderModifier, middlewareName), nil
|
|
}
|
|
}
|
|
|
|
if config.ResponseHeaderModifier != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return headermodifier.NewResponseHeaderModifier(ctx, next, *config.ResponseHeaderModifier, middlewareName), nil
|
|
}
|
|
}
|
|
|
|
if config.RequestRedirect != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return gapiredirect.NewRequestRedirect(ctx, next, *config.RequestRedirect, middlewareName)
|
|
}
|
|
}
|
|
|
|
if config.URLRewrite != nil {
|
|
if middleware != nil {
|
|
return nil, badConf
|
|
}
|
|
middleware = func(next http.Handler) (http.Handler, error) {
|
|
return urlrewrite.NewURLRewrite(ctx, next, *config.URLRewrite, middlewareName), nil
|
|
}
|
|
}
|
|
|
|
if middleware == nil {
|
|
return nil, fmt.Errorf("invalid middleware %q configuration: invalid middleware type or middleware does not exist", middlewareName)
|
|
}
|
|
|
|
return observability.WrapMiddleware(ctx, middleware), nil
|
|
}
|