diff --git a/go.mod b/go.mod index 8723627..dfe2982 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,4 @@ module go.githedgehog.com/toolbox go 1.26 -require github.com/go-chi/chi/v5 v5.3.1 +require github.com/go-chi/chi/v5 v5.3.2 diff --git a/go.sum b/go.sum index bdb0eaa..5a3b06d 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +1,2 @@ -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/chi/v5 v5.3.2 h1:5YQkICvTCSZ25hoRsyJazN0scjzKGiu4VAUc7H1o1nY= +github.com/go-chi/chi/v5 v5.3.2/go.mod h1:R+tYY2hNuVUUjxoPtqUdgBqevM9s9njzkTLutVsOCto= diff --git a/vendor/github.com/go-chi/chi/v5/README.md b/vendor/github.com/go-chi/chi/v5/README.md index e668e20..07aeaa8 100644 --- a/vendor/github.com/go-chi/chi/v5/README.md +++ b/vendor/github.com/go-chi/chi/v5/README.md @@ -465,6 +465,11 @@ 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. 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 index 1495a86..7e1be57 100644 --- a/vendor/github.com/go-chi/chi/v5/middleware/client_ip.go +++ b/vendor/github.com/go-chi/chi/v5/middleware/client_ip.go @@ -62,7 +62,14 @@ func ClientIPFromHeader(trustedHeader string) func(http.Handler) http.Handler { // 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: +// 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 @@ -109,30 +116,34 @@ func ClientIPFromXFF(trustedIPPrefixes ...string) func(http.Handler) http.Handle } } -// ClientIPFromXFFTrustedProxies stores the client IP read from the -// X-Forwarded-For header, given the exact number of trusted reverse proxies -// between this server and the public internet. It returns the IP at position -// len(xff) - numTrustedProxies in the merged X-Forwarded-For list — the IP -// added by the outermost of your trusted proxies, the only IP in the chain -// that none of your proxies have allowed an attacker to forge. Read it with -// [GetClientIP]. +// 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: // -// Use this when: -// - You know exactly how many proxies you sit behind, AND -// - Their IP addresses are dynamic (autoscaling proxy pools, ephemeral -// containers, dynamic CDN edges) so listing CIDRs with [ClientIPFromXFF] -// is impractical. +// Single proxy (one LB / nginx / Heroku / Fly.io / Render) ....... 1 +// Two proxies (Cloudflare → ALB, CloudFront → ALB) .............. 2 +// Three proxies (CDN → API gateway → LB) ......................... 3 // -// WARNING: This variant is brittle to network architecture changes. If you -// add or remove a proxy level, numTrustedProxies silently becomes wrong and -// you may start trusting an attacker-supplied IP. Prefer [ClientIPFromXFF] -// with explicit trusted CIDRs whenever you can. +// 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. // -// If the XFF chain has fewer than numTrustedProxies entries (header missing -// or architecture changed), no client IP is set and [GetClientIP] returns "". +// 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. // -// Like [ClientIPFromXFF], v4-mapped IPv6 folds to plain v4 and IPv6 zones -// are stripped before storage. +// 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 { 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 2c963c5..d4a26b3 100644 --- a/vendor/github.com/go-chi/chi/v5/middleware/compress.go +++ b/vendor/github.com/go-chi/chi/v5/middleware/compress.go @@ -18,6 +18,9 @@ var defaultCompressibleContentTypes = []string{ "text/css", "text/plain", "text/javascript", + "text/markdown", + "text/csv", + "text/vtt", "application/javascript", "application/x-javascript", "application/json", @@ -62,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. @@ -70,9 +77,12 @@ 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 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{}{} diff --git a/vendor/github.com/go-chi/chi/v5/tree.go b/vendor/github.com/go-chi/chi/v5/tree.go index 74ff43d..e7bc9ed 100644 --- a/vendor/github.com/go-chi/chi/v5/tree.go +++ b/vendor/github.com/go-chi/chi/v5/tree.go @@ -7,6 +7,7 @@ package chi import ( "fmt" "net/http" + "reflect" "regexp" "slices" "sort" @@ -478,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 @@ -524,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 @@ -629,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 @@ -650,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 { @@ -663,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) } @@ -673,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) { diff --git a/vendor/modules.txt b/vendor/modules.txt index 99a8485..f329158 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1,4 +1,4 @@ -# github.com/go-chi/chi/v5 v5.3.1 +# github.com/go-chi/chi/v5 v5.3.2 ## explicit; go 1.23 github.com/go-chi/chi/v5 github.com/go-chi/chi/v5/middleware