From 2fd29a64cbc128be315c6db90f70459e08d89943 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 31 Oct 2026 05:39:02 +0000 Subject: [PATCH] Bump github.com/go-chi/chi/v5 from 5.2.3 to 5.4.0 Bumps [github.com/go-chi/chi/v5](https://github.com/go-chi/chi) from 5.2.3 to 5.4.0. - [Release notes](https://github.com/go-chi/chi/releases) - [Changelog](https://github.com/go-chi/chi/blob/master/CHANGELOG.md) - [Commits](https://github.com/go-chi/chi/compare/v5.2.3...v5.4.0) --- updated-dependencies: - dependency-name: github.com/go-chi/chi/v5 dependency-version: 5.4.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- go.mod | 4 +- go.sum | 4 +- vendor/github.com/go-chi/chi/v5/README.md | 150 ++++++-- vendor/github.com/go-chi/chi/v5/chi.go | 21 +- vendor/github.com/go-chi/chi/v5/context.go | 38 +- .../go-chi/chi/v5/middleware/client_ip.go | 341 ++++++++++++++++++ .../go-chi/chi/v5/middleware/compress.go | 36 +- .../chi/v5/middleware/content_charset.go | 15 +- .../go-chi/chi/v5/middleware/logger.go | 22 +- .../go-chi/chi/v5/middleware/profiler.go | 1 - .../go-chi/chi/v5/middleware/realip.go | 17 +- .../go-chi/chi/v5/middleware/recoverer.go | 14 +- .../go-chi/chi/v5/middleware/request_id.go | 6 +- .../go-chi/chi/v5/middleware/route_headers.go | 1 + .../go-chi/chi/v5/middleware/strip.go | 11 +- .../go-chi/chi/v5/middleware/terminal.go | 2 +- .../go-chi/chi/v5/middleware/value.go | 2 +- .../go-chi/chi/v5/middleware/wrap_writer.go | 32 +- vendor/github.com/go-chi/chi/v5/mux.go | 26 +- vendor/github.com/go-chi/chi/v5/path_value.go | 21 -- .../go-chi/chi/v5/path_value_fallback.go | 19 - vendor/github.com/go-chi/chi/v5/pattern.go | 16 - .../go-chi/chi/v5/pattern_fallback.go | 17 - vendor/github.com/go-chi/chi/v5/tree.go | 100 +++-- vendor/modules.txt | 4 +- 25 files changed, 694 insertions(+), 226 deletions(-) create mode 100644 vendor/github.com/go-chi/chi/v5/middleware/client_ip.go delete mode 100644 vendor/github.com/go-chi/chi/v5/path_value.go delete mode 100644 vendor/github.com/go-chi/chi/v5/path_value_fallback.go delete mode 100644 vendor/github.com/go-chi/chi/v5/pattern.go delete mode 100644 vendor/github.com/go-chi/chi/v5/pattern_fallback.go diff --git a/go.mod b/go.mod index 9461a2f9a..f62c01f2f 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,8 @@ go 1.25.0 require ( github.com/cyphar/filepath-securejoin v0.6.1 github.com/dustin/go-humanize v1.0.1 - github.com/go-chi/chi/v5 v5.2.3 + github.com/go-chi/chi/v5 v5.4.0 + github.com/go-logr/logr v1.4.3 github.com/google/go-containerregistry v0.20.7 github.com/json-iterator/go v1.1.12 github.com/nats-io/nats.go v1.48.0 @@ -95,7 +96,6 @@ require ( github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/go-errors/errors v1.4.2 // indirect - github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-openapi/jsonpointer v0.22.1 // indirect github.com/go-openapi/jsonreference v0.21.2 // indirect diff --git a/go.sum b/go.sum index 979217110..62801fa9e 100644 --- a/go.sum +++ b/go.sum @@ -179,8 +179,8 @@ github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sa github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= -github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-chi/chi/v5 v5.4.0 h1:+ufzvFTGKyin26Or5DLrlIDq4ygIDBVcj7oVNeeaYFE= +github.com/go-chi/chi/v5 v5.4.0/go.mod h1:d1IG7GtKPSjPyBP365PH8sAGVHk3qfB/IhjFNRH1wWM= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= diff --git a/vendor/github.com/go-chi/chi/v5/README.md b/vendor/github.com/go-chi/chi/v5/README.md index c58a0e20c..5f9f44356 100644 --- a/vendor/github.com/go-chi/chi/v5/README.md +++ b/vendor/github.com/go-chi/chi/v5/README.md @@ -87,7 +87,7 @@ func main() { // A good base middleware stack r.Use(middleware.RequestID) - r.Use(middleware.RealIP) + r.Use(middleware.ClientIPFromRemoteAddr) // pick one ClientIPFrom* based on your infra, see below r.Use(middleware.Logger) r.Use(middleware.Recoverer) @@ -221,6 +221,7 @@ type Router interface { Patch(pattern string, h http.HandlerFunc) Post(pattern string, h http.HandlerFunc) Put(pattern string, h http.HandlerFunc) + Query(pattern string, h http.HandlerFunc) Trace(pattern string, h http.HandlerFunc) // NotFound defines a handler to respond whenever a route could @@ -349,7 +350,11 @@ with `net/http` can be used with chi's mux. | [Logger] | Logs the start and end of each request with the elapsed processing time | | [NoCache] | Sets response headers to prevent clients from caching | | [Profiler] | Easily attach net/http/pprof to your routers | -| [RealIP] | Sets a http.Request's RemoteAddr to either X-Real-IP or X-Forwarded-For | +| [ClientIPFromHeader] | Capture client IP from a trusted single-IP header (X-Real-IP, CF-Connecting-IP, ...) | +| [ClientIPFromXFF] | Capture client IP from X-Forwarded-For, skipping listed trusted CIDR prefixes | +| [ClientIPFromXFFTrustedProxies] | Capture client IP from X-Forwarded-For given a fixed number of trusted proxies | +| [ClientIPFromRemoteAddr] | Capture client IP from the TCP RemoteAddr (server directly on the public internet) | +| [RealIP] | Deprecated — vulnerable to IP spoofing; use [ClientIPFromXFF] or another ClientIPFrom\* middleware | | [Recoverer] | Gracefully absorb panics and prints the stack trace | | [RequestID] | Injects a request ID into the context of each request | | [RedirectSlashes] | Redirect slashes on routing paths | @@ -363,44 +368,111 @@ with `net/http` can be used with chi's mux. | [WithValue] | Short-hand middleware to set a key/value on the request context | ---------------------------------------------------------------------------------------------------- -[AllowContentEncoding]: https://pkg.go.dev/github.com/go-chi/chi/middleware#AllowContentEncoding -[AllowContentType]: https://pkg.go.dev/github.com/go-chi/chi/middleware#AllowContentType -[BasicAuth]: https://pkg.go.dev/github.com/go-chi/chi/middleware#BasicAuth -[Compress]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Compress -[ContentCharset]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ContentCharset -[CleanPath]: https://pkg.go.dev/github.com/go-chi/chi/middleware#CleanPath -[GetHead]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetHead -[GetReqID]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetReqID -[Heartbeat]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Heartbeat -[Logger]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Logger -[NoCache]: https://pkg.go.dev/github.com/go-chi/chi/middleware#NoCache -[Profiler]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Profiler -[RealIP]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RealIP -[Recoverer]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Recoverer -[RedirectSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RedirectSlashes -[RequestLogger]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RequestLogger -[RequestID]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RequestID -[RouteHeaders]: https://pkg.go.dev/github.com/go-chi/chi/middleware#RouteHeaders -[SetHeader]: https://pkg.go.dev/github.com/go-chi/chi/middleware#SetHeader -[StripSlashes]: https://pkg.go.dev/github.com/go-chi/chi/middleware#StripSlashes +[AllowContentEncoding]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#AllowContentEncoding +[AllowContentType]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#AllowContentType +[BasicAuth]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#BasicAuth +[Compress]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#Compress +[ContentCharset]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#ContentCharset +[CleanPath]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#CleanPath +[GetHead]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#GetHead +[GetReqID]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#GetReqID +[Heartbeat]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#Heartbeat +[Logger]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#Logger +[NoCache]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#NoCache +[Profiler]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#Profiler +[ClientIPFromHeader]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#ClientIPFromHeader +[ClientIPFromXFF]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#ClientIPFromXFF +[ClientIPFromXFFTrustedProxies]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#ClientIPFromXFFTrustedProxies +[ClientIPFromRemoteAddr]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#ClientIPFromRemoteAddr +[GetClientIP]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#GetClientIP +[GetClientIPAddr]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#GetClientIPAddr +[RealIP]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#RealIP +[Recoverer]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#Recoverer +[RedirectSlashes]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#RedirectSlashes +[RequestLogger]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#RequestLogger +[RequestID]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#RequestID +[RouteHeaders]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#RouteHeaders +[SetHeader]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#SetHeader +[StripSlashes]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#StripSlashes [Sunset]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#Sunset -[Throttle]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Throttle -[ThrottleBacklog]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleBacklog -[ThrottleWithOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleWithOpts -[Timeout]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Timeout -[URLFormat]: https://pkg.go.dev/github.com/go-chi/chi/middleware#URLFormat -[WithLogEntry]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WithLogEntry -[WithValue]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WithValue -[Compressor]: https://pkg.go.dev/github.com/go-chi/chi/middleware#Compressor -[DefaultLogFormatter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#DefaultLogFormatter -[EncoderFunc]: https://pkg.go.dev/github.com/go-chi/chi/middleware#EncoderFunc -[HeaderRoute]: https://pkg.go.dev/github.com/go-chi/chi/middleware#HeaderRoute -[HeaderRouter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#HeaderRouter -[LogEntry]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LogEntry -[LogFormatter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LogFormatter -[LoggerInterface]: https://pkg.go.dev/github.com/go-chi/chi/middleware#LoggerInterface -[ThrottleOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleOpts -[WrapResponseWriter]: https://pkg.go.dev/github.com/go-chi/chi/middleware#WrapResponseWriter +[Throttle]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#Throttle +[ThrottleBacklog]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#ThrottleBacklog +[ThrottleWithOpts]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#ThrottleWithOpts +[Timeout]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#Timeout +[URLFormat]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#URLFormat +[WithLogEntry]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#WithLogEntry +[WithValue]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#WithValue +[Compressor]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#Compressor +[DefaultLogFormatter]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#DefaultLogFormatter +[EncoderFunc]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#EncoderFunc +[HeaderRoute]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#HeaderRoute +[HeaderRouter]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#HeaderRouter +[LogEntry]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#LogEntry +[LogFormatter]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#LogFormatter +[LoggerInterface]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#LoggerInterface +[ThrottleOpts]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#ThrottleOpts +[WrapResponseWriter]: https://pkg.go.dev/github.com/go-chi/chi/v5/middleware#WrapResponseWriter + +### Choosing a ClientIP middleware + +The legacy [RealIP] middleware is deprecated — it is vulnerable to IP spoofing +(GHSA-3fxj-6jh8-hvhx, GHSA-rjr7-jggh-pgcp, GHSA-9g5q-2w5x-hmxf) and mutates +`r.RemoteAddr`. Use one of the four `ClientIPFrom*` middlewares instead — pick +exactly one based on your network setup — and read the resulting IP with +[GetClientIP] (string) or [GetClientIPAddr] (`netip.Addr`): + +| Your setup | Use | +|---|---| +| Directly on the public internet, no proxy | `middleware.ClientIPFromRemoteAddr` | +| Behind nginx (`X-Real-IP`), Cloudflare (`CF-Connecting-IP`), Apache (`X-Client-IP`) | `middleware.ClientIPFromHeader("")` | +| Behind one or more proxies whose IP ranges you can list | `middleware.ClientIPFromXFF("10.0.0.0/8", ...)` | +| Behind a known, fixed number of proxies with dynamic IPs | `middleware.ClientIPFromXFFTrustedProxies(2)` | + +```go +r := chi.NewRouter() +r.Use(middleware.RequestID) + +// Pick exactly one. Examples for common deployments: + +// Direct internet exposure (no proxy): +// r.Use(middleware.ClientIPFromRemoteAddr) + +// Behind Cloudflare: +// r.Use(middleware.ClientIPFromHeader("CF-Connecting-IP")) + +// Behind AWS CloudFront (or any proxy fleet with known CIDRs): +r.Use(middleware.ClientIPFromXFF( + "13.32.0.0/15", // CloudFront IPv4 + "52.46.0.0/18", // CloudFront IPv4 + "2600:9000::/28", // CloudFront IPv6 +)) + +// Behind a known number of proxies with dynamic IPs: +// r.Use(middleware.ClientIPFromXFFTrustedProxies(2)) + +r.Use(middleware.Logger) +r.Use(middleware.Recoverer) + +r.Get("/", func(w http.ResponseWriter, r *http.Request) { + clientIP := middleware.GetClientIP(r.Context()) // for logs, rate-limit keys, etc. + _ = clientIP +}) +``` + +These middlewares never mutate `r.RemoteAddr`. They store a normalized +`netip.Addr` in the request context — IPv4-mapped IPv6 (`::ffff:a.b.c.d`) +is folded to plain IPv4, and IPv6 zone identifiers carried in headers are +stripped, so one logical client maps to a single canonical key for logs, +rate limits, and ACLs. + +For `ClientIPFromXFFTrustedProxies`, `numTrustedProxies` is the total proxy +hops between client and server. Prefer `ClientIPFromXFF` with explicit CIDRs +when you can — CIDR-based trust cannot off-by-one. See the godoc for a +deployment recipe and a verify checklist. + +See the per-function godoc for the full semantics of each middleware, and +[adam-p's "The perils of the 'real' client IP"](https://adam-p.ca/blog/2022/03/x-forwarded-for/) +for the underlying threat model. ### Extra middlewares & packages diff --git a/vendor/github.com/go-chi/chi/v5/chi.go b/vendor/github.com/go-chi/chi/v5/chi.go index 2b6ebd337..6911bb551 100644 --- a/vendor/github.com/go-chi/chi/v5/chi.go +++ b/vendor/github.com/go-chi/chi/v5/chi.go @@ -1,8 +1,8 @@ // Package chi is a small, idiomatic and composable router for building HTTP services. // -// chi requires Go 1.14 or newer. +// chi supports the four most recent major versions of Go. // -// Example: +// # Example // // package main // @@ -25,11 +25,13 @@ // http.ListenAndServe(":3333", r) // } // -// See github.com/go-chi/chi/_examples/ for more in-depth examples. +// See [github.com/go-chi/chi/v5/_examples] for more in-depth examples. +// +// # URL patterns // // URL patterns allow for easy matching of path components in HTTP // requests. The matching components can then be accessed using -// chi.URLParam(). All patterns must begin with a slash. +// [chi.URLParam](). All patterns must begin with a slash. // // A simple named placeholder {name} matches any sequence of characters // up to the next / or the end of the URL. Trailing slashes on paths must @@ -37,11 +39,11 @@ // // A placeholder with a name followed by a colon allows a regular // expression match, for example {number:\\d+}. The regular expression -// syntax is Go's normal regexp RE2 syntax, except that / will never be -// matched. An anonymous regexp pattern is allowed, using an empty string -// before the colon in the placeholder, such as {:\\d+} +// syntax is Go's normal [regexp] RE2 syntax (see [regexp/syntax]), except +// that / will never be matched. An anonymous regexp pattern is allowed, using +// an empty string before the colon in the placeholder, such as {:\\d+}. // -// The special placeholder of asterisk matches the rest of the requested +// The special placeholder of asterisk (*) matches the rest of the requested // URL. Any trailing characters in the pattern are ignored. This is the only // placeholder which will match / characters. // @@ -77,7 +79,7 @@ type Router interface { // path, with a fresh middleware stack for the inline-Router. Group(fn func(r Router)) Router - // Route mounts a sub-Router along a `pattern`` string. + // Route mounts a sub-Router along a `pattern` string. Route(pattern string, fn func(r Router)) Router // Mount attaches another http.Handler along ./pattern/* @@ -102,6 +104,7 @@ type Router interface { Patch(pattern string, h http.HandlerFunc) Post(pattern string, h http.HandlerFunc) Put(pattern string, h http.HandlerFunc) + Query(pattern string, h http.HandlerFunc) Trace(pattern string, h http.HandlerFunc) // NotFound defines a handler to respond whenever a route could diff --git a/vendor/github.com/go-chi/chi/v5/context.go b/vendor/github.com/go-chi/chi/v5/context.go index 82220730e..c5c9a7432 100644 --- a/vendor/github.com/go-chi/chi/v5/context.go +++ b/vendor/github.com/go-chi/chi/v5/context.go @@ -3,15 +3,13 @@ package chi import ( "context" "net/http" + "slices" "strings" ) // URLParam returns the url parameter from a http.Request object. func URLParam(r *http.Request, key string) string { - if rctx := RouteContext(r.Context()); rctx != nil { - return rctx.URLParam(key) - } - return "" + return URLParamFromCtx(r.Context(), key) } // URLParamFromCtx returns the url parameter from a http.Request Context. @@ -42,6 +40,8 @@ var ( // Context is the default routing context set on the root node of a // request context to track route patterns, URL parameters and // an optional routing path. +// +// NOTE: New reference fields (slices, maps, pointers) must be deep-copied in Clone. type Context struct { Routes Routes @@ -95,12 +95,36 @@ func (x *Context) Reset() { x.parentCtx = nil } +// Clone a routing context so that it may be used outside of the request/response +// lifecycle, e.g. in a goroutine that outlives the request handler. +// +// Clone must be called before the request handler returns; after that, the +// original context is reset and put back into the pool for reuse by another +// request, racing with the copy. +func (x *Context) Clone() *Context { + clone := *x + + // Detach from the request's context, which is canceled once the request finishes. + clone.parentCtx = nil + + clone.URLParams.Keys = slices.Clone(x.URLParams.Keys) + clone.URLParams.Values = slices.Clone(x.URLParams.Values) + + clone.routeParams.Keys = slices.Clone(x.routeParams.Keys) + clone.routeParams.Values = slices.Clone(x.routeParams.Values) + + clone.RoutePatterns = slices.Clone(x.RoutePatterns) + clone.methodsAllowed = slices.Clone(x.methodsAllowed) + + return &clone +} + // URLParam returns the corresponding URL parameter value from the request // routing context. func (x *Context) URLParam(key string) string { - for k := len(x.URLParams.Keys) - 1; k >= 0; k-- { - if x.URLParams.Keys[k] == key { - return x.URLParams.Values[k] + for i, k := range slices.Backward(x.URLParams.Keys) { + if k == key { + return x.URLParams.Values[i] } } return "" diff --git a/vendor/github.com/go-chi/chi/v5/middleware/client_ip.go b/vendor/github.com/go-chi/chi/v5/middleware/client_ip.go new file mode 100644 index 000000000..55e2dc25d --- /dev/null +++ b/vendor/github.com/go-chi/chi/v5/middleware/client_ip.go @@ -0,0 +1,341 @@ +package middleware + +import ( + "context" + "net" + "net/http" + "net/netip" + "slices" + "strings" +) + +// clientIPCtxKey stores the client IP set by any of the ClientIPFrom* middlewares. +var clientIPCtxKey = &contextKey{"clientIP"} + +// xForwardedForHeader is the canonical form of the X-Forwarded-For header +// name, used by the XFF-based middlewares. +const xForwardedForHeader = "X-Forwarded-For" + +// ClientIPFromHeader stores the client IP from a single-IP header set by +// your reverse proxy. Read it with [GetClientIP]. +// +// Only safe with headers your proxy unconditionally OVERWRITES on every +// request, e.g.: +// +// - X-Real-IP — Nginx with ngx_http_realip_module +// - X-Client-IP — Apache with mod_remoteip +// - CF-Connecting-IP — Cloudflare +// +// True-Client-IP, X-Azure-ClientIP, and Fastly-Client-IP look similar but +// pass through from the client by default in those products; don't use them +// unless your edge strips the inbound value. +// +// If the header reaches us with multiple values (misconfigured proxy that +// appends, or a downstream proxy not stripping a client-supplied value), +// the LAST value wins — that's the one set by the hop closest to us, and +// therefore the most trusted. Fail-closed if the last value doesn't parse: +// no client IP is set rather than falling back to earlier (less-trusted) +// values. +// +// v4-mapped IPv6 (::ffff:a.b.c.d) folds to plain v4 and IPv6 zones are +// stripped before storage. +// +// Overwriting only protects requests that pass through your proxy. A client +// that reaches this server directly can set the header to anything. Pass +// your proxy's CIDR prefixes as trustedIPPrefixes to close that gap: the +// header is then read only when r.RemoteAddr falls within one of them; +// otherwise r.RemoteAddr itself is the client IP. This mirrors nginx +// (set_real_ip_from), Apache (RemoteIPTrustedProxy), and Caddy +// (trusted_proxies), and Cloudflare's advice to accept origin traffic only +// from its IP ranges. Without trustedIPPrefixes, guarantee at the network +// layer (security group / firewall) that only your proxy can reach this +// server. +// +// r.Use(middleware.ClientIPFromHeader("CF-Connecting-IP", +// "173.245.48.0/20", // Cloudflare IPv4, see https://www.cloudflare.com/ips/ +// "2400:cb00::/32", // Cloudflare IPv6 +// )) +// +// Panics at startup if any prefix is invalid. +func ClientIPFromHeader(trustedHeader string, trustedIPPrefixes ...string) func(http.Handler) http.Handler { + header := http.CanonicalHeaderKey(trustedHeader) + prefixes := mustParsePrefixes(trustedIPPrefixes) + return func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var found netip.Addr + if peer, untrusted := untrustedPeer(r, prefixes); untrusted { + found = peer + } else if values := r.Header.Values(header); len(values) > 0 { + found, _ = parseHeaderAddr(values[len(values)-1]) + } + if found.IsValid() { + r = r.WithContext(context.WithValue(r.Context(), clientIPCtxKey, found)) + } + h.ServeHTTP(w, r) + }) + } +} + +// ClientIPFromXFF stores the client IP read from the X-Forwarded-For header, +// walking the chain right-to-left and skipping any IP that falls within one +// of the given trusted CIDR prefixes. The first IP that is not trusted is +// the client. Read it with [GetClientIP]. +// +// An unparseable entry mid-chain aborts the walk and leaves no client IP +// set (fail-closed) — we can't safely trust anything left of garbage. +// +// Use this when you sit behind one or more reverse proxies whose IP ranges +// you can enumerate as CIDRs. Most CDNs publish their IPs: +// +// Cloudflare: https://www.cloudflare.com/ips/ +// AWS: https://ip-ranges.amazonaws.com/ip-ranges.json +// Fastly: https://api.fastly.com/public-ip-list +// Google Cloud: https://www.gstatic.com/ipranges/cloud.json +// +// Example (CloudFront): +// +// r.Use(middleware.ClientIPFromXFF( +// "13.32.0.0/15", // CloudFront IPv4 +// "52.46.0.0/18", // CloudFront IPv4 +// "2600:9000::/28", // CloudFront IPv6 +// )) +// +// The TCP peer in r.RemoteAddr is treated as the rightmost hop: if it is +// not within a trusted prefix, it is the client IP and X-Forwarded-For is +// ignored. A client reaching this server directly therefore cannot spoof +// its IP. If r.RemoteAddr doesn't parse, no client IP is set (fail-closed). +// +// At least one prefix is required. For a single trusted hop directly in +// front of this server (e.g., nginx on localhost), pass its address: +// +// r.Use(middleware.ClientIPFromXFF("127.0.0.1/32", "::1/128")) +// +// v4-mapped IPv6 (::ffff:a.b.c.d) folds to plain v4 and IPv6 zones are +// stripped before the prefix check and storage; otherwise an attacker +// could use either notation to alias a trusted IP past the check. +// +// If you know the number of trusted proxies but not their IPs, use +// [ClientIPFromXFFTrustedProxies] instead. +// +// Panics at startup if no prefix is given or any prefix is invalid. +func ClientIPFromXFF(trustedIPPrefixes ...string) func(http.Handler) http.Handler { + if len(trustedIPPrefixes) == 0 { + panic("middleware.ClientIPFromXFF: at least one trusted IP prefix is required; use ClientIPFromXFFTrustedProxies(1) to trust any peer") + } + prefixes := mustParsePrefixes(trustedIPPrefixes) + return func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var found netip.Addr + if peer, untrusted := untrustedPeer(r, prefixes); untrusted { + found = peer + } else { + walkXFF(r.Header[xForwardedForHeader], func(v string) bool { + ip, ok := parseHeaderAddr(v) + if !ok { + return true // fail-closed; leave found unset + } + if inAnyPrefix(ip, prefixes) { + return false // trusted hop; keep walking left + } + found = ip + return true + }) + } + if found.IsValid() { + r = r.WithContext(context.WithValue(r.Context(), clientIPCtxKey, found)) + } + h.ServeHTTP(w, r) + }) + } +} + +// ClientIPFromXFFTrustedProxies stores the client IP read from +// X-Forwarded-For, given the exact number of trusted reverse proxies +// between this server and the public internet. Read it with [GetClientIP]. +// +// PREFER [ClientIPFromXFF] with explicit CIDRs whenever you can — it +// cannot off-by-one and is robust to architecture changes. Most CDNs +// publish their IP ranges (Cloudflare, AWS, Fastly, Google Cloud). Use +// this counting variant only when proxy IPs are dynamic and unpublishable. +// +// numTrustedProxies = total proxy hops between the client and this server. +// Count every hop in the request path: +// +// Single proxy (one LB / nginx / Heroku / Fly.io / Render) ....... 1 +// Two proxies (Cloudflare → ALB, CloudFront → ALB) .............. 2 +// Three proxies (CDN → API gateway → LB) ......................... 3 +// +// VERIFY BEFORE GOING LIVE: send a request from a known IP and confirm +// [GetClientIP] returns that IP. If it returns a proxy IP, your count is +// too LOW — a client can spoof their IP, fix immediately. If it returns +// "", your count is too HIGH — no leak, but no client IP either. +// +// This middleware reads ONLY X-Forwarded-For; it does not inspect +// r.RemoteAddr. Guarantee at the network layer (security group / firewall) +// that only your proxies can reach this server. +// +// If the XFF chain has fewer than numTrustedProxies entries, no client IP +// is set (fail-closed). Like [ClientIPFromXFF], v4-mapped IPv6 folds to v4 +// and IPv6 zones are stripped before storage. +// +// Panics at startup if numTrustedProxies < 1. +func ClientIPFromXFFTrustedProxies(numTrustedProxies int) func(http.Handler) http.Handler { + if numTrustedProxies < 1 { + panic("middleware.ClientIPFromXFFTrustedProxies: numTrustedProxies must be >= 1") + } + return func(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + n := numTrustedProxies + var entry string + walkXFF(r.Header[xForwardedForHeader], func(v string) bool { + n-- + if n == 0 { + entry = v + return true + } + return false + }) + if entry != "" { + if ip, ok := parseHeaderAddr(entry); ok { + r = r.WithContext(context.WithValue(r.Context(), clientIPCtxKey, ip)) + } + } + h.ServeHTTP(w, r) + }) + } +} + +// ClientIPFromRemoteAddr stores the client IP read from the TCP RemoteAddr +// of the incoming request — the IP address of whoever opened the connection +// to this server. Read it with [GetClientIP]. +// +// Use this when this server is directly connected to the public internet +// with NO reverse proxy in front of it. Behind a reverse proxy, RemoteAddr +// is the proxy's IP, not the client's — use [ClientIPFromHeader] or +// [ClientIPFromXFF] instead. +// +// IPv4 clients on a dual-stack listener surface as ::ffff:a.b.c.d; they +// fold to plain v4 before storage so one logical client maps to one key. +// IPv6 zones are preserved (link-local connections may legitimately have one). +func ClientIPFromRemoteAddr(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if ip, ok := parseRemoteAddr(r.RemoteAddr); ok { + r = r.WithContext(context.WithValue(r.Context(), clientIPCtxKey, ip)) + } + h.ServeHTTP(w, r) + }) +} + +// GetClientIP returns the client IP as a string, as set by one of the +// ClientIPFrom* middlewares. Returns "" if no valid IP was set. +// Convenient for logging, rate-limit keys, etc. +func GetClientIP(ctx context.Context) string { + ip := GetClientIPAddr(ctx) + if !ip.IsValid() { + return "" + } + return ip.String() +} + +// GetClientIPAddr returns the client IP as a [netip.Addr], as set by one of +// the ClientIPFrom* middlewares. The returned Addr is the zero value if not +// set; use [netip.Addr.IsValid] to check. Useful when you need typed work — +// prefix containment, Is4/Is6, etc. — without re-parsing the string. +func GetClientIPAddr(ctx context.Context) netip.Addr { + ip, _ := ctx.Value(clientIPCtxKey).(netip.Addr) + return ip +} + +// walkXFF walks the entries of the merged X-Forwarded-For chain +// RIGHT-TO-LEFT, invoking visit on each trimmed non-empty entry. visit +// returns true to stop the walk. Lazy walk, zero allocations (entries +// are substrings of the input headers). +// +// Multiple XFF headers are merged per RFC 2616 — each header's +// comma-separated entries in order received — so an attacker cannot pick +// which value security logic sees by sending a duplicate header. +func walkXFF(headers []string, visit func(entry string) bool) { + for _, h := range slices.Backward(headers) { + for h != "" { + var v string + if i := strings.LastIndexByte(h, ','); i >= 0 { + v, h = h[i+1:], h[:i] + } else { + v, h = h, "" + } + v = strings.TrimSpace(v) + if v == "" { + continue + } + if visit(v) { + return + } + } + } +} + +// mustParsePrefixes parses CIDR prefixes, panicking on the first invalid one. +func mustParsePrefixes(cidrs []string) []netip.Prefix { + prefixes := make([]netip.Prefix, len(cidrs)) + for i, p := range cidrs { + prefixes[i] = netip.MustParsePrefix(p) + } + return prefixes +} + +// untrustedPeer reports whether the TCP peer in r.RemoteAddr is outside +// prefixes. If so, peer is the client IP, or invalid if RemoteAddr doesn't +// parse. With no prefixes, every peer is trusted. +func untrustedPeer(r *http.Request, prefixes []netip.Prefix) (peer netip.Addr, untrusted bool) { + if len(prefixes) == 0 { + return netip.Addr{}, false + } + peer, ok := parseRemoteAddr(r.RemoteAddr) + if !ok { + return netip.Addr{}, true + } + // Zoned addresses never match a prefix, so check a link-local peer without its zone. + return peer, !inAnyPrefix(peer.WithZone(""), prefixes) +} + +// parseRemoteAddr parses the host of r.RemoteAddr. v4-mapped IPv6 folds to +// plain v4; the IPv6 zone is preserved (link-local connections may +// legitimately have one). +func parseRemoteAddr(remoteAddr string) (netip.Addr, bool) { + host, _, err := net.SplitHostPort(remoteAddr) + if err != nil { + host = remoteAddr // RemoteAddr may already be a bare IP (e.g. in tests). + } + ip, err := netip.ParseAddr(host) + if err != nil { + return netip.Addr{}, false + } + return ip.Unmap(), true +} + +// inAnyPrefix reports whether ip falls within any of the given prefixes. +func inAnyPrefix(ip netip.Addr, prefixes []netip.Prefix) bool { + for _, p := range prefixes { + if p.Contains(ip) { + return true + } + } + return false +} + +// parseHeaderAddr parses s and normalizes for storage: v4-mapped IPv6 +// (::ffff:a.b.c.d) folds to plain v4, IPv6 zone is stripped. Both defend the +// trust-prefix check against attacker-injected aliases — [netip.Prefix.Contains] +// returns false for v4-mapped addresses vs v4 prefixes and for any zoned +// address, so without folding/stripping an attacker could escape an +// otherwise valid trust list. +// +// Header-sourced IPs only. r.RemoteAddr goes through parseRemoteAddr +// (Unmap, but zone preserved for legitimate link-local connections). +func parseHeaderAddr(s string) (netip.Addr, bool) { + ip, err := netip.ParseAddr(s) + if err != nil { + return netip.Addr{}, false + } + return ip.Unmap().WithZone(""), true +} diff --git a/vendor/github.com/go-chi/chi/v5/middleware/compress.go b/vendor/github.com/go-chi/chi/v5/middleware/compress.go index 9c64bd48d..2b3653f23 100644 --- a/vendor/github.com/go-chi/chi/v5/middleware/compress.go +++ b/vendor/github.com/go-chi/chi/v5/middleware/compress.go @@ -18,11 +18,16 @@ var defaultCompressibleContentTypes = []string{ "text/css", "text/plain", "text/javascript", + "text/markdown", + "text/csv", + "text/vtt", "application/javascript", "application/x-javascript", "application/json", "application/atom+xml", "application/rss+xml", + "application/xml", + "text/xml", "image/svg+xml", } @@ -60,6 +65,10 @@ type Compressor struct { // // The level should be one of the ones defined in the flate package. // The types are the content types that are allowed to be compressed. +// +// Catch-all wildcards ("*/*", "/*") are rejected: compressing every response +// wastes CPU on already-compressed types like zip, jpeg or png. Pass explicit +// types instead, e.g. "text/html" or "application/*". func NewCompressor(level int, types ...string) *Compressor { // If types are provided, set those as the allowed types. If none are // provided, use the default list. @@ -68,10 +77,13 @@ func NewCompressor(level int, types ...string) *Compressor { if len(types) > 0 { for _, t := range types { if strings.Contains(strings.TrimSuffix(t, "/*"), "*") { - panic(fmt.Sprintf("middleware/compress: Unsupported content-type wildcard pattern '%s'. Only '/*' supported", t)) + panic(fmt.Sprintf("middleware/compress: Unsupported content-type wildcard pattern '%s'. Only '/*' supported", t)) } - if strings.HasSuffix(t, "/*") { - allowedWildcards[strings.TrimSuffix(t, "/*")] = struct{}{} + if before, ok := strings.CutSuffix(t, "/*"); ok { + if before == "" { + panic(fmt.Sprintf("middleware/compress: Unsupported content-type wildcard pattern '%s'. Only '/*' supported", t)) + } + allowedWildcards[before] = struct{}{} } else { allowedTypes[t] = struct{}{} } @@ -136,13 +148,21 @@ func NewCompressor(level int, types ...string) *Compressor { // // For example, add the Brotli algorithm: // -// import brotli_enc "gopkg.in/kothar/brotli-go.v0/enc" +// import "github.com/google/brotli/go/cbrotli" // // compressor := middleware.NewCompressor(5, "text/html") // compressor.SetEncoder("br", func(w io.Writer, level int) io.Writer { -// params := brotli_enc.NewBrotliParams() -// params.SetQuality(level) -// return brotli_enc.NewBrotliWriter(params, w) +// return cbrotli.NewWriter(w, cbrotli.WriterOptions{Quality: level}) +// }) +// +// Alternatively, this Cgo-free Brotli module can be used: +// +// import "github.com/andybalholm/brotli" +// +// compressor := middleware.NewCompressor(5, "text/html") +// +// compressor.SetEncoder("br", func(w io.Writer, level int) io.Writer { +// return brotli.NewWriterV2(w, level) // }) func (c *Compressor) SetEncoder(encoding string, fn EncoderFunc) { encoding = strings.ToLower(encoding) @@ -162,7 +182,7 @@ func (c *Compressor) SetEncoder(encoding string, fn EncoderFunc) { encoder := fn(io.Discard, c.level) if _, ok := encoder.(ioResetterWriter); ok { pool := &sync.Pool{ - New: func() interface{} { + New: func() any { return fn(io.Discard, c.level) }, } diff --git a/vendor/github.com/go-chi/chi/v5/middleware/content_charset.go b/vendor/github.com/go-chi/chi/v5/middleware/content_charset.go index 07bff9f2e..55773517c 100644 --- a/vendor/github.com/go-chi/chi/v5/middleware/content_charset.go +++ b/vendor/github.com/go-chi/chi/v5/middleware/content_charset.go @@ -2,11 +2,13 @@ package middleware import ( "net/http" + "slices" "strings" ) // ContentCharset generates a handler that writes a 415 Unsupported Media Type response if none of the charsets match. // An empty charset will allow requests with no Content-Type header or no specified charset. +// Requests without a body (ContentLength == 0) are always allowed. func ContentCharset(charsets ...string) func(next http.Handler) http.Handler { for i, c := range charsets { charsets[i] = strings.ToLower(c) @@ -14,6 +16,11 @@ func ContentCharset(charsets ...string) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.ContentLength == 0 { + next.ServeHTTP(w, r) + return + } + if !contentEncoding(r.Header.Get("Content-Type"), charsets...) { w.WriteHeader(http.StatusUnsupportedMediaType) return @@ -29,13 +36,7 @@ func contentEncoding(ce string, charsets ...string) bool { _, ce = split(strings.ToLower(ce), ";") _, ce = split(ce, "charset=") ce, _ = split(ce, ";") - for _, c := range charsets { - if ce == c { - return true - } - } - - return false + return slices.Contains(charsets, ce) } // Split a string in two parts, cleaning any whitespace. diff --git a/vendor/github.com/go-chi/chi/v5/middleware/logger.go b/vendor/github.com/go-chi/chi/v5/middleware/logger.go index cff9bd206..72e735fe4 100644 --- a/vendor/github.com/go-chi/chi/v5/middleware/logger.go +++ b/vendor/github.com/go-chi/chi/v5/middleware/logger.go @@ -67,8 +67,8 @@ type LogFormatter interface { // LogEntry records the final log when a request completes. // See defaultLogEntry for an example implementation. type LogEntry interface { - Write(status, bytes int, header http.Header, elapsed time.Duration, extra interface{}) - Panic(v interface{}, stack []byte) + Write(status, bytes int, header http.Header, elapsed time.Duration, extra any) + Panic(v any, stack []byte) } // GetLogEntry returns the in-context LogEntry for a request. @@ -85,7 +85,7 @@ func WithLogEntry(r *http.Request, entry LogEntry) *http.Request { // LoggerInterface accepts printing to stdlib logger or compatible logger. type LoggerInterface interface { - Print(v ...interface{}) + Print(v ...any) } // DefaultLogFormatter is a simple logger that implements a LogFormatter. @@ -96,6 +96,8 @@ type DefaultLogFormatter struct { // NewLogEntry creates a new LogEntry for the request. func (l *DefaultLogFormatter) NewLogEntry(r *http.Request) LogEntry { + ctx := r.Context() + useColor := !l.NoColor entry := &defaultLogEntry{ DefaultLogFormatter: l, @@ -104,7 +106,7 @@ func (l *DefaultLogFormatter) NewLogEntry(r *http.Request) LogEntry { useColor: useColor, } - reqID := GetReqID(r.Context()) + reqID := GetReqID(ctx) if reqID != "" { cW(entry.buf, useColor, nYellow, "[%s] ", reqID) } @@ -118,7 +120,11 @@ func (l *DefaultLogFormatter) NewLogEntry(r *http.Request) LogEntry { cW(entry.buf, useColor, nCyan, "%s://%s%s %s\" ", scheme, r.Host, r.RequestURI, r.Proto) entry.buf.WriteString("from ") - entry.buf.WriteString(r.RemoteAddr) + clientIP := GetClientIP(ctx) + if clientIP == "" { + clientIP = r.RemoteAddr + } + entry.buf.WriteString(clientIP) entry.buf.WriteString(" - ") return entry @@ -131,7 +137,7 @@ type defaultLogEntry struct { useColor bool } -func (l *defaultLogEntry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra interface{}) { +func (l *defaultLogEntry) Write(status, bytes int, header http.Header, elapsed time.Duration, extra any) { switch { case status < 200: cW(l.buf, l.useColor, bBlue, "%03d", status) @@ -159,8 +165,8 @@ func (l *defaultLogEntry) Write(status, bytes int, header http.Header, elapsed t l.Logger.Print(l.buf.String()) } -func (l *defaultLogEntry) Panic(v interface{}, stack []byte) { - PrintPrettyStack(v) +func (l *defaultLogEntry) Panic(v any, stack []byte) { + printPrettyStack(v, l.useColor) } func init() { diff --git a/vendor/github.com/go-chi/chi/v5/middleware/profiler.go b/vendor/github.com/go-chi/chi/v5/middleware/profiler.go index 0ad6a996b..5784ff68e 100644 --- a/vendor/github.com/go-chi/chi/v5/middleware/profiler.go +++ b/vendor/github.com/go-chi/chi/v5/middleware/profiler.go @@ -1,5 +1,4 @@ //go:build !tinygo -// +build !tinygo package middleware diff --git a/vendor/github.com/go-chi/chi/v5/middleware/realip.go b/vendor/github.com/go-chi/chi/v5/middleware/realip.go index afcb79e20..349f16821 100644 --- a/vendor/github.com/go-chi/chi/v5/middleware/realip.go +++ b/vendor/github.com/go-chi/chi/v5/middleware/realip.go @@ -17,17 +17,14 @@ var xRealIP = http.CanonicalHeaderKey("X-Real-IP") // of parsing either the True-Client-IP, X-Real-IP or the X-Forwarded-For headers // (in that order). // -// This middleware should be inserted fairly early in the middleware stack to -// ensure that subsequent layers (e.g., request loggers) which examine the -// RemoteAddr will see the intended value. +// Deprecated: RealIP is vulnerable to IP spoofing — it mutates r.RemoteAddr +// to the leftmost X-Forwarded-For value, or to True-Client-IP / X-Real-IP +// whether or not your infrastructure actually sets them. See +// GHSA-3fxj-6jh8-hvhx, GHSA-rjr7-jggh-pgcp, GHSA-9g5q-2w5x-hmxf. // -// You should only use this middleware if you can trust the headers passed to -// you (in particular, the three headers this middleware uses), for example -// because you have placed a reverse proxy like HAProxy or nginx in front of -// chi. If your reverse proxies are configured to pass along arbitrary header -// values from the client, or if you use this middleware without a reverse -// proxy, malicious clients will be able to make you very sad (or, depending on -// how you're using RemoteAddr, vulnerable to an attack of some sort). +// Use [ClientIPFromHeader], [ClientIPFromXFF], [ClientIPFromXFFTrustedProxies] +// or [ClientIPFromRemoteAddr] and read the IP with [GetClientIP] instead. +// These never mutate r.RemoteAddr. func RealIP(h http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { if rip := realIP(r); rip != "" { diff --git a/vendor/github.com/go-chi/chi/v5/middleware/recoverer.go b/vendor/github.com/go-chi/chi/v5/middleware/recoverer.go index 81342dfa7..e82de6b68 100644 --- a/vendor/github.com/go-chi/chi/v5/middleware/recoverer.go +++ b/vendor/github.com/go-chi/chi/v5/middleware/recoverer.go @@ -51,10 +51,17 @@ func Recoverer(next http.Handler) http.Handler { // for ability to test the PrintPrettyStack function var recovererErrorWriter io.Writer = os.Stderr -func PrintPrettyStack(rvr interface{}) { +func PrintPrettyStack(rvr any) { + printPrettyStack(rvr, true) +} + +// printPrettyStack prints a formatted stack trace to stderr. When useColor is +// false, ANSI colour codes are suppressed, which is useful for terminals that +// do not support them (e.g. on Windows) or when output is being captured. +func printPrettyStack(rvr any, useColor bool) { debugStack := debug.Stack() s := prettyStack{} - out, err := s.parse(debugStack, rvr) + out, err := s.parse(debugStack, rvr, useColor) if err == nil { recovererErrorWriter.Write(out) } else { @@ -66,9 +73,8 @@ func PrintPrettyStack(rvr interface{}) { type prettyStack struct { } -func (s prettyStack) parse(debugStack []byte, rvr interface{}) ([]byte, error) { +func (s prettyStack) parse(debugStack []byte, rvr any, useColor bool) ([]byte, error) { var err error - useColor := true buf := &bytes.Buffer{} cW(buf, false, bRed, "\n") diff --git a/vendor/github.com/go-chi/chi/v5/middleware/request_id.go b/vendor/github.com/go-chi/chi/v5/middleware/request_id.go index 4903ecc21..e1d4ccb7d 100644 --- a/vendor/github.com/go-chi/chi/v5/middleware/request_id.go +++ b/vendor/github.com/go-chi/chi/v5/middleware/request_id.go @@ -25,7 +25,7 @@ const RequestIDKey ctxKeyRequestID = 0 var RequestIDHeader = "X-Request-Id" var prefix string -var reqid uint64 +var reqid atomic.Uint64 // A quick note on the statistics here: we're trying to calculate the chance that // two randomly generated base62 prefixes will collide. We use the formula from @@ -69,7 +69,7 @@ func RequestID(next http.Handler) http.Handler { ctx := r.Context() requestID := r.Header.Get(RequestIDHeader) if requestID == "" { - myid := atomic.AddUint64(&reqid, 1) + myid := reqid.Add(1) requestID = fmt.Sprintf("%s-%06d", prefix, myid) } ctx = context.WithValue(ctx, RequestIDKey, requestID) @@ -92,5 +92,5 @@ func GetReqID(ctx context.Context) string { // NextRequestID generates the next request ID in the sequence. func NextRequestID() uint64 { - return atomic.AddUint64(&reqid, 1) + return reqid.Add(1) } diff --git a/vendor/github.com/go-chi/chi/v5/middleware/route_headers.go b/vendor/github.com/go-chi/chi/v5/middleware/route_headers.go index 88743769a..1c3334d35 100644 --- a/vendor/github.com/go-chi/chi/v5/middleware/route_headers.go +++ b/vendor/github.com/go-chi/chi/v5/middleware/route_headers.go @@ -79,6 +79,7 @@ func (hr HeaderRouter) Handler(next http.Handler) http.Handler { if len(hr) == 0 { // skip if no routes set next.ServeHTTP(w, r) + return } // find first matching header route, and continue diff --git a/vendor/github.com/go-chi/chi/v5/middleware/strip.go b/vendor/github.com/go-chi/chi/v5/middleware/strip.go index 17aa9bf32..32d21e90b 100644 --- a/vendor/github.com/go-chi/chi/v5/middleware/strip.go +++ b/vendor/github.com/go-chi/chi/v5/middleware/strip.go @@ -47,15 +47,22 @@ func RedirectSlashes(next http.Handler) http.Handler { } else { path = r.URL.Path } + if len(path) > 1 && path[len(path)-1] == '/' { - // Trim all leading and trailing slashes (e.g., "//evil.com", "/some/path//") - path = "/" + strings.Trim(path, "/") + // Normalize backslashes to forward slashes to prevent "/\evil.com" style redirects + // that some clients may interpret as protocol-relative. + path = strings.ReplaceAll(path, `\`, `/`) + + // Collapse leading/trailing slashes and force a single leading slash. + path := "/" + strings.Trim(path, "/") + if r.URL.RawQuery != "" { path = fmt.Sprintf("%s?%s", path, r.URL.RawQuery) } http.Redirect(w, r, path, 301) return } + next.ServeHTTP(w, r) } return http.HandlerFunc(fn) diff --git a/vendor/github.com/go-chi/chi/v5/middleware/terminal.go b/vendor/github.com/go-chi/chi/v5/middleware/terminal.go index 5ead7b924..5613318cd 100644 --- a/vendor/github.com/go-chi/chi/v5/middleware/terminal.go +++ b/vendor/github.com/go-chi/chi/v5/middleware/terminal.go @@ -52,7 +52,7 @@ func init() { } // colorWrite -func cW(w io.Writer, useColor bool, color []byte, s string, args ...interface{}) { +func cW(w io.Writer, useColor bool, color []byte, s string, args ...any) { if IsTTY && useColor { w.Write(color) } diff --git a/vendor/github.com/go-chi/chi/v5/middleware/value.go b/vendor/github.com/go-chi/chi/v5/middleware/value.go index a9dfd4345..fbb9573e1 100644 --- a/vendor/github.com/go-chi/chi/v5/middleware/value.go +++ b/vendor/github.com/go-chi/chi/v5/middleware/value.go @@ -6,7 +6,7 @@ import ( ) // WithValue is a middleware that sets a given key/value in a context chain. -func WithValue(key, val interface{}) func(next http.Handler) http.Handler { +func WithValue(key, val any) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { r = r.WithContext(context.WithValue(r.Context(), key, val)) diff --git a/vendor/github.com/go-chi/chi/v5/middleware/wrap_writer.go b/vendor/github.com/go-chi/chi/v5/middleware/wrap_writer.go index 367e0fcd9..2fdd9c44e 100644 --- a/vendor/github.com/go-chi/chi/v5/middleware/wrap_writer.go +++ b/vendor/github.com/go-chi/chi/v5/middleware/wrap_writer.go @@ -63,6 +63,7 @@ type WrapResponseWriter interface { Unwrap() http.ResponseWriter // Discard causes all writes to the original ResponseWriter be discarded, // instead writing only to the tee'd writer if it's set. + // Flush calls also do not reach the original ResponseWriter. // The caller is responsible for calling WriteHeader and Write on the // original ResponseWriter once the processing is done. Discard() @@ -119,6 +120,13 @@ func (b *basicWriter) maybeWriteHeader() { } } +func (b *basicWriter) flush() { + b.maybeWriteHeader() + if !b.discard { + b.ResponseWriter.(http.Flusher).Flush() + } +} + func (b *basicWriter) Status() int { return b.code } @@ -145,9 +153,7 @@ type flushWriter struct { } func (f *flushWriter) Flush() { - f.wroteHeader = true - fl := f.basicWriter.ResponseWriter.(http.Flusher) - fl.Flush() + f.basicWriter.flush() } var _ http.Flusher = &flushWriter{} @@ -170,9 +176,7 @@ type flushHijackWriter struct { } func (f *flushHijackWriter) Flush() { - f.wroteHeader = true - fl := f.basicWriter.ResponseWriter.(http.Flusher) - fl.Flush() + f.basicWriter.flush() } func (f *flushHijackWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { @@ -192,9 +196,7 @@ type httpFancyWriter struct { } func (f *httpFancyWriter) Flush() { - f.wroteHeader = true - fl := f.basicWriter.ResponseWriter.(http.Flusher) - fl.Flush() + f.basicWriter.flush() } func (f *httpFancyWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { @@ -207,9 +209,13 @@ func (f *http2FancyWriter) Push(target string, opts *http.PushOptions) error { } func (f *httpFancyWriter) ReadFrom(r io.Reader) (int64, error) { - if f.basicWriter.tee != nil { + if f.basicWriter.tee != nil || f.basicWriter.discard { + // Route through basicWriter.Write so that the tee and discard semantics + // are honored (the fast ReaderFrom path below would bypass both, writing + // straight to the original ResponseWriter). basicWriter.Write already + // increments basicWriter.bytes, so we must NOT add n again here (that + // would double-count). n, err := io.Copy(&f.basicWriter, r) - f.basicWriter.bytes += int(n) return n, err } rf := f.basicWriter.ResponseWriter.(io.ReaderFrom) @@ -233,9 +239,7 @@ type http2FancyWriter struct { } func (f *http2FancyWriter) Flush() { - f.wroteHeader = true - fl := f.basicWriter.ResponseWriter.(http.Flusher) - fl.Flush() + f.basicWriter.flush() } var _ http.Flusher = &http2FancyWriter{} diff --git a/vendor/github.com/go-chi/chi/v5/mux.go b/vendor/github.com/go-chi/chi/v5/mux.go index ad66bba91..7cfc6143d 100644 --- a/vendor/github.com/go-chi/chi/v5/mux.go +++ b/vendor/github.com/go-chi/chi/v5/mux.go @@ -51,7 +51,7 @@ type Mux struct { // interface. func NewMux() *Mux { mux := &Mux{tree: &node{}, pool: &sync.Pool{}} - mux.pool.New = func() interface{} { + mux.pool.New = func() any { return NewRouteContext() } return mux @@ -186,6 +186,12 @@ func (mx *Mux) Put(pattern string, handlerFn http.HandlerFunc) { mx.handle(mPUT, pattern, handlerFn) } +// Query adds the route `pattern` that matches a QUERY http method to +// execute the `handlerFn` http.HandlerFunc. +func (mx *Mux) Query(pattern string, handlerFn http.HandlerFunc) { + mx.handle(mQUERY, pattern, handlerFn) +} + // Trace adds the route `pattern` that matches a TRACE http method to // execute the `handlerFn` http.HandlerFunc. func (mx *Mux) Trace(pattern string, handlerFn http.HandlerFunc) { @@ -299,6 +305,14 @@ func (mx *Mux) Mount(pattern string, handler http.Handler) { // Assign sub-Router's with the parent not found & method not allowed handler if not specified. subr, ok := handler.(*Mux) + + // Provide runtime safety for ensuring a router isn't mounted onto itself, + // which includes an inline mux derived from it via With()/Group() since + // that shares the same underlying tree. See issue #843. + if ok && subr.tree == mx.tree { + panic(fmt.Sprintf("chi: attempting to Mount() a router onto itself on '%s'", pattern)) + } + if ok && subr.notFoundHandler == nil && mx.notFoundHandler != nil { subr.NotFound(mx.notFoundHandler) } @@ -467,12 +481,12 @@ func (mx *Mux) routeHTTP(w http.ResponseWriter, r *http.Request) { // Find the route if _, _, h := mx.tree.FindRoute(rctx, method, routePath); h != nil { - if supportsPathValue { - setPathValue(rctx, r) - } - if supportsPattern { - setPattern(rctx, r) + // Set http.Request path values from our request context + for i, key := range rctx.URLParams.Keys { + value := rctx.URLParams.Values[i] + r.SetPathValue(key, value) } + r.Pattern = rctx.RoutePattern() h.ServeHTTP(w, r) return diff --git a/vendor/github.com/go-chi/chi/v5/path_value.go b/vendor/github.com/go-chi/chi/v5/path_value.go deleted file mode 100644 index 77c840f01..000000000 --- a/vendor/github.com/go-chi/chi/v5/path_value.go +++ /dev/null @@ -1,21 +0,0 @@ -//go:build go1.22 && !tinygo -// +build go1.22,!tinygo - - -package chi - -import "net/http" - -// supportsPathValue is true if the Go version is 1.22 and above. -// -// If this is true, `net/http.Request` has methods `SetPathValue` and `PathValue`. -const supportsPathValue = true - -// setPathValue sets the path values in the Request value -// based on the provided request context. -func setPathValue(rctx *Context, r *http.Request) { - for i, key := range rctx.URLParams.Keys { - value := rctx.URLParams.Values[i] - r.SetPathValue(key, value) - } -} diff --git a/vendor/github.com/go-chi/chi/v5/path_value_fallback.go b/vendor/github.com/go-chi/chi/v5/path_value_fallback.go deleted file mode 100644 index 749a8520a..000000000 --- a/vendor/github.com/go-chi/chi/v5/path_value_fallback.go +++ /dev/null @@ -1,19 +0,0 @@ -//go:build !go1.22 || tinygo -// +build !go1.22 tinygo - -package chi - -import "net/http" - -// supportsPathValue is true if the Go version is 1.22 and above. -// -// If this is true, `net/http.Request` has methods `SetPathValue` and `PathValue`. -const supportsPathValue = false - -// setPathValue sets the path values in the Request value -// based on the provided request context. -// -// setPathValue is only supported in Go 1.22 and above so -// this is just a blank function so that it compiles. -func setPathValue(rctx *Context, r *http.Request) { -} diff --git a/vendor/github.com/go-chi/chi/v5/pattern.go b/vendor/github.com/go-chi/chi/v5/pattern.go deleted file mode 100644 index 890a2c217..000000000 --- a/vendor/github.com/go-chi/chi/v5/pattern.go +++ /dev/null @@ -1,16 +0,0 @@ -//go:build go1.23 && !tinygo -// +build go1.23,!tinygo - -package chi - -import "net/http" - -// supportsPattern is true if the Go version is 1.23 and above. -// -// If this is true, `net/http.Request` has field `Pattern`. -const supportsPattern = true - -// setPattern sets the mux matched pattern in the http Request. -func setPattern(rctx *Context, r *http.Request) { - r.Pattern = rctx.routePattern -} diff --git a/vendor/github.com/go-chi/chi/v5/pattern_fallback.go b/vendor/github.com/go-chi/chi/v5/pattern_fallback.go deleted file mode 100644 index 48a94ef82..000000000 --- a/vendor/github.com/go-chi/chi/v5/pattern_fallback.go +++ /dev/null @@ -1,17 +0,0 @@ -//go:build !go1.23 || tinygo -// +build !go1.23 tinygo - -package chi - -import "net/http" - -// supportsPattern is true if the Go version is 1.23 and above. -// -// If this is true, `net/http.Request` has field `Pattern`. -const supportsPattern = false - -// setPattern sets the mux matched pattern in the http Request. -// -// setPattern is only supported in Go 1.23 and above so -// this is just a blank function so that it compiles. -func setPattern(rctx *Context, r *http.Request) {} diff --git a/vendor/github.com/go-chi/chi/v5/tree.go b/vendor/github.com/go-chi/chi/v5/tree.go index bcb86b6f1..af4052c8c 100644 --- a/vendor/github.com/go-chi/chi/v5/tree.go +++ b/vendor/github.com/go-chi/chi/v5/tree.go @@ -7,7 +7,9 @@ package chi import ( "fmt" "net/http" + "reflect" "regexp" + "slices" "sort" "strconv" "strings" @@ -25,11 +27,17 @@ const ( mPATCH mPOST mPUT + mQUERY mTRACE ) var mALL = mCONNECT | mDELETE | mGET | mHEAD | - mOPTIONS | mPATCH | mPOST | mPUT | mTRACE + mOPTIONS | mPATCH | mPOST | mPUT | mQUERY | mTRACE + +// methodQuery is the HTTP QUERY method (RFC 10008), a safe, idempotent +// method that conveys a request body. It is defined here until net/http +// provides an equivalent constant, at which point this is a 1-1 swap. +const methodQuery = "QUERY" var methodMap = map[string]methodTyp{ http.MethodConnect: mCONNECT, @@ -40,6 +48,7 @@ var methodMap = map[string]methodTyp{ http.MethodPatch: mPATCH, http.MethodPost: mPOST, http.MethodPut: mPUT, + methodQuery: mQUERY, http.MethodTrace: mTRACE, } @@ -52,6 +61,7 @@ var reverseMethodMap = map[methodTyp]string{ mPATCH: http.MethodPatch, mPOST: http.MethodPost, mPUT: http.MethodPut, + mQUERY: methodQuery, mTRACE: http.MethodTrace, } @@ -71,6 +81,7 @@ func RegisterMethod(method string) { } mt := methodTyp(2 << n) methodMap[method] = mt + reverseMethodMap[mt] = method mALL |= mt } @@ -328,7 +339,7 @@ func (n *node) replaceChild(label, tail byte, child *node) { func (n *node) getEdge(ntyp nodeTyp, label, tail byte, prefix string) *node { nds := n.children[ntyp] - for i := 0; i < len(nds); i++ { + for i := range nds { if nds[i].label == label && nds[i].tail == tail { if ntyp == ntRegexp && nds[i].prefix != prefix { continue @@ -429,9 +440,7 @@ func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node { } // serially loop through each node grouped by the tail delimiter - for idx := 0; idx < len(nds); idx++ { - xn = nds[idx] - + for _, xn = range nds { // label for param nodes is the delimiter byte p := strings.IndexByte(xsearch, xn.tail) @@ -470,7 +479,9 @@ func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node { if endpoints == mALL || endpoints == mSTUB { continue } - rctx.methodsAllowed = append(rctx.methodsAllowed, endpoints) + if !slices.Contains(rctx.methodsAllowed, endpoints) { + rctx.methodsAllowed = append(rctx.methodsAllowed, endpoints) + } } // flag that the routing context found a route, but not a corresponding @@ -516,7 +527,9 @@ func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node { if endpoints == mALL || endpoints == mSTUB { continue } - rctx.methodsAllowed = append(rctx.methodsAllowed, endpoints) + if !slices.Contains(rctx.methodsAllowed, endpoints) { + rctx.methodsAllowed = append(rctx.methodsAllowed, endpoints) + } } // flag that the routing context found a route, but not a corresponding @@ -621,8 +634,10 @@ func (n *node) routes() []Route { rts := []Route{} n.walk(func(eps endpoints, subroutes Routes) bool { - if eps[mSTUB] != nil && eps[mSTUB].handler != nil && subroutes == nil { - return false + // Hide Mount()'s stub handler, but not a real handler sharing its pattern. + var stubHandler http.Handler + if eps[mSTUB] != nil { + stubHandler = eps[mSTUB].handler } // Group methodHandlers by unique patterns @@ -642,12 +657,17 @@ func (n *node) routes() []Route { for p, mh := range pats { hs := make(map[string]http.Handler) + + // Walk() reads Handlers["*"] for With() middleware when recursing + // into a subroute, so keep it there even if it's also the stub. if mh[mALL] != nil && mh[mALL].handler != nil { - hs["*"] = mh[mALL].handler + if subroutes != nil || !equalHandlers(mh[mALL].handler, stubHandler) { + hs["*"] = mh[mALL].handler + } } for mt, h := range mh { - if h.handler == nil { + if h.handler == nil || equalHandlers(h.handler, stubHandler) { continue } if m, ok := reverseMethodMap[mt]; ok { @@ -655,6 +675,11 @@ func (n *node) routes() []Route { } } + // Keep subroute nodes so Walk() can recurse; a stub-only leaf has nothing to report. + if len(hs) == 0 && subroutes == nil { + continue + } + rt := Route{subroutes, hs, p} rts = append(rts, rt) } @@ -665,6 +690,29 @@ func (n *node) routes() []Route { return rts } +// equalHandlers reports whether a and b are the same handler value. Handlers +// are commonly funcs (e.g. http.HandlerFunc), and a direct == on those +// panics at runtime, so funcs are compared by pointer instead. +func equalHandlers(a, b http.Handler) bool { + if a == nil || b == nil { + return a == b + } + + av := reflect.ValueOf(a) + bv := reflect.ValueOf(b) + if av.Type() != bv.Type() { + return false + } + + if av.Kind() == reflect.Func { + return av.Pointer() == bv.Pointer() + } + if av.Type().Comparable() { + return a == b + } + return false +} + func (n *node) walk(fn func(eps endpoints, subroutes Routes) bool) bool { // Visit the leaf values if any if (n.endpoints != nil || n.subroutes != nil) && fn(n.endpoints, n.subroutes) { @@ -770,20 +818,14 @@ func patParamKeys(pattern string) []string { } } -// longestPrefix finds the length of the shared prefix -// of two strings -func longestPrefix(k1, k2 string) int { - max := len(k1) - if l := len(k2); l < max { - max = l - } - var i int - for i = 0; i < max; i++ { +// longestPrefix finds the length of the shared prefix of two strings +func longestPrefix(k1, k2 string) (i int) { + for i = 0; i < min(len(k1), len(k2)); i++ { if k1[i] != k2[i] { break } } - return i + return } type nodes []*node @@ -797,8 +839,8 @@ func (ns nodes) Less(i, j int) bool { return ns[i].label < ns[j].label } // tailSort pushes nodes with '/' as the tail to the end of the list for param nodes. // The list order determines the traversal order. func (ns nodes) tailSort() { - for i := len(ns) - 1; i >= 0; i-- { - if ns[i].typ > ntStatic && ns[i].tail == '/' { + for i, n := range slices.Backward(ns) { + if n.typ > ntStatic && n.tail == '/' { ns.Swap(i, len(ns)-1) return } @@ -843,11 +885,15 @@ func Walk(r Routes, walkFn WalkFunc) error { func walk(r Routes, walkFn WalkFunc, parentRoute string, parentMw ...func(http.Handler) http.Handler) error { for _, route := range r.Routes() { - mws := make([]func(http.Handler) http.Handler, len(parentMw)) - copy(mws, parentMw) - mws = append(mws, r.Middlewares()...) + mws := slices.Concat(parentMw, r.Middlewares()) if route.SubRoutes != nil { + if handler, ok := route.Handlers["*"]; ok { + if chain, ok := handler.(*ChainHandler); ok { + mws = append(mws, chain.Middlewares...) + } + } + if err := walk(route.SubRoutes, walkFn, parentRoute+route.Pattern, mws...); err != nil { return err } @@ -861,7 +907,7 @@ func walk(r Routes, walkFn WalkFunc, parentRoute string, parentMw ...func(http.H } fullRoute := parentRoute + route.Pattern - fullRoute = strings.Replace(fullRoute, "/*/", "/", -1) + fullRoute = strings.ReplaceAll(fullRoute, "/*/", "/") if chain, ok := handler.(*ChainHandler); ok { if err := walkFn(method, fullRoute, chain.Endpoint, append(mws, chain.Middlewares...)...); err != nil { diff --git a/vendor/modules.txt b/vendor/modules.txt index 5253e2172..7fc27c791 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -305,8 +305,8 @@ github.com/fxamacker/cbor/v2 # github.com/ghodss/yaml v1.0.0 ## explicit github.com/ghodss/yaml -# github.com/go-chi/chi/v5 v5.2.3 -## explicit; go 1.20 +# github.com/go-chi/chi/v5 v5.4.0 +## explicit; go 1.24 github.com/go-chi/chi/v5 github.com/go-chi/chi/v5/middleware # github.com/go-errors/errors v1.4.2