aboutsummaryrefslogtreecommitdiffhomepage
path: root/modules/caddyhttp
diff options
context:
space:
mode:
Diffstat (limited to 'modules/caddyhttp')
-rw-r--r--modules/caddyhttp/app.go60
-rw-r--r--modules/caddyhttp/caddyhttp.go16
-rw-r--r--modules/caddyhttp/celmatcher.go135
-rw-r--r--modules/caddyhttp/celmatcher_test.go8
-rw-r--r--modules/caddyhttp/encode/caddyfile.go36
-rw-r--r--modules/caddyhttp/encode/encode.go30
-rw-r--r--modules/caddyhttp/encode/encode_test.go2
-rw-r--r--modules/caddyhttp/fileserver/browse.go13
-rw-r--r--modules/caddyhttp/fileserver/caddyfile.go29
-rw-r--r--modules/caddyhttp/fileserver/command.go4
-rw-r--r--modules/caddyhttp/fileserver/matcher.go86
-rw-r--r--modules/caddyhttp/fileserver/matcher_test.go37
-rw-r--r--modules/caddyhttp/fileserver/staticfiles.go6
-rw-r--r--modules/caddyhttp/fileserver/testdata/large.txt3
-rw-r--r--modules/caddyhttp/headers/headers.go4
-rw-r--r--modules/caddyhttp/ip_matchers.go62
-rw-r--r--modules/caddyhttp/matchers.go219
-rw-r--r--modules/caddyhttp/matchers_test.go53
-rw-r--r--modules/caddyhttp/metrics.go5
-rw-r--r--modules/caddyhttp/replacer.go11
-rw-r--r--modules/caddyhttp/requestbody/requestbody.go4
-rw-r--r--modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go72
-rw-r--r--modules/caddyhttp/reverseproxy/fastcgi/client.go11
-rw-r--r--modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go2
-rw-r--r--modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go67
-rw-r--r--modules/caddyhttp/reverseproxy/healthchecks.go2
-rw-r--r--modules/caddyhttp/reverseproxy/httptransport.go4
-rw-r--r--modules/caddyhttp/reverseproxy/reverseproxy.go85
-rw-r--r--modules/caddyhttp/reverseproxy/selectionpolicies.go19
-rw-r--r--modules/caddyhttp/reverseproxy/selectionpolicies_test.go52
-rw-r--r--modules/caddyhttp/reverseproxy/streaming.go84
-rw-r--r--modules/caddyhttp/rewrite/caddyfile.go3
-rw-r--r--modules/caddyhttp/rewrite/rewrite.go3
-rw-r--r--modules/caddyhttp/rewrite/rewrite_test.go5
-rw-r--r--modules/caddyhttp/routes.go97
-rw-r--r--modules/caddyhttp/server.go26
-rw-r--r--modules/caddyhttp/server_test.go9
-rw-r--r--modules/caddyhttp/tracing/tracer.go6
-rw-r--r--modules/caddyhttp/vars.go38
39 files changed, 1023 insertions, 385 deletions
diff --git a/modules/caddyhttp/app.go b/modules/caddyhttp/app.go
index 7dc2bee72..850d3aa8f 100644
--- a/modules/caddyhttp/app.go
+++ b/modules/caddyhttp/app.go
@@ -15,6 +15,7 @@
package caddyhttp
import (
+ "cmp"
"context"
"crypto/tls"
"fmt"
@@ -142,6 +143,10 @@ type App struct {
// affect functionality.
Servers map[string]*Server `json:"servers,omitempty"`
+ // If set, metrics observations will be enabled.
+ // This setting is EXPERIMENTAL and subject to change.
+ Metrics *Metrics `json:"metrics,omitempty"`
+
ctx caddy.Context
logger *zap.Logger
tlsApp *caddytls.TLS
@@ -184,6 +189,10 @@ func (app *App) Provision(ctx caddy.Context) error {
return err
}
+ if app.Metrics != nil {
+ app.Metrics.init = sync.Once{}
+ app.Metrics.httpMetrics = &httpMetrics{}
+ }
// prepare each server
oldContext := ctx.Context
for srvName, srv := range app.Servers {
@@ -196,6 +205,15 @@ func (app *App) Provision(ctx caddy.Context) error {
srv.errorLogger = app.logger.Named("log.error")
srv.shutdownAtMu = new(sync.RWMutex)
+ if srv.Metrics != nil {
+ srv.logger.Warn("per-server 'metrics' is deprecated; use 'metrics' in the root 'http' app instead")
+ app.Metrics = cmp.Or[*Metrics](app.Metrics, &Metrics{
+ init: sync.Once{},
+ httpMetrics: &httpMetrics{},
+ })
+ app.Metrics.PerHost = app.Metrics.PerHost || srv.Metrics.PerHost
+ }
+
// only enable access logs if configured
if srv.Logs != nil {
srv.accessLogger = app.logger.Named("log.access")
@@ -342,16 +360,11 @@ func (app *App) Provision(ctx caddy.Context) error {
srv.listenerWrappers = append([]caddy.ListenerWrapper{new(tlsPlaceholderWrapper)}, srv.listenerWrappers...)
}
}
-
// pre-compile the primary handler chain, and be sure to wrap it in our
// route handler so that important security checks are done, etc.
primaryRoute := emptyHandler
if srv.Routes != nil {
- if srv.Metrics != nil {
- srv.Metrics.init = sync.Once{}
- srv.Metrics.httpMetrics = &httpMetrics{}
- }
- err := srv.Routes.ProvisionHandlers(ctx, srv.Metrics)
+ err := srv.Routes.ProvisionHandlers(ctx, app.Metrics)
if err != nil {
return fmt.Errorf("server %s: setting up route handlers: %v", srvName, err)
}
@@ -370,7 +383,7 @@ func (app *App) Provision(ctx caddy.Context) error {
// provision the named routes (they get compiled at runtime)
for name, route := range srv.NamedRoutes {
- err := route.Provision(ctx, srv.Metrics)
+ err := route.Provision(ctx, app.Metrics)
if err != nil {
return fmt.Errorf("server %s: setting up named route '%s' handlers: %v", name, srvName, err)
}
@@ -388,6 +401,9 @@ func (app *App) Provision(ctx caddy.Context) error {
if srv.IdleTimeout == 0 {
srv.IdleTimeout = defaultIdleTimeout
}
+ if srv.ReadHeaderTimeout == 0 {
+ srv.ReadHeaderTimeout = defaultReadHeaderTimeout // see #6663
+ }
}
ctx.Context = oldContext
return nil
@@ -689,16 +705,7 @@ func (app *App) Stop() error {
return
}
- // First close h3server then close listeners unlike stdlib for several reasons:
- // 1, udp has only a single socket, once closed, no more data can be read and
- // written. In contrast, closing tcp listeners won't affect established connections.
- // This have something to do with graceful shutdown when upstream implements it.
- // 2, h3server will only close listeners it's registered (quic listeners). Closing
- // listener first and these listeners maybe unregistered thus won't be closed. caddy
- // distinguishes quic-listener and underlying datagram sockets.
-
- // TODO: CloseGracefully, once implemented upstream (see https://github.com/quic-go/quic-go/issues/2103)
- if err := server.h3server.Close(); err != nil {
+ if err := server.h3server.Shutdown(ctx); err != nil {
app.logger.Error("HTTP/3 server shutdown",
zap.Error(err),
zap.Strings("addresses", server.Listen))
@@ -766,11 +773,20 @@ func (app *App) httpsPort() int {
return app.HTTPSPort
}
-// defaultIdleTimeout is the default HTTP server timeout
-// for closing idle connections; useful to avoid resource
-// exhaustion behind hungry CDNs, for example (we've had
-// several complaints without this).
-const defaultIdleTimeout = caddy.Duration(5 * time.Minute)
+const (
+ // defaultIdleTimeout is the default HTTP server timeout
+ // for closing idle connections; useful to avoid resource
+ // exhaustion behind hungry CDNs, for example (we've had
+ // several complaints without this).
+ defaultIdleTimeout = caddy.Duration(5 * time.Minute)
+
+ // defaultReadHeaderTimeout is the default timeout for
+ // reading HTTP headers from clients. Headers are generally
+ // small, often less than 1 KB, so it shouldn't take a
+ // long time even on legitimately slow connections or
+ // busy servers to read it.
+ defaultReadHeaderTimeout = caddy.Duration(time.Minute)
+)
// Interface guards
var (
diff --git a/modules/caddyhttp/caddyhttp.go b/modules/caddyhttp/caddyhttp.go
index e1e71f4a0..aacafc92e 100644
--- a/modules/caddyhttp/caddyhttp.go
+++ b/modules/caddyhttp/caddyhttp.go
@@ -36,10 +36,26 @@ func init() {
// RequestMatcher is a type that can match to a request.
// A route matcher MUST NOT modify the request, with the
// only exception being its context.
+//
+// Deprecated: Matchers should now implement RequestMatcherWithError.
+// You may remove any interface guards for RequestMatcher
+// but keep your Match() methods for backwards compatibility.
type RequestMatcher interface {
Match(*http.Request) bool
}
+// RequestMatcherWithError is like RequestMatcher but can return an error.
+// An error during matching will abort the request middleware chain and
+// invoke the error middleware chain.
+//
+// This will eventually replace RequestMatcher. Matcher modules
+// should implement both interfaces, and once all modules have
+// been updated to use RequestMatcherWithError, the RequestMatcher
+// interface may eventually be dropped.
+type RequestMatcherWithError interface {
+ MatchWithError(*http.Request) (bool, error)
+}
+
// Handler is like http.Handler except ServeHTTP may return an error.
//
// If any handler encounters an error, it should be returned for proper
diff --git a/modules/caddyhttp/celmatcher.go b/modules/caddyhttp/celmatcher.go
index 2a03ebba7..3d118ea79 100644
--- a/modules/caddyhttp/celmatcher.go
+++ b/modules/caddyhttp/celmatcher.go
@@ -202,17 +202,25 @@ func (m *MatchExpression) Provision(ctx caddy.Context) error {
// Match returns true if r matches m.
func (m MatchExpression) Match(r *http.Request) bool {
+ match, err := m.MatchWithError(r)
+ if err != nil {
+ SetVar(r.Context(), MatcherErrorVarKey, err)
+ }
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+func (m MatchExpression) MatchWithError(r *http.Request) (bool, error) {
celReq := celHTTPRequest{r}
out, _, err := m.prg.Eval(celReq)
if err != nil {
m.log.Error("evaluating expression", zap.Error(err))
- SetVar(r.Context(), MatcherErrorVarKey, err)
- return false
+ return false, err
}
if outBool, ok := out.Value().(bool); ok {
- return outBool
+ return outBool, nil
}
- return false
+ return false, nil
}
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
@@ -380,7 +388,7 @@ type CELLibraryProducer interface {
// limited set of function signatures. For strong type validation you may need
// to provide a custom macro which does a more detailed analysis of the CEL
// literal provided to the macro as an argument.
-func CELMatcherImpl(macroName, funcName string, matcherDataTypes []*cel.Type, fac CELMatcherFactory) (cel.Library, error) {
+func CELMatcherImpl(macroName, funcName string, matcherDataTypes []*cel.Type, fac any) (cel.Library, error) {
requestType := cel.ObjectType("http.Request")
var macro parser.Macro
switch len(matcherDataTypes) {
@@ -424,7 +432,11 @@ func CELMatcherImpl(macroName, funcName string, matcherDataTypes []*cel.Type, fa
}
// CELMatcherFactory converts a constant CEL value into a RequestMatcher.
-type CELMatcherFactory func(data ref.Val) (RequestMatcher, error)
+// Deprecated: Use CELMatcherWithErrorFactory instead.
+type CELMatcherFactory = func(data ref.Val) (RequestMatcher, error)
+
+// CELMatcherWithErrorFactory converts a constant CEL value into a RequestMatcherWithError.
+type CELMatcherWithErrorFactory = func(data ref.Val) (RequestMatcherWithError, error)
// matcherCELLibrary is a simplistic configurable cel.Library implementation.
type matcherCELLibrary struct {
@@ -452,7 +464,7 @@ func (lib *matcherCELLibrary) ProgramOptions() []cel.ProgramOption {
// that takes a single argument, and optimizes the implementation to precompile
// the matcher and return a function that references the precompiled and
// provisioned matcher.
-func CELMatcherDecorator(funcName string, fac CELMatcherFactory) interpreter.InterpretableDecorator {
+func CELMatcherDecorator(funcName string, fac any) interpreter.InterpretableDecorator {
return func(i interpreter.Interpretable) (interpreter.Interpretable, error) {
call, ok := i.(interpreter.InterpretableCall)
if !ok {
@@ -481,35 +493,92 @@ func CELMatcherDecorator(funcName string, fac CELMatcherFactory) interpreter.Int
// and matcher provisioning should be handled at dynamically.
return i, nil
}
- matcher, err := fac(matcherData.Value())
- if err != nil {
- return nil, err
+
+ if factory, ok := fac.(CELMatcherWithErrorFactory); ok {
+ matcher, err := factory(matcherData.Value())
+ if err != nil {
+ return nil, err
+ }
+ return interpreter.NewCall(
+ i.ID(), funcName, funcName+"_opt",
+ []interpreter.Interpretable{reqAttr},
+ func(args ...ref.Val) ref.Val {
+ // The request value, guaranteed to be of type celHTTPRequest
+ celReq := args[0]
+ // If needed this call could be changed to convert the value
+ // to a *http.Request using CEL's ConvertToNative method.
+ httpReq := celReq.Value().(celHTTPRequest)
+ match, err := matcher.MatchWithError(httpReq.Request)
+ if err != nil {
+ return types.WrapErr(err)
+ }
+ return types.Bool(match)
+ },
+ ), nil
}
- return interpreter.NewCall(
- i.ID(), funcName, funcName+"_opt",
- []interpreter.Interpretable{reqAttr},
- func(args ...ref.Val) ref.Val {
- // The request value, guaranteed to be of type celHTTPRequest
- celReq := args[0]
- // If needed this call could be changed to convert the value
- // to a *http.Request using CEL's ConvertToNative method.
- httpReq := celReq.Value().(celHTTPRequest)
- return types.Bool(matcher.Match(httpReq.Request))
- },
- ), nil
+
+ if factory, ok := fac.(CELMatcherFactory); ok {
+ matcher, err := factory(matcherData.Value())
+ if err != nil {
+ return nil, err
+ }
+ return interpreter.NewCall(
+ i.ID(), funcName, funcName+"_opt",
+ []interpreter.Interpretable{reqAttr},
+ func(args ...ref.Val) ref.Val {
+ // The request value, guaranteed to be of type celHTTPRequest
+ celReq := args[0]
+ // If needed this call could be changed to convert the value
+ // to a *http.Request using CEL's ConvertToNative method.
+ httpReq := celReq.Value().(celHTTPRequest)
+ if m, ok := matcher.(RequestMatcherWithError); ok {
+ match, err := m.MatchWithError(httpReq.Request)
+ if err != nil {
+ return types.WrapErr(err)
+ }
+ return types.Bool(match)
+ }
+ return types.Bool(matcher.Match(httpReq.Request))
+ },
+ ), nil
+ }
+
+ return nil, fmt.Errorf("invalid matcher factory, must be CELMatcherFactory or CELMatcherWithErrorFactory: %T", fac)
}
}
// CELMatcherRuntimeFunction creates a function binding for when the input to the matcher
// is dynamically resolved rather than a set of static constant values.
-func CELMatcherRuntimeFunction(funcName string, fac CELMatcherFactory) functions.BinaryOp {
+func CELMatcherRuntimeFunction(funcName string, fac any) functions.BinaryOp {
return func(celReq, matcherData ref.Val) ref.Val {
- matcher, err := fac(matcherData)
- if err != nil {
- return types.WrapErr(err)
+ if factory, ok := fac.(CELMatcherWithErrorFactory); ok {
+ matcher, err := factory(matcherData)
+ if err != nil {
+ return types.WrapErr(err)
+ }
+ httpReq := celReq.Value().(celHTTPRequest)
+ match, err := matcher.MatchWithError(httpReq.Request)
+ if err != nil {
+ return types.WrapErr(err)
+ }
+ return types.Bool(match)
+ }
+ if factory, ok := fac.(CELMatcherFactory); ok {
+ matcher, err := factory(matcherData)
+ if err != nil {
+ return types.WrapErr(err)
+ }
+ httpReq := celReq.Value().(celHTTPRequest)
+ if m, ok := matcher.(RequestMatcherWithError); ok {
+ match, err := m.MatchWithError(httpReq.Request)
+ if err != nil {
+ return types.WrapErr(err)
+ }
+ return types.Bool(match)
+ }
+ return types.Bool(matcher.Match(httpReq.Request))
}
- httpReq := celReq.Value().(celHTTPRequest)
- return types.Bool(matcher.Match(httpReq.Request))
+ return types.NewErr("CELMatcherRuntimeFunction invalid matcher factory: %T", fac)
}
}
@@ -733,9 +802,9 @@ const MatcherNameCtxKey = "matcher_name"
// Interface guards
var (
- _ caddy.Provisioner = (*MatchExpression)(nil)
- _ RequestMatcher = (*MatchExpression)(nil)
- _ caddyfile.Unmarshaler = (*MatchExpression)(nil)
- _ json.Marshaler = (*MatchExpression)(nil)
- _ json.Unmarshaler = (*MatchExpression)(nil)
+ _ caddy.Provisioner = (*MatchExpression)(nil)
+ _ RequestMatcherWithError = (*MatchExpression)(nil)
+ _ caddyfile.Unmarshaler = (*MatchExpression)(nil)
+ _ json.Marshaler = (*MatchExpression)(nil)
+ _ json.Unmarshaler = (*MatchExpression)(nil)
)
diff --git a/modules/caddyhttp/celmatcher_test.go b/modules/caddyhttp/celmatcher_test.go
index 26491b7ca..a7e91529c 100644
--- a/modules/caddyhttp/celmatcher_test.go
+++ b/modules/caddyhttp/celmatcher_test.go
@@ -489,7 +489,11 @@ func TestMatchExpressionMatch(t *testing.T) {
}
}
- if tc.expression.Match(req) != tc.wantResult {
+ matches, err := tc.expression.MatchWithError(req)
+ if err != nil {
+ t.Errorf("MatchExpression.Match() error = %v", err)
+ }
+ if matches != tc.wantResult {
t.Errorf("MatchExpression.Match() expected to return '%t', for expression : '%s'", tc.wantResult, tc.expression.Expr)
}
})
@@ -532,7 +536,7 @@ func BenchmarkMatchExpressionMatch(b *testing.B) {
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
- tc.expression.Match(req)
+ tc.expression.MatchWithError(req)
}
})
}
diff --git a/modules/caddyhttp/encode/caddyfile.go b/modules/caddyhttp/encode/caddyfile.go
index e8ea4b807..8b8657080 100644
--- a/modules/caddyhttp/encode/caddyfile.go
+++ b/modules/caddyhttp/encode/caddyfile.go
@@ -57,21 +57,7 @@ func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
d.Next() // consume directive name
prefer := []string{}
- for _, arg := range d.RemainingArgs() {
- mod, err := caddy.GetModule("http.encoders." + arg)
- if err != nil {
- return d.Errf("finding encoder module '%s': %v", mod, err)
- }
- encoding, ok := mod.New().(Encoding)
- if !ok {
- return d.Errf("module %s is not an HTTP encoding", mod)
- }
- if enc.EncodingsRaw == nil {
- enc.EncodingsRaw = make(caddy.ModuleMap)
- }
- enc.EncodingsRaw[arg] = caddyconfig.JSON(encoding, nil)
- prefer = append(prefer, arg)
- }
+ remainingArgs := d.RemainingArgs()
responseMatchers := make(map[string]caddyhttp.ResponseMatcher)
for d.NextBlock(0) {
@@ -111,6 +97,26 @@ func (enc *Encode) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
}
}
+ if len(prefer) == 0 && len(remainingArgs) == 0 {
+ remainingArgs = []string{"zstd", "gzip"}
+ }
+
+ for _, arg := range remainingArgs {
+ mod, err := caddy.GetModule("http.encoders." + arg)
+ if err != nil {
+ return d.Errf("finding encoder module '%s': %v", mod, err)
+ }
+ encoding, ok := mod.New().(Encoding)
+ if !ok {
+ return d.Errf("module %s is not an HTTP encoding", mod)
+ }
+ if enc.EncodingsRaw == nil {
+ enc.EncodingsRaw = make(caddy.ModuleMap)
+ }
+ enc.EncodingsRaw[arg] = caddyconfig.JSON(encoding, nil)
+ prefer = append(prefer, arg)
+ }
+
// use the order in which the encoders were defined.
enc.Prefer = prefer
diff --git a/modules/caddyhttp/encode/encode.go b/modules/caddyhttp/encode/encode.go
index f0d56a90d..597772ccc 100644
--- a/modules/caddyhttp/encode/encode.go
+++ b/modules/caddyhttp/encode/encode.go
@@ -156,7 +156,7 @@ func (enc *Encode) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyh
if _, ok := enc.writerPools[encName]; !ok {
continue // encoding not offered
}
- w = enc.openResponseWriter(encName, w)
+ w = enc.openResponseWriter(encName, w, r.Method == http.MethodConnect)
defer w.(*responseWriter).Close()
// to comply with RFC 9110 section 8.8.3(.3), we modify the Etag when encoding
@@ -201,14 +201,14 @@ func (enc *Encode) addEncoding(e Encoding) error {
// openResponseWriter creates a new response writer that may (or may not)
// encode the response with encodingName. The returned response writer MUST
// be closed after the handler completes.
-func (enc *Encode) openResponseWriter(encodingName string, w http.ResponseWriter) *responseWriter {
+func (enc *Encode) openResponseWriter(encodingName string, w http.ResponseWriter, isConnect bool) *responseWriter {
var rw responseWriter
- return enc.initResponseWriter(&rw, encodingName, w)
+ return enc.initResponseWriter(&rw, encodingName, w, isConnect)
}
// initResponseWriter initializes the responseWriter instance
// allocated in openResponseWriter, enabling mid-stack inlining.
-func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, wrappedRW http.ResponseWriter) *responseWriter {
+func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, wrappedRW http.ResponseWriter, isConnect bool) *responseWriter {
if rww, ok := wrappedRW.(*caddyhttp.ResponseWriterWrapper); ok {
rw.ResponseWriter = rww
} else {
@@ -216,6 +216,7 @@ func (enc *Encode) initResponseWriter(rw *responseWriter, encodingName string, w
}
rw.encodingName = encodingName
rw.config = enc
+ rw.isConnect = isConnect
return rw
}
@@ -230,6 +231,7 @@ type responseWriter struct {
config *Encode
statusCode int
wroteHeader bool
+ isConnect bool
}
// WriteHeader stores the status to write when the time comes
@@ -245,6 +247,14 @@ func (rw *responseWriter) WriteHeader(status int) {
rw.Header().Add("Vary", "Accept-Encoding")
}
+ // write status immediately if status is 2xx and the request is CONNECT
+ // since it means the response is successful.
+ // see: https://github.com/caddyserver/caddy/issues/6733#issuecomment-2525058845
+ if rw.isConnect && 200 <= status && status <= 299 {
+ rw.ResponseWriter.WriteHeader(status)
+ rw.wroteHeader = true
+ }
+
// write status immediately when status code is informational
// see: https://caddy.community/t/disappear-103-early-hints-response-with-encode-enable-caddy-v2-7-6/23081/5
if 100 <= status && status <= 199 {
@@ -260,6 +270,12 @@ func (enc *Encode) Match(rw *responseWriter) bool {
// FlushError is an alternative Flush returning an error. It delays the actual Flush of the underlying
// ResponseWriterWrapper until headers were written.
func (rw *responseWriter) FlushError() error {
+ // WriteHeader wasn't called and is a CONNECT request, treat it as a success.
+ // otherwise, wait until header is written.
+ if rw.isConnect && !rw.wroteHeader && rw.statusCode == 0 {
+ rw.WriteHeader(http.StatusOK)
+ }
+
if !rw.wroteHeader {
// flushing the underlying ResponseWriter will write header and status code,
// but we need to delay that until we can determine if we must encode and
@@ -288,6 +304,12 @@ func (rw *responseWriter) Write(p []byte) (int, error) {
return 0, nil
}
+ // WriteHeader wasn't called and is a CONNECT request, treat it as a success.
+ // otherwise, determine if the response should be compressed.
+ if rw.isConnect && !rw.wroteHeader && rw.statusCode == 0 {
+ rw.WriteHeader(http.StatusOK)
+ }
+
// sniff content-type and determine content-length
if !rw.wroteHeader && rw.config.MinLength > 0 {
var gtMinLength bool
diff --git a/modules/caddyhttp/encode/encode_test.go b/modules/caddyhttp/encode/encode_test.go
index d76945498..83effa58c 100644
--- a/modules/caddyhttp/encode/encode_test.go
+++ b/modules/caddyhttp/encode/encode_test.go
@@ -9,7 +9,7 @@ import (
func BenchmarkOpenResponseWriter(b *testing.B) {
enc := new(Encode)
for n := 0; n < b.N; n++ {
- enc.openResponseWriter("test", nil)
+ enc.openResponseWriter("test", nil, false)
}
}
diff --git a/modules/caddyhttp/fileserver/browse.go b/modules/caddyhttp/fileserver/browse.go
index a19b4e17a..0a623c79e 100644
--- a/modules/caddyhttp/fileserver/browse.go
+++ b/modules/caddyhttp/fileserver/browse.go
@@ -66,8 +66,15 @@ type Browse struct {
// - `sort size` will sort by size in ascending order
// The first option must be `sort_by` and the second option must be `order` (if exists).
SortOptions []string `json:"sort,omitempty"`
+
+ // FileLimit limits the number of up to n DirEntry values in directory order.
+ FileLimit int `json:"file_limit,omitempty"`
}
+const (
+ defaultDirEntryLimit = 10000
+)
+
func (fsrv *FileServer) serveBrowse(fileSystem fs.FS, root, dirPath string, w http.ResponseWriter, r *http.Request, next caddyhttp.Handler) error {
if c := fsrv.logger.Check(zapcore.DebugLevel, "browse enabled; listing directory contents"); c != nil {
c.Write(zap.String("path", dirPath), zap.String("root", root))
@@ -206,7 +213,11 @@ func (fsrv *FileServer) serveBrowse(fileSystem fs.FS, root, dirPath string, w ht
}
func (fsrv *FileServer) loadDirectoryContents(ctx context.Context, fileSystem fs.FS, dir fs.ReadDirFile, root, urlPath string, repl *caddy.Replacer) (*browseTemplateContext, error) {
- files, err := dir.ReadDir(10000) // TODO: this limit should probably be configurable
+ dirLimit := defaultDirEntryLimit
+ if fsrv.Browse.FileLimit != 0 {
+ dirLimit = fsrv.Browse.FileLimit
+ }
+ files, err := dir.ReadDir(dirLimit)
if err != nil && err != io.EOF {
return nil, err
}
diff --git a/modules/caddyhttp/fileserver/caddyfile.go b/modules/caddyhttp/fileserver/caddyfile.go
index 71b7638e4..80a37322b 100644
--- a/modules/caddyhttp/fileserver/caddyfile.go
+++ b/modules/caddyhttp/fileserver/caddyfile.go
@@ -16,6 +16,7 @@ package fileserver
import (
"path/filepath"
+ "strconv"
"strings"
"github.com/caddyserver/caddy/v2"
@@ -78,7 +79,7 @@ func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return d.ArgErr()
}
- for d.NextBlock(0) {
+ for nesting := d.Nesting(); d.NextBlock(nesting); {
switch d.Val() {
case "fs":
if !d.NextArg() {
@@ -129,15 +130,29 @@ func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
return d.Errf("unknown sort option '%s'", dVal)
}
}
+ case "file_limit":
+ fileLimit := d.RemainingArgs()
+ if len(fileLimit) != 1 {
+ return d.Err("file_limit should have an integer value")
+ }
+ val, _ := strconv.Atoi(fileLimit[0])
+ if fsrv.Browse.FileLimit != 0 {
+ return d.Err("file_limit is already enabled")
+ }
+ fsrv.Browse.FileLimit = val
default:
return d.Errf("unknown subdirective '%s'", d.Val())
}
}
case "precompressed":
- var order []string
- for d.NextArg() {
- modID := "http.precompressed." + d.Val()
+ fsrv.PrecompressedOrder = d.RemainingArgs()
+ if len(fsrv.PrecompressedOrder) == 0 {
+ fsrv.PrecompressedOrder = []string{"br", "zstd", "gzip"}
+ }
+
+ for _, format := range fsrv.PrecompressedOrder {
+ modID := "http.precompressed." + format
mod, err := caddy.GetModule(modID)
if err != nil {
return d.Errf("getting module named '%s': %v", modID, err)
@@ -150,10 +165,8 @@ func (fsrv *FileServer) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
if fsrv.PrecompressedRaw == nil {
fsrv.PrecompressedRaw = make(caddy.ModuleMap)
}
- fsrv.PrecompressedRaw[d.Val()] = caddyconfig.JSON(precompress, nil)
- order = append(order, d.Val())
+ fsrv.PrecompressedRaw[format] = caddyconfig.JSON(precompress, nil)
}
- fsrv.PrecompressedOrder = order
case "status":
if !d.NextArg() {
@@ -263,7 +276,7 @@ func parseTryFiles(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
tryPolicy = h.Val()
switch tryPolicy {
- case tryPolicyFirstExist, tryPolicyLargestSize, tryPolicySmallestSize, tryPolicyMostRecentlyMod:
+ case tryPolicyFirstExist, tryPolicyFirstExistFallback, tryPolicyLargestSize, tryPolicySmallestSize, tryPolicyMostRecentlyMod:
default:
return nil, h.Errf("unrecognized try policy: %s", tryPolicy)
}
diff --git a/modules/caddyhttp/fileserver/command.go b/modules/caddyhttp/fileserver/command.go
index a76998405..a04d7cade 100644
--- a/modules/caddyhttp/fileserver/command.go
+++ b/modules/caddyhttp/fileserver/command.go
@@ -66,6 +66,7 @@ respond with a file listing.`,
cmd.Flags().BoolP("templates", "t", false, "Enable template rendering")
cmd.Flags().BoolP("access-log", "a", false, "Enable the access log")
cmd.Flags().BoolP("debug", "v", false, "Enable verbose debug logs")
+ cmd.Flags().IntP("file-limit", "f", defaultDirEntryLimit, "Max directories to read")
cmd.Flags().BoolP("no-compress", "", false, "Disable Zstandard and Gzip compression")
cmd.Flags().StringSliceP("precompressed", "p", []string{}, "Specify precompression file extensions. Compression preference implied from flag order.")
cmd.RunE = caddycmd.WrapCommandFuncForCobra(cmdFileServer)
@@ -91,6 +92,7 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) {
browse := fs.Bool("browse")
templates := fs.Bool("templates")
accessLog := fs.Bool("access-log")
+ fileLimit := fs.Int("file-limit")
debug := fs.Bool("debug")
revealSymlinks := fs.Bool("reveal-symlinks")
compress := !fs.Bool("no-compress")
@@ -151,7 +153,7 @@ func cmdFileServer(fs caddycmd.Flags) (int, error) {
}
if browse {
- handler.Browse = &Browse{RevealSymlinks: revealSymlinks}
+ handler.Browse = &Browse{RevealSymlinks: revealSymlinks, FileLimit: fileLimit}
}
handlers = append(handlers, caddyconfig.JSONModuleObject(handler, "handler", "file_server", nil))
diff --git a/modules/caddyhttp/fileserver/matcher.go b/modules/caddyhttp/fileserver/matcher.go
index 71de1db29..2bc665d4f 100644
--- a/modules/caddyhttp/fileserver/matcher.go
+++ b/modules/caddyhttp/fileserver/matcher.go
@@ -90,6 +90,7 @@ type MatchFile struct {
// How to choose a file in TryFiles. Can be:
//
// - first_exist
+ // - first_exist_fallback
// - smallest_size
// - largest_size
// - most_recently_modified
@@ -173,7 +174,7 @@ func (m *MatchFile) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
func (MatchFile) CELLibrary(ctx caddy.Context) (cel.Library, error) {
requestType := cel.ObjectType("http.Request")
- matcherFactory := func(data ref.Val) (caddyhttp.RequestMatcher, error) {
+ matcherFactory := func(data ref.Val) (caddyhttp.RequestMatcherWithError, error) {
values, err := caddyhttp.CELValueToMapStrList(data)
if err != nil {
return nil, err
@@ -191,7 +192,7 @@ func (MatchFile) CELLibrary(ctx caddy.Context) (cel.Library, error) {
var try_policy string
if len(values["try_policy"]) > 0 {
- root = values["try_policy"][0]
+ try_policy = values["try_policy"][0]
}
m := MatchFile{
@@ -296,6 +297,7 @@ func (m MatchFile) Validate() error {
switch m.TryPolicy {
case "",
tryPolicyFirstExist,
+ tryPolicyFirstExistFallback,
tryPolicyLargestSize,
tryPolicySmallestSize,
tryPolicyMostRecentlyMod:
@@ -313,12 +315,22 @@ func (m MatchFile) Validate() error {
// - http.matchers.file.type: file or directory
// - http.matchers.file.remainder: Portion remaining after splitting file path (if configured)
func (m MatchFile) Match(r *http.Request) bool {
+ match, err := m.selectFile(r)
+ if err != nil {
+ // nolint:staticcheck
+ caddyhttp.SetVar(r.Context(), caddyhttp.MatcherErrorVarKey, err)
+ }
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+func (m MatchFile) MatchWithError(r *http.Request) (bool, error) {
return m.selectFile(r)
}
// selectFile chooses a file according to m.TryPolicy by appending
// the paths in m.TryFiles to m.Root, with placeholder replacements.
-func (m MatchFile) selectFile(r *http.Request) (matched bool) {
+func (m MatchFile) selectFile(r *http.Request) (bool, error) {
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
root := filepath.Clean(repl.ReplaceAll(m.Root, "."))
@@ -330,7 +342,7 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
if c := m.logger.Check(zapcore.ErrorLevel, "use of unregistered filesystem"); c != nil {
c.Write(zap.String("fs", fsName))
}
- return false
+ return false, nil
}
type matchCandidate struct {
fullpath, relative, splitRemainder string
@@ -405,13 +417,13 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
}
// setPlaceholders creates the placeholders for the matched file
- setPlaceholders := func(candidate matchCandidate, info fs.FileInfo) {
+ setPlaceholders := func(candidate matchCandidate, isDir bool) {
repl.Set("http.matchers.file.relative", filepath.ToSlash(candidate.relative))
repl.Set("http.matchers.file.absolute", filepath.ToSlash(candidate.fullpath))
repl.Set("http.matchers.file.remainder", filepath.ToSlash(candidate.splitRemainder))
fileType := "file"
- if info.IsDir() {
+ if isDir {
fileType = "directory"
}
repl.Set("http.matchers.file.type", fileType)
@@ -419,17 +431,32 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
// match file according to the configured policy
switch m.TryPolicy {
- case "", tryPolicyFirstExist:
- for _, pattern := range m.TryFiles {
+ case "", tryPolicyFirstExist, tryPolicyFirstExistFallback:
+ maxI := -1
+ if m.TryPolicy == tryPolicyFirstExistFallback {
+ maxI = len(m.TryFiles) - 1
+ }
+
+ for i, pattern := range m.TryFiles {
+ // If the pattern is a status code, emit an error,
+ // which short-circuits the middleware pipeline and
+ // writes an HTTP error response.
if err := parseErrorCode(pattern); err != nil {
- caddyhttp.SetVar(r.Context(), caddyhttp.MatcherErrorVarKey, err)
- return
+ return false, err
}
+
candidates := makeCandidates(pattern)
for _, c := range candidates {
+ // Skip the IO if using fallback policy and it's the latest item
+ if i == maxI {
+ setPlaceholders(c, false)
+
+ return true, nil
+ }
+
if info, exists := m.strictFileExists(fileSystem, c.fullpath); exists {
- setPlaceholders(c, info)
- return true
+ setPlaceholders(c, info.IsDir())
+ return true, nil
}
}
}
@@ -450,10 +477,10 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
}
}
if largestInfo == nil {
- return false
+ return false, nil
}
- setPlaceholders(largest, largestInfo)
- return true
+ setPlaceholders(largest, largestInfo.IsDir())
+ return true, nil
case tryPolicySmallestSize:
var smallestSize int64
@@ -471,10 +498,10 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
}
}
if smallestInfo == nil {
- return false
+ return false, nil
}
- setPlaceholders(smallest, smallestInfo)
- return true
+ setPlaceholders(smallest, smallestInfo.IsDir())
+ return true, nil
case tryPolicyMostRecentlyMod:
var recent matchCandidate
@@ -491,13 +518,13 @@ func (m MatchFile) selectFile(r *http.Request) (matched bool) {
}
}
if recentInfo == nil {
- return false
+ return false, nil
}
- setPlaceholders(recent, recentInfo)
- return true
+ setPlaceholders(recent, recentInfo.IsDir())
+ return true, nil
}
- return
+ return false, nil
}
// parseErrorCode checks if the input is a status
@@ -695,15 +722,16 @@ var globSafeRepl = strings.NewReplacer(
)
const (
- tryPolicyFirstExist = "first_exist"
- tryPolicyLargestSize = "largest_size"
- tryPolicySmallestSize = "smallest_size"
- tryPolicyMostRecentlyMod = "most_recently_modified"
+ tryPolicyFirstExist = "first_exist"
+ tryPolicyFirstExistFallback = "first_exist_fallback"
+ tryPolicyLargestSize = "largest_size"
+ tryPolicySmallestSize = "smallest_size"
+ tryPolicyMostRecentlyMod = "most_recently_modified"
)
// Interface guards
var (
- _ caddy.Validator = (*MatchFile)(nil)
- _ caddyhttp.RequestMatcher = (*MatchFile)(nil)
- _ caddyhttp.CELLibraryProducer = (*MatchFile)(nil)
+ _ caddy.Validator = (*MatchFile)(nil)
+ _ caddyhttp.RequestMatcherWithError = (*MatchFile)(nil)
+ _ caddyhttp.CELLibraryProducer = (*MatchFile)(nil)
)
diff --git a/modules/caddyhttp/fileserver/matcher_test.go b/modules/caddyhttp/fileserver/matcher_test.go
index 527c16bd1..b6697b9d8 100644
--- a/modules/caddyhttp/fileserver/matcher_test.go
+++ b/modules/caddyhttp/fileserver/matcher_test.go
@@ -130,7 +130,10 @@ func TestFileMatcher(t *testing.T) {
req := &http.Request{URL: u}
repl := caddyhttp.NewTestReplacer(req)
- result := m.Match(req)
+ result, err := m.MatchWithError(req)
+ if err != nil {
+ t.Errorf("Test %d: unexpected error: %v", i, err)
+ }
if result != tc.matched {
t.Errorf("Test %d: expected match=%t, got %t", i, tc.matched, result)
}
@@ -240,7 +243,10 @@ func TestPHPFileMatcher(t *testing.T) {
req := &http.Request{URL: u}
repl := caddyhttp.NewTestReplacer(req)
- result := m.Match(req)
+ result, err := m.MatchWithError(req)
+ if err != nil {
+ t.Errorf("Test %d: unexpected error: %v", i, err)
+ }
if result != tc.matched {
t.Errorf("Test %d: expected match=%t, got %t", i, tc.matched, result)
}
@@ -289,6 +295,7 @@ var expressionTests = []struct {
wantErr bool
wantResult bool
clientCertificate []byte
+ expectedPath string
}{
{
name: "file error no args (MatchFile)",
@@ -354,6 +361,15 @@ var expressionTests = []struct {
urlTarget: "https://example.com/nopenope.txt",
wantResult: false,
},
+ {
+ name: "file match long pattern foo.txt with try_policy (MatchFile)",
+ expression: &caddyhttp.MatchExpression{
+ Expr: `file({"root": "./testdata", "try_policy": "largest_size", "try_files": ["foo.txt", "large.txt"]})`,
+ },
+ urlTarget: "https://example.com/",
+ wantResult: true,
+ expectedPath: "/large.txt",
+ },
}
func TestMatchExpressionMatch(t *testing.T) {
@@ -379,9 +395,24 @@ func TestMatchExpressionMatch(t *testing.T) {
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)
- if tc.expression.Match(req) != tc.wantResult {
+ matches, err := tc.expression.MatchWithError(req)
+ if err != nil {
+ t.Errorf("MatchExpression.Match() error = %v", err)
+ return
+ }
+ if matches != tc.wantResult {
t.Errorf("MatchExpression.Match() expected to return '%t', for expression : '%s'", tc.wantResult, tc.expression.Expr)
}
+
+ if tc.expectedPath != "" {
+ path, ok := repl.Get("http.matchers.file.relative")
+ if !ok {
+ t.Errorf("MatchExpression.Match() expected to return path '%s', but got none", tc.expectedPath)
+ }
+ if path != tc.expectedPath {
+ t.Errorf("MatchExpression.Match() expected to return path '%s', but got '%s'", tc.expectedPath, path)
+ }
+ }
})
}
}
diff --git a/modules/caddyhttp/fileserver/staticfiles.go b/modules/caddyhttp/fileserver/staticfiles.go
index 4ae69b647..2b0caecfc 100644
--- a/modules/caddyhttp/fileserver/staticfiles.go
+++ b/modules/caddyhttp/fileserver/staticfiles.go
@@ -204,7 +204,7 @@ func (fsrv *FileServer) Provision(ctx caddy.Context) error {
// absolute paths before the server starts for very slight performance improvement
for i, h := range fsrv.Hide {
if !strings.Contains(h, "{") && strings.Contains(h, separator) {
- if abs, err := filepath.Abs(h); err == nil {
+ if abs, err := caddy.FastAbs(h); err == nil {
fsrv.Hide[i] = abs
}
}
@@ -636,7 +636,7 @@ func (fsrv *FileServer) transformHidePaths(repl *caddy.Replacer) []string {
for i := range fsrv.Hide {
hide[i] = repl.ReplaceAll(fsrv.Hide[i], "")
if strings.Contains(hide[i], separator) {
- abs, err := filepath.Abs(hide[i])
+ abs, err := caddy.FastAbs(hide[i])
if err == nil {
hide[i] = abs
}
@@ -655,7 +655,7 @@ func fileHidden(filename string, hide []string) bool {
}
// all path comparisons use the complete absolute path if possible
- filenameAbs, err := filepath.Abs(filename)
+ filenameAbs, err := caddy.FastAbs(filename)
if err == nil {
filename = filenameAbs
}
diff --git a/modules/caddyhttp/fileserver/testdata/large.txt b/modules/caddyhttp/fileserver/testdata/large.txt
new file mode 100644
index 000000000..c36623744
--- /dev/null
+++ b/modules/caddyhttp/fileserver/testdata/large.txt
@@ -0,0 +1,3 @@
+This is a file with more content than the other files in this directory
+such that tests using the largest_size policy pick this file, or the
+smallest_size policy avoids this file. \ No newline at end of file
diff --git a/modules/caddyhttp/headers/headers.go b/modules/caddyhttp/headers/headers.go
index a3279d913..c66bd4144 100644
--- a/modules/caddyhttp/headers/headers.go
+++ b/modules/caddyhttp/headers/headers.go
@@ -200,9 +200,7 @@ func (ops HeaderOps) ApplyTo(hdr http.Header, repl *caddy.Replacer) {
for _, fieldName := range ops.Delete {
fieldName = repl.ReplaceKnown(fieldName, "")
if fieldName == "*" {
- for existingField := range hdr {
- delete(hdr, existingField)
- }
+ clear(hdr)
}
}
diff --git a/modules/caddyhttp/ip_matchers.go b/modules/caddyhttp/ip_matchers.go
index 99eb39dff..5e0b356e7 100644
--- a/modules/caddyhttp/ip_matchers.go
+++ b/modules/caddyhttp/ip_matchers.go
@@ -108,7 +108,7 @@ func (MatchRemoteIP) CELLibrary(ctx caddy.Context) (cel.Library, error) {
// internal data type of the MatchPath value.
[]*cel.Type{cel.ListType(cel.StringType)},
// function to convert a constant list of strings to a MatchPath instance.
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
refStringList := reflect.TypeOf([]string{})
strList, err := data.ConvertToNative(refStringList)
if err != nil {
@@ -145,9 +145,23 @@ func (m *MatchRemoteIP) Provision(ctx caddy.Context) error {
// Match returns true if r matches m.
func (m MatchRemoteIP) Match(r *http.Request) bool {
+ match, err := m.MatchWithError(r)
+ if err != nil {
+ SetVar(r.Context(), MatcherErrorVarKey, err)
+ }
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+func (m MatchRemoteIP) MatchWithError(r *http.Request) (bool, error) {
+ // if handshake is not finished, we infer 0-RTT that has
+ // not verified remote IP; could be spoofed, so we throw
+ // HTTP 425 status to tell the client to try again after
+ // the handshake is complete
if r.TLS != nil && !r.TLS.HandshakeComplete {
- return false // if handshake is not finished, we infer 0-RTT that has not verified remote IP; could be spoofed
+ return false, Error(http.StatusTooEarly, fmt.Errorf("TLS handshake not complete, remote IP cannot be verified"))
}
+
address := r.RemoteAddr
clientIP, zoneID, err := parseIPZoneFromString(address)
if err != nil {
@@ -155,7 +169,7 @@ func (m MatchRemoteIP) Match(r *http.Request) bool {
c.Write(zap.Error(err))
}
- return false
+ return false, nil
}
matches, zoneFilter := matchIPByCidrZones(clientIP, zoneID, m.cidrs, m.zones)
if !matches && !zoneFilter {
@@ -163,7 +177,7 @@ func (m MatchRemoteIP) Match(r *http.Request) bool {
c.Write(zap.String("zone", zoneID))
}
}
- return matches
+ return matches, nil
}
// CaddyModule returns the Caddy module information.
@@ -207,7 +221,7 @@ func (MatchClientIP) CELLibrary(ctx caddy.Context) (cel.Library, error) {
// internal data type of the MatchPath value.
[]*cel.Type{cel.ListType(cel.StringType)},
// function to convert a constant list of strings to a MatchPath instance.
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
refStringList := reflect.TypeOf([]string{})
strList, err := data.ConvertToNative(refStringList)
if err != nil {
@@ -238,20 +252,34 @@ func (m *MatchClientIP) Provision(ctx caddy.Context) error {
// Match returns true if r matches m.
func (m MatchClientIP) Match(r *http.Request) bool {
+ match, err := m.MatchWithError(r)
+ if err != nil {
+ SetVar(r.Context(), MatcherErrorVarKey, err)
+ }
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+func (m MatchClientIP) MatchWithError(r *http.Request) (bool, error) {
+ // if handshake is not finished, we infer 0-RTT that has
+ // not verified remote IP; could be spoofed, so we throw
+ // HTTP 425 status to tell the client to try again after
+ // the handshake is complete
if r.TLS != nil && !r.TLS.HandshakeComplete {
- return false // if handshake is not finished, we infer 0-RTT that has not verified remote IP; could be spoofed
+ return false, Error(http.StatusTooEarly, fmt.Errorf("TLS handshake not complete, remote IP cannot be verified"))
}
+
address := GetVar(r.Context(), ClientIPVarKey).(string)
clientIP, zoneID, err := parseIPZoneFromString(address)
if err != nil {
m.logger.Error("getting client IP", zap.Error(err))
- return false
+ return false, nil
}
matches, zoneFilter := matchIPByCidrZones(clientIP, zoneID, m.cidrs, m.zones)
if !matches && !zoneFilter {
m.logger.Debug("zone ID from client IP did not match", zap.String("zone", zoneID))
}
- return matches
+ return matches, nil
}
func provisionCidrsZonesFromRanges(ranges []string) ([]*netip.Prefix, []string, error) {
@@ -326,13 +354,13 @@ func matchIPByCidrZones(clientIP netip.Addr, zoneID string, cidrs []*netip.Prefi
// Interface guards
var (
- _ RequestMatcher = (*MatchRemoteIP)(nil)
- _ caddy.Provisioner = (*MatchRemoteIP)(nil)
- _ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil)
- _ CELLibraryProducer = (*MatchRemoteIP)(nil)
-
- _ RequestMatcher = (*MatchClientIP)(nil)
- _ caddy.Provisioner = (*MatchClientIP)(nil)
- _ caddyfile.Unmarshaler = (*MatchClientIP)(nil)
- _ CELLibraryProducer = (*MatchClientIP)(nil)
+ _ RequestMatcherWithError = (*MatchRemoteIP)(nil)
+ _ caddy.Provisioner = (*MatchRemoteIP)(nil)
+ _ caddyfile.Unmarshaler = (*MatchRemoteIP)(nil)
+ _ CELLibraryProducer = (*MatchRemoteIP)(nil)
+
+ _ RequestMatcherWithError = (*MatchClientIP)(nil)
+ _ caddy.Provisioner = (*MatchClientIP)(nil)
+ _ caddyfile.Unmarshaler = (*MatchClientIP)(nil)
+ _ CELLibraryProducer = (*MatchClientIP)(nil)
)
diff --git a/modules/caddyhttp/matchers.go b/modules/caddyhttp/matchers.go
index 7f62bdd3a..e5ca28b95 100644
--- a/modules/caddyhttp/matchers.go
+++ b/modules/caddyhttp/matchers.go
@@ -296,6 +296,12 @@ func (m MatchHost) Provision(_ caddy.Context) error {
// Match returns true if r matches m.
func (m MatchHost) Match(r *http.Request) bool {
+ match, _ := m.MatchWithError(r)
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+func (m MatchHost) MatchWithError(r *http.Request) (bool, error) {
reqHost, _, err := net.SplitHostPort(r.Host)
if err != nil {
// OK; probably didn't have a port
@@ -315,7 +321,7 @@ func (m MatchHost) Match(r *http.Request) bool {
return m[i] >= reqHost
})
if pos < len(m) && m[pos] == reqHost {
- return true
+ return true, nil
}
}
@@ -346,13 +352,13 @@ outer:
continue outer
}
}
- return true
+ return true, nil
} else if strings.EqualFold(reqHost, host) {
- return true
+ return true, nil
}
}
- return false
+ return false, nil
}
// CELLibrary produces options that expose this matcher for use in CEL
@@ -366,7 +372,7 @@ func (MatchHost) CELLibrary(ctx caddy.Context) (cel.Library, error) {
"host",
"host_match_request_list",
[]*cel.Type{cel.ListType(cel.StringType)},
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
refStringList := reflect.TypeOf([]string{})
strList, err := data.ConvertToNative(refStringList)
if err != nil {
@@ -411,6 +417,12 @@ func (m MatchPath) Provision(_ caddy.Context) error {
// Match returns true if r matches m.
func (m MatchPath) Match(r *http.Request) bool {
+ match, _ := m.MatchWithError(r)
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+func (m MatchPath) MatchWithError(r *http.Request) (bool, error) {
// Even though RFC 9110 says that path matching is case-sensitive
// (https://www.rfc-editor.org/rfc/rfc9110.html#section-4.2.3),
// we do case-insensitive matching to mitigate security issues
@@ -436,7 +448,7 @@ func (m MatchPath) Match(r *http.Request) bool {
// special case: whole path is wildcard; this is unnecessary
// as it matches all requests, which is the same as no matcher
if matchPattern == "*" {
- return true
+ return true, nil
}
// Clean the path, merge doubled slashes, etc.
@@ -464,7 +476,7 @@ func (m MatchPath) Match(r *http.Request) bool {
if strings.Contains(matchPattern, "%") {
reqPathForPattern := CleanPath(r.URL.EscapedPath(), mergeSlashes)
if m.matchPatternWithEscapeSequence(reqPathForPattern, matchPattern) {
- return true
+ return true, nil
}
// doing prefix/suffix/substring matches doesn't make sense
@@ -483,7 +495,7 @@ func (m MatchPath) Match(r *http.Request) bool {
strings.HasPrefix(matchPattern, "*") &&
strings.HasSuffix(matchPattern, "*") {
if strings.Contains(reqPathForPattern, matchPattern[1:len(matchPattern)-1]) {
- return true
+ return true, nil
}
continue
}
@@ -495,7 +507,7 @@ func (m MatchPath) Match(r *http.Request) bool {
// treat it as a fast suffix match
if strings.HasPrefix(matchPattern, "*") {
if strings.HasSuffix(reqPathForPattern, matchPattern[1:]) {
- return true
+ return true, nil
}
continue
}
@@ -504,7 +516,7 @@ func (m MatchPath) Match(r *http.Request) bool {
// treat it as a fast prefix match
if strings.HasSuffix(matchPattern, "*") {
if strings.HasPrefix(reqPathForPattern, matchPattern[:len(matchPattern)-1]) {
- return true
+ return true, nil
}
continue
}
@@ -515,10 +527,10 @@ func (m MatchPath) Match(r *http.Request) bool {
// because we can't handle it anyway
matches, _ := path.Match(matchPattern, reqPathForPattern)
if matches {
- return true
+ return true, nil
}
}
- return false
+ return false, nil
}
func (MatchPath) matchPatternWithEscapeSequence(escapedPath, matchPath string) bool {
@@ -642,7 +654,7 @@ func (MatchPath) CELLibrary(ctx caddy.Context) (cel.Library, error) {
// internal data type of the MatchPath value.
[]*cel.Type{cel.ListType(cel.StringType)},
// function to convert a constant list of strings to a MatchPath instance.
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
refStringList := reflect.TypeOf([]string{})
strList, err := data.ConvertToNative(refStringList)
if err != nil {
@@ -677,6 +689,12 @@ func (MatchPathRE) CaddyModule() caddy.ModuleInfo {
// Match returns true if r matches m.
func (m MatchPathRE) Match(r *http.Request) bool {
+ match, _ := m.MatchWithError(r)
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+func (m MatchPathRE) MatchWithError(r *http.Request) (bool, error) {
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
// Clean the path, merges doubled slashes, etc.
@@ -684,7 +702,7 @@ func (m MatchPathRE) Match(r *http.Request) bool {
// the path matcher. See #4407
cleanedPath := cleanPath(r.URL.Path)
- return m.MatchRegexp.Match(cleanedPath, repl)
+ return m.MatchRegexp.Match(cleanedPath, repl), nil
}
// CELLibrary produces options that expose this matcher for use in CEL
@@ -698,7 +716,7 @@ func (MatchPathRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
"path_regexp",
"path_regexp_request_string",
[]*cel.Type{cel.StringType},
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
pattern := data.(types.String)
matcher := MatchPathRE{MatchRegexp{
Name: ctx.Value(MatcherNameCtxKey).(string),
@@ -715,7 +733,7 @@ func (MatchPathRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
"path_regexp",
"path_regexp_request_string_string",
[]*cel.Type{cel.StringType, cel.StringType},
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
refStringList := reflect.TypeOf([]string{})
params, err := data.ConvertToNative(refStringList)
if err != nil {
@@ -764,7 +782,13 @@ func (m *MatchMethod) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// Match returns true if r matches m.
func (m MatchMethod) Match(r *http.Request) bool {
- return slices.Contains(m, r.Method)
+ match, _ := m.MatchWithError(r)
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+func (m MatchMethod) MatchWithError(r *http.Request) (bool, error) {
+ return slices.Contains(m, r.Method), nil
}
// CELLibrary produces options that expose this matcher for use in CEL
@@ -778,7 +802,7 @@ func (MatchMethod) CELLibrary(_ caddy.Context) (cel.Library, error) {
"method",
"method_request_list",
[]*cel.Type{cel.ListType(cel.StringType)},
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
refStringList := reflect.TypeOf([]string{})
strList, err := data.ConvertToNative(refStringList)
if err != nil {
@@ -823,10 +847,17 @@ func (m *MatchQuery) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// Match returns true if r matches m. An empty m matches an empty query string.
func (m MatchQuery) Match(r *http.Request) bool {
+ match, _ := m.MatchWithError(r)
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+// An empty m matches an empty query string.
+func (m MatchQuery) MatchWithError(r *http.Request) (bool, error) {
// If no query keys are configured, this only
// matches an empty query string.
if len(m) == 0 {
- return len(r.URL.Query()) == 0
+ return len(r.URL.Query()) == 0, nil
}
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
@@ -843,7 +874,7 @@ func (m MatchQuery) Match(r *http.Request) bool {
// "Relying on parser alignment for security is doomed." Overall conclusion is that
// splitting on & and rejecting ; in key=value pairs is safer than accepting raw ;.
// We regard the Go team's decision as sound and thus reject malformed query strings.
- return false
+ return false, nil
}
// Count the amount of matched keys, to ensure we AND
@@ -854,7 +885,7 @@ func (m MatchQuery) Match(r *http.Request) bool {
param = repl.ReplaceAll(param, "")
paramVal, found := parsed[param]
if !found {
- return false
+ return false, nil
}
for _, v := range vals {
v = repl.ReplaceAll(v, "")
@@ -864,7 +895,7 @@ func (m MatchQuery) Match(r *http.Request) bool {
}
}
}
- return matchedKeys == len(m)
+ return matchedKeys == len(m), nil
}
// CELLibrary produces options that expose this matcher for use in CEL
@@ -878,7 +909,7 @@ func (MatchQuery) CELLibrary(_ caddy.Context) (cel.Library, error) {
"query",
"query_matcher_request_map",
[]*cel.Type{CELTypeJSON},
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
mapStrListStr, err := CELValueToMapStrList(data)
if err != nil {
return nil, err
@@ -940,8 +971,14 @@ func (m *MatchHeader) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// Match returns true if r matches m.
func (m MatchHeader) Match(r *http.Request) bool {
+ match, _ := m.MatchWithError(r)
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+func (m MatchHeader) MatchWithError(r *http.Request) (bool, error) {
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
- return matchHeaders(r.Header, http.Header(m), r.Host, r.TransferEncoding, repl)
+ return matchHeaders(r.Header, http.Header(m), r.Host, r.TransferEncoding, repl), nil
}
// CELLibrary produces options that expose this matcher for use in CEL
@@ -956,7 +993,7 @@ func (MatchHeader) CELLibrary(_ caddy.Context) (cel.Library, error) {
"header",
"header_matcher_request_map",
[]*cel.Type{CELTypeJSON},
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
mapStrListStr, err := CELValueToMapStrList(data)
if err != nil {
return nil, err
@@ -1079,6 +1116,12 @@ func (m *MatchHeaderRE) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// Match returns true if r matches m.
func (m MatchHeaderRE) Match(r *http.Request) bool {
+ match, _ := m.MatchWithError(r)
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+func (m MatchHeaderRE) MatchWithError(r *http.Request) (bool, error) {
for field, rm := range m {
actualFieldVals := getHeaderFieldVals(r.Header, field, r.Host, r.TransferEncoding)
match := false
@@ -1091,10 +1134,10 @@ func (m MatchHeaderRE) Match(r *http.Request) bool {
}
}
if !match {
- return false
+ return false, nil
}
}
- return true
+ return true, nil
}
// Provision compiles m's regular expressions.
@@ -1130,7 +1173,7 @@ func (MatchHeaderRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
"header_regexp",
"header_regexp_request_string_string",
[]*cel.Type{cel.StringType, cel.StringType},
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
refStringList := reflect.TypeOf([]string{})
params, err := data.ConvertToNative(refStringList)
if err != nil {
@@ -1153,7 +1196,7 @@ func (MatchHeaderRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
"header_regexp",
"header_regexp_request_string_string_string",
[]*cel.Type{cel.StringType, cel.StringType, cel.StringType},
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
refStringList := reflect.TypeOf([]string{})
params, err := data.ConvertToNative(refStringList)
if err != nil {
@@ -1191,31 +1234,37 @@ func (MatchProtocol) CaddyModule() caddy.ModuleInfo {
// Match returns true if r matches m.
func (m MatchProtocol) Match(r *http.Request) bool {
+ match, _ := m.MatchWithError(r)
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+func (m MatchProtocol) MatchWithError(r *http.Request) (bool, error) {
switch string(m) {
case "grpc":
- return strings.HasPrefix(r.Header.Get("content-type"), "application/grpc")
+ return strings.HasPrefix(r.Header.Get("content-type"), "application/grpc"), nil
case "https":
- return r.TLS != nil
+ return r.TLS != nil, nil
case "http":
- return r.TLS == nil
+ return r.TLS == nil, nil
case "http/1.0":
- return r.ProtoMajor == 1 && r.ProtoMinor == 0
+ return r.ProtoMajor == 1 && r.ProtoMinor == 0, nil
case "http/1.0+":
- return r.ProtoAtLeast(1, 0)
+ return r.ProtoAtLeast(1, 0), nil
case "http/1.1":
- return r.ProtoMajor == 1 && r.ProtoMinor == 1
+ return r.ProtoMajor == 1 && r.ProtoMinor == 1, nil
case "http/1.1+":
- return r.ProtoAtLeast(1, 1)
+ return r.ProtoAtLeast(1, 1), nil
case "http/2":
- return r.ProtoMajor == 2
+ return r.ProtoMajor == 2, nil
case "http/2+":
- return r.ProtoAtLeast(2, 0)
+ return r.ProtoAtLeast(2, 0), nil
case "http/3":
- return r.ProtoMajor == 3
+ return r.ProtoMajor == 3, nil
case "http/3+":
- return r.ProtoAtLeast(3, 0)
+ return r.ProtoAtLeast(3, 0), nil
}
- return false
+ return false, nil
}
// UnmarshalCaddyfile implements caddyfile.Unmarshaler.
@@ -1242,7 +1291,7 @@ func (MatchProtocol) CELLibrary(_ caddy.Context) (cel.Library, error) {
"protocol",
"protocol_request_string",
[]*cel.Type{cel.StringType},
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
protocolStr, ok := data.(types.String)
if !ok {
return nil, errors.New("protocol argument was not a string")
@@ -1262,16 +1311,22 @@ func (MatchTLS) CaddyModule() caddy.ModuleInfo {
// Match returns true if r matches m.
func (m MatchTLS) Match(r *http.Request) bool {
+ match, _ := m.MatchWithError(r)
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+func (m MatchTLS) MatchWithError(r *http.Request) (bool, error) {
if r.TLS == nil {
- return false
+ return false, nil
}
if m.HandshakeComplete != nil {
if (!*m.HandshakeComplete && r.TLS.HandshakeComplete) ||
(*m.HandshakeComplete && !r.TLS.HandshakeComplete) {
- return false
+ return false, nil
}
}
- return true
+ return true, nil
}
// UnmarshalCaddyfile parses Caddyfile tokens for this matcher. Syntax:
@@ -1341,7 +1396,15 @@ func (m *MatchNot) Provision(ctx caddy.Context) error {
for _, modMap := range matcherSets.([]map[string]any) {
var ms MatcherSet
for _, modIface := range modMap {
- ms = append(ms, modIface.(RequestMatcher))
+ if mod, ok := modIface.(RequestMatcherWithError); ok {
+ ms = append(ms, mod)
+ continue
+ }
+ if mod, ok := modIface.(RequestMatcher); ok {
+ ms = append(ms, mod)
+ continue
+ }
+ return fmt.Errorf("module is not a request matcher: %T", modIface)
}
m.MatcherSets = append(m.MatcherSets, ms)
}
@@ -1352,12 +1415,24 @@ func (m *MatchNot) Provision(ctx caddy.Context) error {
// the embedded matchers, false is returned if any of its matcher
// sets return true.
func (m MatchNot) Match(r *http.Request) bool {
+ match, _ := m.MatchWithError(r)
+ return match
+}
+
+// MatchWithError returns true if r matches m. Since this matcher
+// negates the embedded matchers, false is returned if any of its
+// matcher sets return true.
+func (m MatchNot) MatchWithError(r *http.Request) (bool, error) {
for _, ms := range m.MatcherSets {
- if ms.Match(r) {
- return false
+ matches, err := ms.MatchWithError(r)
+ if err != nil {
+ return false, err
+ }
+ if matches {
+ return false, nil
}
}
- return true
+ return true, nil
}
// MatchRegexp is an embedable type for matching
@@ -1473,7 +1548,7 @@ func (mre *MatchRegexp) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// ParseCaddyfileNestedMatcher parses the Caddyfile tokens for a nested
// matcher set, and returns its raw module map value.
func ParseCaddyfileNestedMatcherSet(d *caddyfile.Dispenser) (caddy.ModuleMap, error) {
- matcherMap := make(map[string]RequestMatcher)
+ matcherMap := make(map[string]any)
// in case there are multiple instances of the same matcher, concatenate
// their tokens (we expect that UnmarshalCaddyfile should be able to
@@ -1498,11 +1573,15 @@ func ParseCaddyfileNestedMatcherSet(d *caddyfile.Dispenser) (caddy.ModuleMap, er
if err != nil {
return nil, err
}
- rm, ok := unm.(RequestMatcher)
- if !ok {
- return nil, fmt.Errorf("matcher module '%s' is not a request matcher", matcherName)
+ if rm, ok := unm.(RequestMatcherWithError); ok {
+ matcherMap[matcherName] = rm
+ continue
}
- matcherMap[matcherName] = rm
+ if rm, ok := unm.(RequestMatcher); ok {
+ matcherMap[matcherName] = rm
+ continue
+ }
+ return nil, fmt.Errorf("matcher module '%s' is not a request matcher", matcherName)
}
// we should now have a functional matcher, but we also
@@ -1528,24 +1607,28 @@ const regexpPlaceholderPrefix = "http.regexp"
// holds an optional error emitted from a request matcher,
// to short-circuit the handler chain, since matchers cannot
// return errors via the RequestMatcher interface.
+//
+// Deprecated: Matchers should implement RequestMatcherWithError
+// which can return an error directly, instead of smuggling it
+// through the vars map.
const MatcherErrorVarKey = "matchers.error"
// Interface guards
var (
- _ RequestMatcher = (*MatchHost)(nil)
- _ caddy.Provisioner = (*MatchHost)(nil)
- _ RequestMatcher = (*MatchPath)(nil)
- _ RequestMatcher = (*MatchPathRE)(nil)
- _ caddy.Provisioner = (*MatchPathRE)(nil)
- _ RequestMatcher = (*MatchMethod)(nil)
- _ RequestMatcher = (*MatchQuery)(nil)
- _ RequestMatcher = (*MatchHeader)(nil)
- _ RequestMatcher = (*MatchHeaderRE)(nil)
- _ caddy.Provisioner = (*MatchHeaderRE)(nil)
- _ RequestMatcher = (*MatchProtocol)(nil)
- _ RequestMatcher = (*MatchNot)(nil)
- _ caddy.Provisioner = (*MatchNot)(nil)
- _ caddy.Provisioner = (*MatchRegexp)(nil)
+ _ RequestMatcherWithError = (*MatchHost)(nil)
+ _ caddy.Provisioner = (*MatchHost)(nil)
+ _ RequestMatcherWithError = (*MatchPath)(nil)
+ _ RequestMatcherWithError = (*MatchPathRE)(nil)
+ _ caddy.Provisioner = (*MatchPathRE)(nil)
+ _ RequestMatcherWithError = (*MatchMethod)(nil)
+ _ RequestMatcherWithError = (*MatchQuery)(nil)
+ _ RequestMatcherWithError = (*MatchHeader)(nil)
+ _ RequestMatcherWithError = (*MatchHeaderRE)(nil)
+ _ caddy.Provisioner = (*MatchHeaderRE)(nil)
+ _ RequestMatcherWithError = (*MatchProtocol)(nil)
+ _ RequestMatcherWithError = (*MatchNot)(nil)
+ _ caddy.Provisioner = (*MatchNot)(nil)
+ _ caddy.Provisioner = (*MatchRegexp)(nil)
_ caddyfile.Unmarshaler = (*MatchHost)(nil)
_ caddyfile.Unmarshaler = (*MatchPath)(nil)
diff --git a/modules/caddyhttp/matchers_test.go b/modules/caddyhttp/matchers_test.go
index 05eaade5b..f7be6909e 100644
--- a/modules/caddyhttp/matchers_test.go
+++ b/modules/caddyhttp/matchers_test.go
@@ -158,7 +158,10 @@ func TestHostMatcher(t *testing.T) {
t.Errorf("Test %d %v: provisioning failed: %v", i, tc.match, err)
}
- actual := tc.match.Match(req)
+ actual, err := tc.match.MatchWithError(req)
+ if err != nil {
+ t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
+ }
if actual != tc.expect {
t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input)
continue
@@ -430,7 +433,10 @@ func TestPathMatcher(t *testing.T) {
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)
- actual := tc.match.Match(req)
+ actual, err := tc.match.MatchWithError(req)
+ if err != nil {
+ t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
+ }
if actual != tc.expect {
t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input)
continue
@@ -451,7 +457,10 @@ func TestPathMatcherWindows(t *testing.T) {
req = req.WithContext(ctx)
match := MatchPath{"*.php"}
- matched := match.Match(req)
+ matched, err := match.MatchWithError(req)
+ if err != nil {
+ t.Errorf("Expected no error, but got: %v", err)
+ }
if !matched {
t.Errorf("Expected to match; should ignore trailing dots and spaces")
}
@@ -555,7 +564,10 @@ func TestPathREMatcher(t *testing.T) {
req = req.WithContext(ctx)
addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
- actual := tc.match.Match(req)
+ actual, err := tc.match.MatchWithError(req)
+ if err != nil {
+ t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
+ }
if actual != tc.expect {
t.Errorf("Test %d [%v]: Expected %t, got %t for input '%s'",
i, tc.match.Pattern, tc.expect, actual, tc.input)
@@ -691,7 +703,10 @@ func TestHeaderMatcher(t *testing.T) {
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)
- actual := tc.match.Match(req)
+ actual, err := tc.match.MatchWithError(req)
+ if err != nil {
+ t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
+ }
if actual != tc.expect {
t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input)
continue
@@ -818,7 +833,10 @@ func TestQueryMatcher(t *testing.T) {
repl.Set("http.vars.debug", "1")
repl.Set("http.vars.key", "somekey")
req = req.WithContext(ctx)
- actual := tc.match.Match(req)
+ actual, err := tc.match.MatchWithError(req)
+ if err != nil {
+ t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
+ }
if actual != tc.expect {
t.Errorf("Test %d %v: Expected %t, got %t for '%s'", i, tc.match, tc.expect, actual, tc.input)
continue
@@ -887,7 +905,10 @@ func TestHeaderREMatcher(t *testing.T) {
req = req.WithContext(ctx)
addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
- actual := tc.match.Match(req)
+ actual, err := tc.match.MatchWithError(req)
+ if err != nil {
+ t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
+ }
if actual != tc.expect {
t.Errorf("Test %d [%v]: Expected %t, got %t for input '%s'",
i, tc.match, tc.expect, actual, tc.input)
@@ -927,7 +948,7 @@ func BenchmarkHeaderREMatcher(b *testing.B) {
req = req.WithContext(ctx)
addHTTPVarsToReplacer(repl, req, httptest.NewRecorder())
for run := 0; run < b.N; run++ {
- match.Match(req)
+ match.MatchWithError(req)
}
}
@@ -998,7 +1019,10 @@ func TestVarREMatcher(t *testing.T) {
tc.input.ServeHTTP(httptest.NewRecorder(), req, emptyHandler)
- actual := tc.match.Match(req)
+ actual, err := tc.match.MatchWithError(req)
+ if err != nil {
+ t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
+ }
if actual != tc.expect {
t.Errorf("Test %d [%v]: Expected %t, got %t for input '%s'",
i, tc.match, tc.expect, actual, tc.input)
@@ -1123,7 +1147,10 @@ func TestNotMatcher(t *testing.T) {
ctx := context.WithValue(req.Context(), caddy.ReplacerCtxKey, repl)
req = req.WithContext(ctx)
- actual := tc.match.Match(req)
+ actual, err := tc.match.MatchWithError(req)
+ if err != nil {
+ t.Errorf("Test %d %v: matching failed: %v", i, tc.match, err)
+ }
if actual != tc.expect {
t.Errorf("Test %d %+v: Expected %t, got %t for: host=%s path=%s'", i, tc.match, tc.expect, actual, tc.host, tc.path)
continue
@@ -1155,7 +1182,7 @@ func BenchmarkLargeHostMatcher(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
- matcher.Match(req)
+ matcher.MatchWithError(req)
}
}
@@ -1169,7 +1196,7 @@ func BenchmarkHostMatcherWithoutPlaceholder(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
- match.Match(req)
+ match.MatchWithError(req)
}
}
@@ -1187,6 +1214,6 @@ func BenchmarkHostMatcherWithPlaceholder(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
- match.Match(req)
+ match.MatchWithError(req)
}
}
diff --git a/modules/caddyhttp/metrics.go b/modules/caddyhttp/metrics.go
index 947721429..9bb97e0b4 100644
--- a/modules/caddyhttp/metrics.go
+++ b/modules/caddyhttp/metrics.go
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"net/http"
+ "strings"
"sync"
"time"
@@ -133,8 +134,8 @@ func (h *metricsInstrumentedHandler) ServeHTTP(w http.ResponseWriter, r *http.Re
statusLabels := prometheus.Labels{"server": server, "handler": h.handler, "method": method, "code": ""}
if h.metrics.PerHost {
- labels["host"] = r.Host
- statusLabels["host"] = r.Host
+ labels["host"] = strings.ToLower(r.Host)
+ statusLabels["host"] = strings.ToLower(r.Host)
}
inFlight := h.metrics.httpMetrics.requestInFlight.With(labels)
diff --git a/modules/caddyhttp/replacer.go b/modules/caddyhttp/replacer.go
index 2c0f32357..776aa6294 100644
--- a/modules/caddyhttp/replacer.go
+++ b/modules/caddyhttp/replacer.go
@@ -186,6 +186,11 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
return path.Ext(req.URL.Path), true
case "http.request.uri.query":
return req.URL.RawQuery, true
+ case "http.request.uri.prefixed_query":
+ if req.URL.RawQuery == "" {
+ return "", true
+ }
+ return "?" + req.URL.RawQuery, true
case "http.request.duration":
start := GetVar(req.Context(), "start_time").(time.Time)
return time.Since(start), true
@@ -239,6 +244,12 @@ func addHTTPVarsToReplacer(repl *caddy.Replacer, req *http.Request, w http.Respo
case "http.request.orig_uri.query":
or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request)
return or.URL.RawQuery, true
+ case "http.request.orig_uri.prefixed_query":
+ or, _ := req.Context().Value(OriginalRequestCtxKey).(http.Request)
+ if or.URL.RawQuery == "" {
+ return "", true
+ }
+ return "?" + or.URL.RawQuery, true
}
// remote IP range/prefix (e.g. keep top 24 bits of 1.2.3.4 => "1.2.3.0/24")
diff --git a/modules/caddyhttp/requestbody/requestbody.go b/modules/caddyhttp/requestbody/requestbody.go
index 1c804aa13..830050416 100644
--- a/modules/caddyhttp/requestbody/requestbody.go
+++ b/modules/caddyhttp/requestbody/requestbody.go
@@ -15,6 +15,7 @@
package requestbody
import (
+ "errors"
"io"
"net/http"
"time"
@@ -94,7 +95,8 @@ type errorWrapper struct {
func (ew errorWrapper) Read(p []byte) (n int, err error) {
n, err = ew.ReadCloser.Read(p)
- if err != nil && err.Error() == "http: request body too large" {
+ var mbe *http.MaxBytesError
+ if errors.As(err, &mbe) {
err = caddyhttp.Error(http.StatusRequestEntityTooLarge, err)
}
return
diff --git a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go
index 68eee32be..6fe7df3fd 100644
--- a/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go
+++ b/modules/caddyhttp/reverseproxy/fastcgi/caddyfile.go
@@ -18,6 +18,7 @@ import (
"encoding/json"
"net/http"
"strconv"
+ "strings"
"github.com/caddyserver/caddy/v2"
"github.com/caddyserver/caddy/v2/caddyconfig"
@@ -179,7 +180,7 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
indexFile := "index.php"
// set up for explicitly overriding try_files
- tryFiles := []string{}
+ var tryFiles []string
// if the user specified a matcher token, use that
// matcher in a route that wraps both of our routes;
@@ -310,37 +311,60 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
// if the index is turned off, we skip the redirect and try_files
if indexFile != "off" {
- // route to redirect to canonical path if index PHP file
- redirMatcherSet := caddy.ModuleMap{
- "file": h.JSON(fileserver.MatchFile{
- TryFiles: []string{"{http.request.uri.path}/" + indexFile},
- }),
- "not": h.JSON(caddyhttp.MatchNot{
- MatcherSetsRaw: []caddy.ModuleMap{
- {
- "path": h.JSON(caddyhttp.MatchPath{"*/"}),
- },
- },
- }),
- }
- redirHandler := caddyhttp.StaticResponse{
- StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
- Headers: http.Header{"Location": []string{"{http.request.orig_uri.path}/"}},
- }
- redirRoute := caddyhttp.Route{
- MatcherSetsRaw: []caddy.ModuleMap{redirMatcherSet},
- HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(redirHandler, "handler", "static_response", nil)},
- }
+ dirRedir := false
+ dirIndex := "{http.request.uri.path}/" + indexFile
+ tryPolicy := "first_exist_fallback"
// if tryFiles wasn't overridden, use a reasonable default
if len(tryFiles) == 0 {
- tryFiles = []string{"{http.request.uri.path}", "{http.request.uri.path}/" + indexFile, indexFile}
+ tryFiles = []string{"{http.request.uri.path}", dirIndex, indexFile}
+ dirRedir = true
+ } else {
+ if !strings.HasSuffix(tryFiles[len(tryFiles)-1], ".php") {
+ // use first_exist strategy if the last file is not a PHP file
+ tryPolicy = ""
+ }
+
+ for _, tf := range tryFiles {
+ if tf == dirIndex {
+ dirRedir = true
+
+ break
+ }
+ }
+ }
+
+ if dirRedir {
+ // route to redirect to canonical path if index PHP file
+ redirMatcherSet := caddy.ModuleMap{
+ "file": h.JSON(fileserver.MatchFile{
+ TryFiles: []string{dirIndex},
+ }),
+ "not": h.JSON(caddyhttp.MatchNot{
+ MatcherSetsRaw: []caddy.ModuleMap{
+ {
+ "path": h.JSON(caddyhttp.MatchPath{"*/"}),
+ },
+ },
+ }),
+ }
+ redirHandler := caddyhttp.StaticResponse{
+ StatusCode: caddyhttp.WeakString(strconv.Itoa(http.StatusPermanentRedirect)),
+ Headers: http.Header{"Location": []string{"{http.request.orig_uri.path}/{http.request.orig_uri.prefixed_query}"}},
+ }
+ redirRoute := caddyhttp.Route{
+ MatcherSetsRaw: []caddy.ModuleMap{redirMatcherSet},
+ HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(redirHandler, "handler", "static_response", nil)},
+ }
+
+ routes = append(routes, redirRoute)
}
// route to rewrite to PHP index file
rewriteMatcherSet := caddy.ModuleMap{
"file": h.JSON(fileserver.MatchFile{
TryFiles: tryFiles,
+ TryPolicy: tryPolicy,
SplitPath: extensions,
}),
}
@@ -352,7 +376,7 @@ func parsePHPFastCGI(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(rewriteHandler, "handler", "rewrite", nil)},
}
- routes = append(routes, redirRoute, rewriteRoute)
+ routes = append(routes, rewriteRoute)
}
// route to actually reverse proxy requests to PHP files;
diff --git a/modules/caddyhttp/reverseproxy/fastcgi/client.go b/modules/caddyhttp/reverseproxy/fastcgi/client.go
index 7284fe672..684394f53 100644
--- a/modules/caddyhttp/reverseproxy/fastcgi/client.go
+++ b/modules/caddyhttp/reverseproxy/fastcgi/client.go
@@ -41,6 +41,8 @@ import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
+
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
// FCGIListenSockFileno describes listen socket file number.
@@ -136,6 +138,15 @@ type client struct {
// Do made the request and returns a io.Reader that translates the data read
// from fcgi responder out of fcgi packet before returning it.
func (c *client) Do(p map[string]string, req io.Reader) (r io.Reader, err error) {
+ // check for CONTENT_LENGTH, since the lack of it or wrong value will cause the backend to hang
+ if clStr, ok := p["CONTENT_LENGTH"]; !ok {
+ return nil, caddyhttp.Error(http.StatusLengthRequired, nil)
+ } else if _, err := strconv.ParseUint(clStr, 10, 64); err != nil {
+ // stdlib won't return a negative Content-Length, but we check just in case,
+ // the most likely cause is from a missing content length, which is -1
+ return nil, caddyhttp.Error(http.StatusLengthRequired, err)
+ }
+
writer := &streamWriter{c: c}
writer.buf = bufPool.Get().(*bytes.Buffer)
writer.buf.Reset()
diff --git a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
index 3985465ba..d451dd380 100644
--- a/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
+++ b/modules/caddyhttp/reverseproxy/fastcgi/fastcgi.go
@@ -228,7 +228,7 @@ func (t Transport) buildEnv(r *http.Request) (envVars, error) {
ip = strings.Replace(ip, "]", "", 1)
// make sure file root is absolute
- root, err := filepath.Abs(repl.ReplaceAll(t.Root, "."))
+ root, err := caddy.FastAbs(repl.ReplaceAll(t.Root, "."))
if err != nil {
return nil, err
}
diff --git a/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go b/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go
index 8350096ae..347f6dfbf 100644
--- a/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go
+++ b/modules/caddyhttp/reverseproxy/forwardauth/caddyfile.go
@@ -17,6 +17,7 @@ package forwardauth
import (
"encoding/json"
"net/http"
+ "sort"
"strings"
"github.com/caddyserver/caddy/v2"
@@ -170,42 +171,66 @@ func parseCaddyfile(h httpcaddyfile.Helper) ([]httpcaddyfile.ConfigValue, error)
return nil, dispenser.Errf("the 'uri' subdirective is required")
}
- // set up handler for good responses; when a response
- // has 2xx status, then we will copy some headers from
- // the response onto the original request, and allow
- // handling to continue down the middleware chain,
- // by _not_ executing a terminal handler.
+ // Set up handler for good responses; when a response has 2xx status,
+ // then we will copy some headers from the response onto the original
+ // request, and allow handling to continue down the middleware chain,
+ // by _not_ executing a terminal handler. We must have at least one
+ // route in the response handler, even if it's no-op, so that the
+ // response handling logic in reverse_proxy doesn't skip this entry.
goodResponseHandler := caddyhttp.ResponseHandler{
Match: &caddyhttp.ResponseMatcher{
StatusCode: []int{2},
},
- Routes: []caddyhttp.Route{},
- }
-
- handler := &headers.Handler{
- Request: &headers.HeaderOps{
- Set: http.Header{},
+ Routes: []caddyhttp.Route{
+ {
+ HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
+ &caddyhttp.VarsMiddleware{},
+ "handler",
+ "vars",
+ nil,
+ )},
+ },
},
}
- // the list of headers to copy may be empty, but that's okay; we
- // need at least one handler in the routes for the response handling
- // logic in reverse_proxy to not skip this entry as empty.
- for from, to := range headersToCopy {
- handler.Request.Set.Set(to, "{http.reverse_proxy.header."+http.CanonicalHeaderKey(from)+"}")
+ // Sort the headers so that the order in the JSON output is deterministic.
+ sortedHeadersToCopy := make([]string, 0, len(headersToCopy))
+ for k := range headersToCopy {
+ sortedHeadersToCopy = append(sortedHeadersToCopy, k)
}
+ sort.Strings(sortedHeadersToCopy)
- goodResponseHandler.Routes = append(
- goodResponseHandler.Routes,
- caddyhttp.Route{
+ // Set up handlers to copy headers from the auth response onto the
+ // original request. We use vars matchers to test that the placeholder
+ // values aren't empty, because the header handler would not replace
+ // placeholders which have no value.
+ copyHeaderRoutes := []caddyhttp.Route{}
+ for _, from := range sortedHeadersToCopy {
+ to := http.CanonicalHeaderKey(headersToCopy[from])
+ placeholderName := "http.reverse_proxy.header." + http.CanonicalHeaderKey(from)
+ handler := &headers.Handler{
+ Request: &headers.HeaderOps{
+ Set: http.Header{
+ to: []string{"{" + placeholderName + "}"},
+ },
+ },
+ }
+ copyHeaderRoutes = append(copyHeaderRoutes, caddyhttp.Route{
+ MatcherSetsRaw: []caddy.ModuleMap{{
+ "not": h.JSON(caddyhttp.MatchNot{MatcherSetsRaw: []caddy.ModuleMap{{
+ "vars": h.JSON(caddyhttp.VarsMatcher{"{" + placeholderName + "}": []string{""}}),
+ }}}),
+ }},
HandlersRaw: []json.RawMessage{caddyconfig.JSONModuleObject(
handler,
"handler",
"headers",
nil,
)},
- },
- )
+ })
+ }
+
+ goodResponseHandler.Routes = append(goodResponseHandler.Routes, copyHeaderRoutes...)
// note that when a response has any other status than 2xx, then we
// use the reverse proxy's default behaviour of copying the response
diff --git a/modules/caddyhttp/reverseproxy/healthchecks.go b/modules/caddyhttp/reverseproxy/healthchecks.go
index 1735e45a4..f0ffee5b8 100644
--- a/modules/caddyhttp/reverseproxy/healthchecks.go
+++ b/modules/caddyhttp/reverseproxy/healthchecks.go
@@ -72,7 +72,7 @@ type HealthChecks struct {
// health checks (that is, health checks which occur in a
// background goroutine independently).
type ActiveHealthChecks struct {
- // DEPRECATED: Use 'uri' instead. This field will be removed. TODO: remove this field
+ // Deprecated: Use 'uri' instead. This field will be removed. TODO: remove this field
Path string `json:"path,omitempty"`
// The URI (path and query) to use for health checks
diff --git a/modules/caddyhttp/reverseproxy/httptransport.go b/modules/caddyhttp/reverseproxy/httptransport.go
index 2b4c3f094..910033ca1 100644
--- a/modules/caddyhttp/reverseproxy/httptransport.go
+++ b/modules/caddyhttp/reverseproxy/httptransport.go
@@ -545,11 +545,11 @@ type TLSConfig struct {
// Certificate authority module which provides the certificate pool of trusted certificates
CARaw json.RawMessage `json:"ca,omitempty" caddy:"namespace=tls.ca_pool.source inline_key=provider"`
- // DEPRECATED: Use the `ca` field with the `tls.ca_pool.source.inline` module instead.
+ // Deprecated: Use the `ca` field with the `tls.ca_pool.source.inline` module instead.
// Optional list of base64-encoded DER-encoded CA certificates to trust.
RootCAPool []string `json:"root_ca_pool,omitempty"`
- // DEPRECATED: Use the `ca` field with the `tls.ca_pool.source.file` module instead.
+ // Deprecated: Use the `ca` field with the `tls.ca_pool.source.file` module instead.
// List of PEM-encoded CA certificate files to add to the same trust
// store as RootCAPool (or root_ca_pool in the JSON).
RootCAPEMFiles []string `json:"root_ca_pem_files,omitempty"`
diff --git a/modules/caddyhttp/reverseproxy/reverseproxy.go b/modules/caddyhttp/reverseproxy/reverseproxy.go
index 123bf774b..f9485c570 100644
--- a/modules/caddyhttp/reverseproxy/reverseproxy.go
+++ b/modules/caddyhttp/reverseproxy/reverseproxy.go
@@ -17,6 +17,8 @@ package reverseproxy
import (
"bytes"
"context"
+ "crypto/rand"
+ "encoding/base64"
"encoding/json"
"errors"
"fmt"
@@ -108,11 +110,6 @@ type Handler struct {
// response is recognized as a streaming response, or if its
// content length is -1; for such responses, writes are flushed
// to the client immediately.
- //
- // Normally, a request will be canceled if the client disconnects
- // before the response is received from the backend. If explicitly
- // set to -1, client disconnection will be ignored and the request
- // will be completed to help facilitate low-latency streaming.
FlushInterval caddy.Duration `json:"flush_interval,omitempty"`
// A list of IP ranges (supports CIDR notation) from which
@@ -399,6 +396,23 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request, next caddyht
return caddyhttp.Error(http.StatusInternalServerError,
fmt.Errorf("preparing request for upstream round-trip: %v", err))
}
+ // websocket over http2, assuming backend doesn't support this, the request will be modified to http1.1 upgrade
+ // TODO: once we can reliably detect backend support this, it can be removed for those backends
+ if r.ProtoMajor == 2 && r.Method == http.MethodConnect && r.Header.Get(":protocol") == "websocket" {
+ clonedReq.Header.Del(":protocol")
+ // keep the body for later use. http1.1 upgrade uses http.NoBody
+ caddyhttp.SetVar(clonedReq.Context(), "h2_websocket_body", clonedReq.Body)
+ clonedReq.Body = http.NoBody
+ clonedReq.Method = http.MethodGet
+ clonedReq.Header.Set("Upgrade", "websocket")
+ clonedReq.Header.Set("Connection", "Upgrade")
+ key := make([]byte, 16)
+ _, randErr := rand.Read(key)
+ if randErr != nil {
+ return randErr
+ }
+ clonedReq.Header["Sec-WebSocket-Key"] = []string{base64.StdEncoding.EncodeToString(key)}
+ }
// we will need the original headers and Host value if
// header operations are configured; this is so that each
@@ -496,7 +510,7 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h
if proxyErr == nil {
proxyErr = caddyhttp.Error(http.StatusServiceUnavailable, errNoUpstream)
}
- if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r) {
+ if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r, h.logger) {
return true, proxyErr
}
return false, proxyErr
@@ -554,7 +568,7 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h
// ding the health status of the upstream (an error can still
// occur after the roundtrip if, for example, a response handler
// after the roundtrip returns an error)
- if succ, ok := proxyErr.(roundtripSucceeded); ok {
+ if succ, ok := proxyErr.(roundtripSucceededError); ok {
return true, succ.error
}
@@ -562,7 +576,7 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h
h.countFailure(upstream)
// if we've tried long enough, break
- if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r) {
+ if !h.LoadBalancing.tryAgain(h.ctx, start, retries, proxyErr, r, h.logger) {
return true, proxyErr
}
@@ -625,7 +639,8 @@ func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http.
if h.RequestBuffers != 0 && req.Body != nil {
var readBytes int64
req.Body, readBytes = h.bufferedBody(req.Body, h.RequestBuffers)
- if h.RequestBuffers == -1 {
+ // set Content-Length when body is fully buffered
+ if b, ok := req.Body.(bodyReadCloser); ok && b.body == nil {
req.ContentLength = readBytes
req.Header.Set("Content-Length", strconv.FormatInt(req.ContentLength, 10))
}
@@ -807,37 +822,44 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe
shouldLogCredentials := server.Logs != nil && server.Logs.ShouldLogCredentials
// Forward 1xx status codes, backported from https://github.com/golang/go/pull/53164
+ var (
+ roundTripMutex sync.Mutex
+ roundTripDone bool
+ )
trace := &httptrace.ClientTrace{
Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
+ roundTripMutex.Lock()
+ defer roundTripMutex.Unlock()
+ if roundTripDone {
+ // If RoundTrip has returned, don't try to further modify
+ // the ResponseWriter's header map.
+ return nil
+ }
h := rw.Header()
copyHeader(h, http.Header(header))
rw.WriteHeader(code)
// Clear headers coming from the backend
// (it's not automatically done by ResponseWriter.WriteHeader() for 1xx responses)
- for k := range header {
- delete(h, k)
- }
+ clear(h)
return nil
},
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))
- // if FlushInterval is explicitly configured to -1 (i.e. flush continuously to achieve
- // low-latency streaming), don't let the transport cancel the request if the client
- // disconnects: user probably wants us to finish sending the data to the upstream
- // regardless, and we should expect client disconnection in low-latency streaming
- // scenarios (see issue #4922)
- if h.FlushInterval == -1 {
- req = req.WithContext(context.WithoutCancel(req.Context()))
- }
-
- // do the round-trip; emit debug log with values we know are
- // safe, or if there is no error, emit fuller log entry
+ // do the round-trip
start := time.Now()
res, err := h.Transport.RoundTrip(req)
duration := time.Since(start)
+
+ // record that the round trip is done for the 1xx response handler
+ roundTripMutex.Lock()
+ roundTripDone = true
+ roundTripMutex.Unlock()
+
+ // emit debug log with values we know are safe,
+ // or if there is no error, emit fuller log entry
logger := h.logger.With(
zap.String("upstream", di.Upstream.String()),
zap.Duration("duration", duration),
@@ -951,10 +973,10 @@ func (h *Handler) reverseProxy(rw http.ResponseWriter, req *http.Request, origRe
res.Body.Close()
}
- // wrap any route error in roundtripSucceeded so caller knows that
+ // wrap any route error in roundtripSucceededError so caller knows that
// the roundtrip was successful and to not retry
if routeErr != nil {
- return roundtripSucceeded{routeErr}
+ return roundtripSucceededError{routeErr}
}
// we're done handling the response, and we don't want to
@@ -1073,7 +1095,7 @@ func (h *Handler) finalizeResponse(
// If true is returned, it has already blocked long enough before
// the next retry (i.e. no more sleeping is needed). If false is
// returned, the handler should stop trying to proxy the request.
-func (lb LoadBalancing) tryAgain(ctx caddy.Context, start time.Time, retries int, proxyErr error, req *http.Request) bool {
+func (lb LoadBalancing) tryAgain(ctx caddy.Context, start time.Time, retries int, proxyErr error, req *http.Request, logger *zap.Logger) bool {
// no retries are configured
if lb.TryDuration == 0 && lb.Retries == 0 {
return false
@@ -1108,7 +1130,12 @@ func (lb LoadBalancing) tryAgain(ctx caddy.Context, start time.Time, retries int
return false
}
- if !lb.RetryMatch.AnyMatch(req) {
+ match, err := lb.RetryMatch.AnyMatchWithError(req)
+ if err != nil {
+ logger.Error("error matching request for retry", zap.Error(err))
+ return false
+ }
+ if !match {
return false
}
}
@@ -1426,9 +1453,9 @@ type TLSTransport interface {
EnableTLS(base *TLSConfig) error
}
-// roundtripSucceeded is an error type that is returned if the
+// roundtripSucceededError is an error type that is returned if the
// roundtrip succeeded, but an error occurred after-the-fact.
-type roundtripSucceeded struct{ error }
+type roundtripSucceededError struct{ error }
// bodyReadCloser is a reader that, upon closing, will return
// its buffer to the pool and close the underlying body reader.
diff --git a/modules/caddyhttp/reverseproxy/selectionpolicies.go b/modules/caddyhttp/reverseproxy/selectionpolicies.go
index 293ff75e2..fcf7f90f6 100644
--- a/modules/caddyhttp/reverseproxy/selectionpolicies.go
+++ b/modules/caddyhttp/reverseproxy/selectionpolicies.go
@@ -111,8 +111,8 @@ func (r *WeightedRoundRobinSelection) UnmarshalCaddyfile(d *caddyfile.Dispenser)
if err != nil {
return d.Errf("invalid weight value '%s': %v", weight, err)
}
- if weightInt < 1 {
- return d.Errf("invalid weight value '%s': weight should be non-zero and positive", weight)
+ if weightInt < 0 {
+ return d.Errf("invalid weight value '%s': weight should be non-negative", weight)
}
r.Weights = append(r.Weights, weightInt)
}
@@ -136,8 +136,15 @@ func (r *WeightedRoundRobinSelection) Select(pool UpstreamPool, _ *http.Request,
return pool[0]
}
var index, totalWeight int
+ var weights []int
+
+ for _, w := range r.Weights {
+ if w > 0 {
+ weights = append(weights, w)
+ }
+ }
currentWeight := int(atomic.AddUint32(&r.index, 1)) % r.totalWeight
- for i, weight := range r.Weights {
+ for i, weight := range weights {
totalWeight += weight
if currentWeight < totalWeight {
index = i
@@ -145,9 +152,9 @@ func (r *WeightedRoundRobinSelection) Select(pool UpstreamPool, _ *http.Request,
}
}
- upstreams := make([]*Upstream, 0, len(r.Weights))
- for _, upstream := range pool {
- if !upstream.Available() {
+ upstreams := make([]*Upstream, 0, len(weights))
+ for i, upstream := range pool {
+ if !upstream.Available() || r.Weights[i] == 0 {
continue
}
upstreams = append(upstreams, upstream)
diff --git a/modules/caddyhttp/reverseproxy/selectionpolicies_test.go b/modules/caddyhttp/reverseproxy/selectionpolicies_test.go
index a4701ce86..580abbdde 100644
--- a/modules/caddyhttp/reverseproxy/selectionpolicies_test.go
+++ b/modules/caddyhttp/reverseproxy/selectionpolicies_test.go
@@ -131,6 +131,58 @@ func TestWeightedRoundRobinPolicy(t *testing.T) {
}
}
+func TestWeightedRoundRobinPolicyWithZeroWeight(t *testing.T) {
+ pool := testPool()
+ wrrPolicy := WeightedRoundRobinSelection{
+ Weights: []int{0, 2, 1},
+ totalWeight: 3,
+ }
+ req, _ := http.NewRequest("GET", "/", nil)
+
+ h := wrrPolicy.Select(pool, req, nil)
+ if h != pool[1] {
+ t.Error("Expected first weighted round robin host to be second host in the pool.")
+ }
+
+ h = wrrPolicy.Select(pool, req, nil)
+ if h != pool[2] {
+ t.Error("Expected second weighted round robin host to be third host in the pool.")
+ }
+
+ h = wrrPolicy.Select(pool, req, nil)
+ if h != pool[1] {
+ t.Error("Expected third weighted round robin host to be second host in the pool.")
+ }
+
+ // mark second host as down
+ pool[1].setHealthy(false)
+ h = wrrPolicy.Select(pool, req, nil)
+ if h != pool[2] {
+ t.Error("Expect select next available host.")
+ }
+
+ h = wrrPolicy.Select(pool, req, nil)
+ if h != pool[2] {
+ t.Error("Expect select only available host.")
+ }
+ // mark second host as up
+ pool[1].setHealthy(true)
+
+ h = wrrPolicy.Select(pool, req, nil)
+ if h != pool[1] {
+ t.Error("Expect select first host on availability.")
+ }
+
+ // test next select in full cycle
+ expected := []*Upstream{pool[1], pool[2], pool[1], pool[1], pool[2], pool[1]}
+ for i, want := range expected {
+ got := wrrPolicy.Select(pool, req, nil)
+ if want != got {
+ t.Errorf("Selection %d: got host[%s], want host[%s]", i+1, got, want)
+ }
+ }
+}
+
func TestLeastConnPolicy(t *testing.T) {
pool := testPool()
lcPolicy := LeastConnSelection{}
diff --git a/modules/caddyhttp/reverseproxy/streaming.go b/modules/caddyhttp/reverseproxy/streaming.go
index 3fde10b35..d697eb402 100644
--- a/modules/caddyhttp/reverseproxy/streaming.go
+++ b/modules/caddyhttp/reverseproxy/streaming.go
@@ -19,6 +19,7 @@
package reverseproxy
import (
+ "bufio"
"context"
"errors"
"fmt"
@@ -33,8 +34,29 @@ import (
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"golang.org/x/net/http/httpguts"
+
+ "github.com/caddyserver/caddy/v2/modules/caddyhttp"
)
+type h2ReadWriteCloser struct {
+ io.ReadCloser
+ http.ResponseWriter
+}
+
+func (rwc h2ReadWriteCloser) Write(p []byte) (n int, err error) {
+ n, err = rwc.ResponseWriter.Write(p)
+ if err != nil {
+ return 0, err
+ }
+
+ //nolint:bodyclose
+ err = http.NewResponseController(rwc.ResponseWriter).Flush()
+ if err != nil {
+ return 0, err
+ }
+ return n, nil
+}
+
func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup, rw http.ResponseWriter, req *http.Request, res *http.Response) {
reqUpType := upgradeType(req.Header)
resUpType := upgradeType(res.Header)
@@ -67,24 +89,58 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup,
// like the rest of handler chain.
copyHeader(rw.Header(), res.Header)
normalizeWebsocketHeaders(rw.Header())
- rw.WriteHeader(res.StatusCode)
- logger.Debug("upgrading connection")
+ var (
+ conn io.ReadWriteCloser
+ brw *bufio.ReadWriter
+ )
+ // websocket over http2, assuming backend doesn't support this, the request will be modified to http1.1 upgrade
+ // TODO: once we can reliably detect backend support this, it can be removed for those backends
+ if body, ok := caddyhttp.GetVar(req.Context(), "h2_websocket_body").(io.ReadCloser); ok {
+ req.Body = body
+ rw.Header().Del("Upgrade")
+ rw.Header().Del("Connection")
+ delete(rw.Header(), "Sec-WebSocket-Accept")
+ rw.WriteHeader(http.StatusOK)
+
+ if c := logger.Check(zap.DebugLevel, "upgrading connection"); c != nil {
+ c.Write(zap.Int("http_version", 2))
+ }
- //nolint:bodyclose
- conn, brw, hijackErr := http.NewResponseController(rw).Hijack()
- if errors.Is(hijackErr, http.ErrNotSupported) {
- if c := logger.Check(zapcore.ErrorLevel, "can't switch protocols using non-Hijacker ResponseWriter"); c != nil {
- c.Write(zap.String("type", fmt.Sprintf("%T", rw)))
+ //nolint:bodyclose
+ flushErr := http.NewResponseController(rw).Flush()
+ if flushErr != nil {
+ if c := h.logger.Check(zap.ErrorLevel, "failed to flush http2 websocket response"); c != nil {
+ c.Write(zap.Error(flushErr))
+ }
+ return
}
- return
- }
+ conn = h2ReadWriteCloser{req.Body, rw}
+ // bufio is not needed, use minimal buffer
+ brw = bufio.NewReadWriter(bufio.NewReaderSize(conn, 1), bufio.NewWriterSize(conn, 1))
+ } else {
+ rw.WriteHeader(res.StatusCode)
- if hijackErr != nil {
- if c := logger.Check(zapcore.ErrorLevel, "hijack failed on protocol switch"); c != nil {
- c.Write(zap.Error(hijackErr))
+ if c := logger.Check(zap.DebugLevel, "upgrading connection"); c != nil {
+ c.Write(zap.Int("http_version", req.ProtoMajor))
+ }
+
+ var hijackErr error
+ //nolint:bodyclose
+ conn, brw, hijackErr = http.NewResponseController(rw).Hijack()
+ if errors.Is(hijackErr, http.ErrNotSupported) {
+ if c := h.logger.Check(zap.ErrorLevel, "can't switch protocols using non-Hijacker ResponseWriter"); c != nil {
+ c.Write(zap.String("type", fmt.Sprintf("%T", rw)))
+ }
+ return
+ }
+
+ if hijackErr != nil {
+ if c := h.logger.Check(zap.ErrorLevel, "hijack failed on protocol switch"); c != nil {
+ c.Write(zap.Error(hijackErr))
+ }
+ return
}
- return
}
// adopted from https://github.com/golang/go/commit/8bcf2834afdf6a1f7937390903a41518715ef6f5
@@ -103,7 +159,7 @@ func (h *Handler) handleUpgradeResponse(logger *zap.Logger, wg *sync.WaitGroup,
start := time.Now()
defer func() {
conn.Close()
- if c := logger.Check(zapcore.DebugLevel, "hijack failed on protocol switch"); c != nil {
+ if c := logger.Check(zapcore.DebugLevel, "connection closed"); c != nil {
c.Write(zap.Duration("duration", time.Since(start)))
}
}()
diff --git a/modules/caddyhttp/rewrite/caddyfile.go b/modules/caddyhttp/rewrite/caddyfile.go
index 89f44c79b..5f9b97adf 100644
--- a/modules/caddyhttp/rewrite/caddyfile.go
+++ b/modules/caddyhttp/rewrite/caddyfile.go
@@ -110,9 +110,6 @@ func parseCaddyfileURI(h httpcaddyfile.Helper) (caddyhttp.MiddlewareHandler, err
return nil, h.ArgErr()
}
rewr.StripPathPrefix = args[1]
- if !strings.HasPrefix(rewr.StripPathPrefix, "/") {
- rewr.StripPathPrefix = "/" + rewr.StripPathPrefix
- }
case "strip_suffix":
if len(args) != 2 {
diff --git a/modules/caddyhttp/rewrite/rewrite.go b/modules/caddyhttp/rewrite/rewrite.go
index e76682729..31ebfb430 100644
--- a/modules/caddyhttp/rewrite/rewrite.go
+++ b/modules/caddyhttp/rewrite/rewrite.go
@@ -259,6 +259,9 @@ func (rewr Rewrite) Rewrite(r *http.Request, repl *caddy.Replacer) bool {
// strip path prefix or suffix
if rewr.StripPathPrefix != "" {
prefix := repl.ReplaceAll(rewr.StripPathPrefix, "")
+ if !strings.HasPrefix(prefix, "/") {
+ prefix = "/" + prefix
+ }
mergeSlashes := !strings.Contains(prefix, "//")
changePath(r, func(escapedPath string) string {
escapedPath = caddyhttp.CleanPath(escapedPath, mergeSlashes)
diff --git a/modules/caddyhttp/rewrite/rewrite_test.go b/modules/caddyhttp/rewrite/rewrite_test.go
index aaa142bc2..81360baee 100644
--- a/modules/caddyhttp/rewrite/rewrite_test.go
+++ b/modules/caddyhttp/rewrite/rewrite_test.go
@@ -236,6 +236,11 @@ func TestRewrite(t *testing.T) {
expect: newRequest(t, "GET", "/foo/bar"),
},
{
+ rule: Rewrite{StripPathPrefix: "prefix"},
+ input: newRequest(t, "GET", "/prefix/foo/bar"),
+ expect: newRequest(t, "GET", "/foo/bar"),
+ },
+ {
rule: Rewrite{StripPathPrefix: "/prefix"},
input: newRequest(t, "GET", "/prefix"),
expect: newRequest(t, "GET", ""),
diff --git a/modules/caddyhttp/routes.go b/modules/caddyhttp/routes.go
index 939d01e55..ccb5f2515 100644
--- a/modules/caddyhttp/routes.go
+++ b/modules/caddyhttp/routes.go
@@ -254,18 +254,13 @@ func wrapRoute(route Route) Middleware {
nextCopy := next
// route must match at least one of the matcher sets
- if !route.MatcherSets.AnyMatch(req) {
+ matches, err := route.MatcherSets.AnyMatchWithError(req)
+ if err != nil {
// allow matchers the opportunity to short circuit
// the request and trigger the error handling chain
- err, ok := GetVar(req.Context(), MatcherErrorVarKey).(error)
- if ok {
- // clear out the error from context, otherwise
- // it will cascade to the error routes (#4916)
- SetVar(req.Context(), MatcherErrorVarKey, nil)
- // return the matcher's error
- return err
- }
-
+ return err
+ }
+ if !matches {
// call the next handler, and skip this one,
// since the matcher didn't match
return nextCopy.ServeHTTP(rw, req)
@@ -341,19 +336,58 @@ func wrapMiddleware(ctx caddy.Context, mh MiddlewareHandler, metrics *Metrics) M
// MatcherSet is a set of matchers which
// must all match in order for the request
// to be matched successfully.
-type MatcherSet []RequestMatcher
+type MatcherSet []any
// Match returns true if the request matches all
// matchers in mset or if there are no matchers.
func (mset MatcherSet) Match(r *http.Request) bool {
for _, m := range mset {
- if !m.Match(r) {
- return false
+ if me, ok := m.(RequestMatcherWithError); ok {
+ match, _ := me.MatchWithError(r)
+ if !match {
+ return false
+ }
+ continue
+ }
+ if me, ok := m.(RequestMatcher); ok {
+ if !me.Match(r) {
+ return false
+ }
+ continue
}
+ return false
}
return true
}
+// MatchWithError returns true if r matches m.
+func (mset MatcherSet) MatchWithError(r *http.Request) (bool, error) {
+ for _, m := range mset {
+ if me, ok := m.(RequestMatcherWithError); ok {
+ match, err := me.MatchWithError(r)
+ if err != nil || !match {
+ return match, err
+ }
+ continue
+ }
+ if me, ok := m.(RequestMatcher); ok {
+ if !me.Match(r) {
+ // for backwards compatibility
+ err, ok := GetVar(r.Context(), MatcherErrorVarKey).(error)
+ if ok {
+ // clear out the error from context since we've consumed it
+ SetVar(r.Context(), MatcherErrorVarKey, nil)
+ return false, err
+ }
+ return false, nil
+ }
+ continue
+ }
+ return false, fmt.Errorf("matcher is not a RequestMatcher or RequestMatcherWithError: %#v", m)
+ }
+ return true, nil
+}
+
// RawMatcherSets is a group of matcher sets
// in their raw, JSON form.
type RawMatcherSets []caddy.ModuleMap
@@ -366,25 +400,50 @@ type MatcherSets []MatcherSet
// AnyMatch returns true if req matches any of the
// matcher sets in ms or if there are no matchers,
// in which case the request always matches.
+//
+// Deprecated: Use AnyMatchWithError instead.
func (ms MatcherSets) AnyMatch(req *http.Request) bool {
for _, m := range ms {
- if m.Match(req) {
- return true
+ match, err := m.MatchWithError(req)
+ if err != nil {
+ SetVar(req.Context(), MatcherErrorVarKey, err)
+ return false
+ }
+ if match {
+ return match
}
}
return len(ms) == 0
}
+// AnyMatchWithError returns true if req matches any of the
+// matcher sets in ms or if there are no matchers, in which
+// case the request always matches. If any matcher returns
+// an error, we cut short and return the error.
+func (ms MatcherSets) AnyMatchWithError(req *http.Request) (bool, error) {
+ for _, m := range ms {
+ match, err := m.MatchWithError(req)
+ if err != nil || match {
+ return match, err
+ }
+ }
+ return len(ms) == 0, nil
+}
+
// FromInterface fills ms from an 'any' value obtained from LoadModule.
func (ms *MatcherSets) FromInterface(matcherSets any) error {
for _, matcherSetIfaces := range matcherSets.([]map[string]any) {
var matcherSet MatcherSet
for _, matcher := range matcherSetIfaces {
- reqMatcher, ok := matcher.(RequestMatcher)
- if !ok {
- return fmt.Errorf("decoded module is not a RequestMatcher: %#v", matcher)
+ if m, ok := matcher.(RequestMatcherWithError); ok {
+ matcherSet = append(matcherSet, m)
+ continue
+ }
+ if m, ok := matcher.(RequestMatcher); ok {
+ matcherSet = append(matcherSet, m)
+ continue
}
- matcherSet = append(matcherSet, reqMatcher)
+ return fmt.Errorf("decoded module is not a RequestMatcher or RequestMatcherWithError: %#v", matcher)
}
*ms = append(*ms, matcherSet)
}
diff --git a/modules/caddyhttp/server.go b/modules/caddyhttp/server.go
index 5aa7e0f63..12c032dee 100644
--- a/modules/caddyhttp/server.go
+++ b/modules/caddyhttp/server.go
@@ -61,6 +61,7 @@ type Server struct {
ReadTimeout caddy.Duration `json:"read_timeout,omitempty"`
// ReadHeaderTimeout is like ReadTimeout but for request headers.
+ // Default is 1 minute.
ReadHeaderTimeout caddy.Duration `json:"read_header_timeout,omitempty"`
// WriteTimeout is how long to allow a write to a client. Note
@@ -226,6 +227,7 @@ type Server struct {
// If set, metrics observations will be enabled.
// This setting is EXPERIMENTAL and subject to change.
+ // DEPRECATED: Use the app-level `metrics` field.
Metrics *Metrics `json:"metrics,omitempty"`
name string
@@ -614,22 +616,7 @@ func (s *Server) serveHTTP3(addr caddy.NetworkAddress, tlsCfg *tls.Config) error
// create HTTP/3 server if not done already
if s.h3server == nil {
s.h3server = &http3.Server{
- // Currently when closing a http3.Server, only listeners are closed. But caddy reuses these listeners
- // if possible, requests are still read and handled by the old handler. Close these connections manually.
- // see issue: https://github.com/caddyserver/caddy/issues/6195
- // Will interrupt ongoing requests.
- // TODO: remove the handler wrap after http3.Server.CloseGracefully is implemented, see App.Stop
- Handler: http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
- select {
- case <-s.ctx.Done():
- if quicConn, ok := request.Context().Value(quicConnCtxKey).(quic.Connection); ok {
- //nolint:errcheck
- quicConn.CloseWithError(quic.ApplicationErrorCode(http3.ErrCodeRequestRejected), "")
- }
- default:
- s.ServeHTTP(writer, request)
- }
- }),
+ Handler: s,
TLSConfig: tlsCfg,
MaxHeaderBytes: s.MaxHeaderBytes,
QUICConfig: &quic.Config{
@@ -637,9 +624,6 @@ func (s *Server) serveHTTP3(addr caddy.NetworkAddress, tlsCfg *tls.Config) error
Tracer: qlog.DefaultConnectionTracer,
},
IdleTimeout: time.Duration(s.IdleTimeout),
- ConnContext: func(ctx context.Context, c quic.Connection) context.Context {
- return context.WithValue(ctx, quicConnCtxKey, c)
- },
}
}
@@ -1099,10 +1083,6 @@ const (
// For referencing underlying net.Conn
ConnCtxKey caddy.CtxKey = "conn"
- // For referencing underlying quic.Connection
- // TODO: export if needed later
- quicConnCtxKey caddy.CtxKey = "quic_conn"
-
// For tracking whether the client is a trusted proxy
TrustedProxyVarKey string = "trusted_proxy"
diff --git a/modules/caddyhttp/server_test.go b/modules/caddyhttp/server_test.go
index 2c8033d45..53f35368f 100644
--- a/modules/caddyhttp/server_test.go
+++ b/modules/caddyhttp/server_test.go
@@ -69,12 +69,13 @@ func TestServer_LogRequest(t *testing.T) {
}`, buf.String())
}
-func TestServer_LogRequest_WithTraceID(t *testing.T) {
+func TestServer_LogRequest_WithTrace(t *testing.T) {
s := &Server{}
extra := new(ExtraLogFields)
ctx := context.WithValue(context.Background(), ExtraLogFieldsCtxKey, extra)
extra.Add(zap.String("traceID", "1234567890abcdef"))
+ extra.Add(zap.String("spanID", "12345678"))
req := httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ctx)
rec := httptest.NewRecorder()
@@ -93,7 +94,8 @@ func TestServer_LogRequest_WithTraceID(t *testing.T) {
"msg":"handled request", "level":"info", "bytes_read":0,
"duration":"50ms", "resp_headers": {}, "size":0,
"status":0, "user_id":"",
- "traceID":"1234567890abcdef"
+ "traceID":"1234567890abcdef",
+ "spanID":"12345678"
}`, buf.String())
}
@@ -144,12 +146,13 @@ func BenchmarkServer_LogRequest_NopLogger(b *testing.B) {
}
}
-func BenchmarkServer_LogRequest_WithTraceID(b *testing.B) {
+func BenchmarkServer_LogRequest_WithTrace(b *testing.B) {
s := &Server{}
extra := new(ExtraLogFields)
ctx := context.WithValue(context.Background(), ExtraLogFieldsCtxKey, extra)
extra.Add(zap.String("traceID", "1234567890abcdef"))
+ extra.Add(zap.String("spanID", "12345678"))
req := httptest.NewRequest(http.MethodGet, "/", nil).WithContext(ctx)
rec := httptest.NewRecorder()
diff --git a/modules/caddyhttp/tracing/tracer.go b/modules/caddyhttp/tracing/tracer.go
index 89c617bf4..261952aa6 100644
--- a/modules/caddyhttp/tracing/tracer.go
+++ b/modules/caddyhttp/tracing/tracer.go
@@ -88,11 +88,15 @@ func (ot *openTelemetryWrapper) serveHTTP(w http.ResponseWriter, r *http.Request
spanCtx := trace.SpanContextFromContext(ctx)
if spanCtx.IsValid() {
traceID := spanCtx.TraceID().String()
+ spanID := spanCtx.SpanID().String()
// Add a trace_id placeholder, accessible via `{http.vars.trace_id}`.
caddyhttp.SetVar(ctx, "trace_id", traceID)
- // Add the trace id to the log fields for the request.
+ // Add a span_id placeholder, accessible via `{http.vars.span_id}`.
+ caddyhttp.SetVar(ctx, "span_id", spanID)
+ // Add the traceID and spanID to the log fields for the request.
if extra, ok := ctx.Value(caddyhttp.ExtraLogFieldsCtxKey).(*caddyhttp.ExtraLogFields); ok {
extra.Add(zap.String("traceID", traceID))
+ extra.Add(zap.String("spanID", spanID))
}
}
next := ctx.Value(nextCallCtxKey).(*nextCall)
diff --git a/modules/caddyhttp/vars.go b/modules/caddyhttp/vars.go
index 77e06e3cb..7ab891fc0 100644
--- a/modules/caddyhttp/vars.go
+++ b/modules/caddyhttp/vars.go
@@ -166,8 +166,14 @@ func (m *VarsMatcher) UnmarshalCaddyfile(d *caddyfile.Dispenser) error {
// Match matches a request based on variables in the context,
// or placeholders if the key is not a variable.
func (m VarsMatcher) Match(r *http.Request) bool {
+ match, _ := m.MatchWithError(r)
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+func (m VarsMatcher) MatchWithError(r *http.Request) (bool, error) {
if len(m) == 0 {
- return true
+ return true, nil
}
vars := r.Context().Value(VarsCtxKey).(map[string]any)
@@ -200,11 +206,11 @@ func (m VarsMatcher) Match(r *http.Request) bool {
varStr = fmt.Sprintf("%v", vv)
}
if varStr == matcherValExpanded {
- return true
+ return true, nil
}
}
}
- return false
+ return false, nil
}
// CELLibrary produces options that expose this matcher for use in CEL
@@ -219,7 +225,7 @@ func (VarsMatcher) CELLibrary(_ caddy.Context) (cel.Library, error) {
"vars",
"vars_matcher_request_map",
[]*cel.Type{CELTypeJSON},
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
mapStrListStr, err := CELValueToMapStrList(data)
if err != nil {
return nil, err
@@ -294,6 +300,12 @@ func (m MatchVarsRE) Provision(ctx caddy.Context) error {
// Match returns true if r matches m.
func (m MatchVarsRE) Match(r *http.Request) bool {
+ match, _ := m.MatchWithError(r)
+ return match
+}
+
+// MatchWithError returns true if r matches m.
+func (m MatchVarsRE) MatchWithError(r *http.Request) (bool, error) {
vars := r.Context().Value(VarsCtxKey).(map[string]any)
repl := r.Context().Value(caddy.ReplacerCtxKey).(*caddy.Replacer)
for key, val := range m {
@@ -322,10 +334,10 @@ func (m MatchVarsRE) Match(r *http.Request) bool {
valExpanded := repl.ReplaceAll(varStr, "")
if match := val.Match(valExpanded, repl); match {
- return match
+ return match, nil
}
}
- return false
+ return false, nil
}
// CELLibrary produces options that expose this matcher for use in CEL
@@ -340,7 +352,7 @@ func (MatchVarsRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
"vars_regexp",
"vars_regexp_request_string_string",
[]*cel.Type{cel.StringType, cel.StringType},
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
refStringList := reflect.TypeOf([]string{})
params, err := data.ConvertToNative(refStringList)
if err != nil {
@@ -363,7 +375,7 @@ func (MatchVarsRE) CELLibrary(ctx caddy.Context) (cel.Library, error) {
"vars_regexp",
"vars_regexp_request_string_string_string",
[]*cel.Type{cel.StringType, cel.StringType, cel.StringType},
- func(data ref.Val) (RequestMatcher, error) {
+ func(data ref.Val) (RequestMatcherWithError, error) {
refStringList := reflect.TypeOf([]string{})
params, err := data.ConvertToNative(refStringList)
if err != nil {
@@ -435,8 +447,10 @@ func SetVar(ctx context.Context, key string, value any) {
// Interface guards
var (
- _ MiddlewareHandler = (*VarsMiddleware)(nil)
- _ caddyfile.Unmarshaler = (*VarsMiddleware)(nil)
- _ RequestMatcher = (*VarsMatcher)(nil)
- _ caddyfile.Unmarshaler = (*VarsMatcher)(nil)
+ _ MiddlewareHandler = (*VarsMiddleware)(nil)
+ _ caddyfile.Unmarshaler = (*VarsMiddleware)(nil)
+ _ RequestMatcherWithError = (*VarsMatcher)(nil)
+ _ caddyfile.Unmarshaler = (*VarsMatcher)(nil)
+ _ RequestMatcherWithError = (*MatchVarsRE)(nil)
+ _ caddyfile.Unmarshaler = (*MatchVarsRE)(nil)
)