diff --git a/go.mod b/go.mod index 50eb5598..7faef57a 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/fortytw2/leaktest v1.3.0 github.com/fsnotify/fsnotify v1.4.9 github.com/getsentry/sentry-go v0.43.0 - github.com/go-chi/chi/v5 v5.2.2 + github.com/go-chi/chi/v5 v5.3.1 github.com/go-chi/cors v1.2.1 github.com/go-jose/go-jose/v4 v4.1.4 github.com/gobwas/ws v1.2.1 diff --git a/go.sum b/go.sum index 398bae26..41c8d8dd 100644 --- a/go.sum +++ b/go.sum @@ -54,8 +54,8 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= -github.com/go-chi/chi/v5 v5.2.2 h1:CMwsvRVTbXVytCk1Wd72Zy1LAsAh9GxMmSNWLHCG618= -github.com/go-chi/chi/v5 v5.2.2/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +github.com/go-chi/chi/v5 v5.3.1 h1:3j4HZLGZQ3JpMCrPJF/Jl3mYJfWLKBfNJ6quurUGCf8= +github.com/go-chi/chi/v5 v5.3.1/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= github.com/go-chi/cors v1.2.1 h1:xEC8UT3Rlp2QuWNEr4Fs/c2EAGVKBwy/1vHx3bppil4= github.com/go-chi/cors v1.2.1/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= diff --git a/vendor/github.com/go-chi/chi/v5/README.md b/vendor/github.com/go-chi/chi/v5/README.md index c58a0e20..e668e204 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 | @@ -375,6 +380,12 @@ with `net/http` can be used with chi's mux. [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 +[ClientIPFromHeader]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ClientIPFromHeader +[ClientIPFromXFF]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ClientIPFromXFF +[ClientIPFromXFFTrustedProxies]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ClientIPFromXFFTrustedProxies +[ClientIPFromRemoteAddr]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ClientIPFromRemoteAddr +[GetClientIP]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetClientIP +[GetClientIPAddr]: https://pkg.go.dev/github.com/go-chi/chi/middleware#GetClientIPAddr [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 @@ -402,6 +413,62 @@ with `net/http` can be used with chi's mux. [ThrottleOpts]: https://pkg.go.dev/github.com/go-chi/chi/middleware#ThrottleOpts [WrapResponseWriter]: https://pkg.go.dev/github.com/go-chi/chi/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. + +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 Please see https://github.com/go-chi for additional packages. diff --git a/vendor/github.com/go-chi/chi/v5/chi.go b/vendor/github.com/go-chi/chi/v5/chi.go index 2b6ebd33..cb129e30 100644 --- a/vendor/github.com/go-chi/chi/v5/chi.go +++ b/vendor/github.com/go-chi/chi/v5/chi.go @@ -1,6 +1,6 @@ // 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: // @@ -77,7 +77,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 +102,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 aacf6eff..82220730 100644 --- a/vendor/github.com/go-chi/chi/v5/context.go +++ b/vendor/github.com/go-chi/chi/v5/context.go @@ -133,11 +133,12 @@ func (x *Context) RoutePattern() string { return routePattern } -// replaceWildcards takes a route pattern and recursively replaces all -// occurrences of "/*/" to "/". +// replaceWildcards takes a route pattern and replaces all occurrences of +// "/*/" with "/". It iteratively runs until no wildcards remain to +// correctly handle consecutive wildcards. func replaceWildcards(p string) string { - if strings.Contains(p, "/*/") { - return replaceWildcards(strings.Replace(p, "/*/", "/", -1)) + for strings.Contains(p, "/*/") { + p = strings.ReplaceAll(p, "/*/", "/") } return p } diff --git a/vendor/github.com/go-chi/chi/v5/mux.go b/vendor/github.com/go-chi/chi/v5/mux.go index f1266971..37cd5004 100644 --- a/vendor/github.com/go-chi/chi/v5/mux.go +++ b/vendor/github.com/go-chi/chi/v5/mux.go @@ -107,7 +107,8 @@ func (mx *Mux) Use(middlewares ...func(http.Handler) http.Handler) { // Handle adds the route `pattern` that matches any http method to // execute the `handler` http.Handler. func (mx *Mux) Handle(pattern string, handler http.Handler) { - if method, rest, found := strings.Cut(pattern, " "); found { + if i := strings.IndexAny(pattern, " \t"); i >= 0 { + method, rest := pattern[:i], strings.TrimLeft(pattern[i+1:], " \t") mx.Method(method, rest, handler) return } @@ -118,12 +119,7 @@ func (mx *Mux) Handle(pattern string, handler http.Handler) { // HandleFunc adds the route `pattern` that matches any http method to // execute the `handlerFn` http.HandlerFunc. func (mx *Mux) HandleFunc(pattern string, handlerFn http.HandlerFunc) { - if method, rest, found := strings.Cut(pattern, " "); found { - mx.Method(method, rest, handlerFn) - return - } - - mx.handle(mALL, pattern, handlerFn) + mx.Handle(pattern, handlerFn) } // Method adds the route `pattern` that matches `method` http method to @@ -190,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) { @@ -471,9 +473,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) + // 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 77c840f0..00000000 --- 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 749a8520..00000000 --- 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/tree.go b/vendor/github.com/go-chi/chi/v5/tree.go index 85fcfdbb..74ff43db 100644 --- a/vendor/github.com/go-chi/chi/v5/tree.go +++ b/vendor/github.com/go-chi/chi/v5/tree.go @@ -8,6 +8,7 @@ import ( "fmt" "net/http" "regexp" + "slices" "sort" "strconv" "strings" @@ -25,11 +26,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 +47,7 @@ var methodMap = map[string]methodTyp{ http.MethodPatch: mPATCH, http.MethodPost: mPOST, http.MethodPut: mPUT, + methodQuery: mQUERY, http.MethodTrace: mTRACE, } @@ -52,6 +60,7 @@ var reverseMethodMap = map[methodTyp]string{ mPATCH: http.MethodPatch, mPOST: http.MethodPost, mPUT: http.MethodPut, + mQUERY: methodQuery, mTRACE: http.MethodTrace, } @@ -71,6 +80,7 @@ func RegisterMethod(method string) { } mt := methodTyp(2 << n) methodMap[method] = mt + reverseMethodMap[mt] = method mALL |= mt } @@ -328,7 +338,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 +439,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) @@ -650,11 +658,9 @@ func (n *node) routes() []Route { if h.handler == nil { continue } - m := methodTypString(mt) - if m == "" { - continue + if m, ok := reverseMethodMap[mt]; ok { + hs[m] = h.handler } - hs[m] = h.handler } rt := Route{subroutes, hs, p} @@ -772,29 +778,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 -} - -func methodTypString(method methodTyp) string { - for s, t := range methodMap { - if method == t { - return s - } - } - return "" + return } type nodes []*node @@ -854,11 +845,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 } @@ -872,7 +867,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 f9c3ce02..1a13a8f5 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -64,8 +64,8 @@ github.com/getsentry/sentry-go/internal/telemetry github.com/getsentry/sentry-go/internal/util # github.com/gin-gonic/gin v1.9.1 ## explicit; go 1.20 -# github.com/go-chi/chi/v5 v5.2.2 -## explicit; go 1.20 +# github.com/go-chi/chi/v5 v5.3.1 +## explicit; go 1.23 github.com/go-chi/chi/v5 # github.com/go-chi/cors v1.2.1 ## explicit; go 1.14