diff --git a/go.mod b/go.mod index ac48311..bafa0bd 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.26.0 require ( github.com/getkin/kin-openapi v0.140.0 - github.com/go-chi/chi/v5 v5.3.0 + github.com/go-chi/chi/v5 v5.3.2 github.com/google/uuid v1.6.0 github.com/hashicorp/nomad/api v0.0.0-20260220212019-daca79db0bd6 github.com/jedib0t/go-pretty/v6 v6.8.1 diff --git a/go.sum b/go.sum index 60508eb..119f949 100644 --- a/go.sum +++ b/go.sum @@ -11,8 +11,8 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2 github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/getkin/kin-openapi v0.140.0 h1:JFn675aXRFjyiZKa/BFWploGldQlI0gobp4J5k0EZ2g= github.com/getkin/kin-openapi v0.140.0/go.mod h1:lISrB64F0CPcuDJ3LdtPTMJBY8VENjR9wJBdrcT6J3g= -github.com/go-chi/chi/v5 v5.3.0 h1:halUjDxhshgXHMrao5bB8eNBXo/rnzwr8m5m36glehM= -github.com/go-chi/chi/v5 v5.3.0/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= github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= diff --git a/vendor/github.com/go-chi/chi/v5/README.md b/vendor/github.com/go-chi/chi/v5/README.md index a116596..07aeaa8 100644 --- a/vendor/github.com/go-chi/chi/v5/README.md +++ b/vendor/github.com/go-chi/chi/v5/README.md @@ -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 @@ -464,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/chi.go b/vendor/github.com/go-chi/chi/v5/chi.go index ad0ca74..cb129e3 100644 --- a/vendor/github.com/go-chi/chi/v5/chi.go +++ b/vendor/github.com/go-chi/chi/v5/chi.go @@ -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/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 4e46f70..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,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,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/middleware/logger.go b/vendor/github.com/go-chi/chi/v5/middleware/logger.go index 4d30a9a..ba5391f 100644 --- a/vendor/github.com/go-chi/chi/v5/middleware/logger.go +++ b/vendor/github.com/go-chi/chi/v5/middleware/logger.go @@ -166,7 +166,7 @@ func (l *defaultLogEntry) Write(status, bytes int, header http.Header, elapsed t } func (l *defaultLogEntry) Panic(v interface{}, stack []byte) { - PrintPrettyStack(v) + 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 0ad6a99..5784ff6 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/recoverer.go b/vendor/github.com/go-chi/chi/v5/middleware/recoverer.go index 81342df..ba77de7 100644 --- a/vendor/github.com/go-chi/chi/v5/middleware/recoverer.go +++ b/vendor/github.com/go-chi/chi/v5/middleware/recoverer.go @@ -52,9 +52,16 @@ func Recoverer(next http.Handler) http.Handler { var recovererErrorWriter io.Writer = os.Stderr func PrintPrettyStack(rvr interface{}) { + 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 interface{}, 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 interface{}, 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/wrap_writer.go b/vendor/github.com/go-chi/chi/v5/middleware/wrap_writer.go index b2de875..936ce7e 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 @@ -207,10 +207,12 @@ func (f *http2FancyWriter) Push(target string, opts *http.PushOptions) error { } func (f *httpFancyWriter) ReadFrom(r io.Reader) (int64, error) { - if f.basicWriter.tee != nil { - // Route through basicWriter.Write so that data is also written to the - // tee writer. basicWriter.Write already increments basicWriter.bytes, - // so we must NOT add n again here (that would double-count). + 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) return n, err } diff --git a/vendor/github.com/go-chi/chi/v5/mux.go b/vendor/github.com/go-chi/chi/v5/mux.go index 3da7f3f..37cd500 100644 --- a/vendor/github.com/go-chi/chi/v5/mux.go +++ b/vendor/github.com/go-chi/chi/v5/mux.go @@ -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) { diff --git a/vendor/github.com/go-chi/chi/v5/tree.go b/vendor/github.com/go-chi/chi/v5/tree.go index 95f31d4..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" @@ -26,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, @@ -41,6 +48,7 @@ var methodMap = map[string]methodTyp{ http.MethodPatch: mPATCH, http.MethodPost: mPOST, http.MethodPut: mPUT, + methodQuery: mQUERY, http.MethodTrace: mTRACE, } @@ -53,6 +61,7 @@ var reverseMethodMap = map[methodTyp]string{ mPATCH: http.MethodPatch, mPOST: http.MethodPost, mPUT: http.MethodPut, + mQUERY: methodQuery, mTRACE: http.MethodTrace, } @@ -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) { diff --git a/vendor/modules.txt b/vendor/modules.txt index d41bd12..b3b57bb 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -7,7 +7,7 @@ github.com/davecgh/go-spew/spew # github.com/getkin/kin-openapi v0.140.0 ## explicit; go 1.25 github.com/getkin/kin-openapi/openapi3 -# github.com/go-chi/chi/v5 v5.3.0 +# 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