diff --git a/go.mod b/go.mod index d0e11fc..0d482cd 100644 --- a/go.mod +++ b/go.mod @@ -6,7 +6,7 @@ require ( github.com/fatih/color v1.18.0 github.com/go-pkgz/lgr v0.12.1 github.com/go-pkgz/repeater/v2 v2.2.0 - github.com/go-pkgz/rest v1.20.4 + github.com/go-pkgz/rest v1.20.6 github.com/go-pkgz/routegroup v1.6.0 github.com/invopop/jsonschema v0.13.0 github.com/jessevdk/go-flags v1.6.1 diff --git a/go.sum b/go.sum index b0ab648..650a13e 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,8 @@ github.com/go-pkgz/lgr v0.12.1 h1:8GVfG2rSARq3Eaj5PP158rtBR2LHVGkwioIkQBGbvKg= github.com/go-pkgz/lgr v0.12.1/go.mod h1:A4AxjOthFVFK6jRnVYMeusno5SeDAxcLVHd0kI/lN/Y= github.com/go-pkgz/repeater/v2 v2.2.0 h1:8nZR/NaknmLfx2YMHbr78u9OL4Xj+8+romm9dz4FpMg= github.com/go-pkgz/repeater/v2 v2.2.0/go.mod h1:RgX5vUbLKq7PV82QUDP5pFbQS1os4Z+U9XzKymK23A8= -github.com/go-pkgz/rest v1.20.4 h1:8ufcP1IqoDhCvIFdXPtvyX4HSS16SM6THBe2a6L0/kg= -github.com/go-pkgz/rest v1.20.4/go.mod h1:2/LEZGndSxpVvExsMn48AjUgiTn6kILqjpoaRnl62JU= +github.com/go-pkgz/rest v1.20.6 h1:O/IhQ3I2cS4bJYvL1TLcy63w2OcXZTTBG3R+wTIqPS4= +github.com/go-pkgz/rest v1.20.6/go.mod h1:NY+MX1is2kJckJt+nHDNovS/5j9jmF4yQuSno4qg7XU= github.com/go-pkgz/routegroup v1.6.0 h1:44XHZgF6JIIldRlv+zjg6SygULASmjifnfIQjwCT0e4= github.com/go-pkgz/routegroup v1.6.0/go.mod h1:Pmu04fhgWhRtBMIJ8HXppnnzOPjnL/IEPBIdO2zmeqg= github.com/go-shiori/dom v0.0.0-20230515143342-73569d674e1c h1:wpkoddUomPfHiOziHZixGO5ZBS73cKqVzZipfrLmO1w= diff --git a/vendor/github.com/go-pkgz/rest/.golangci.yml b/vendor/github.com/go-pkgz/rest/.golangci.yml index ec36f9d..bd6fb4a 100644 --- a/vendor/github.com/go-pkgz/rest/.golangci.yml +++ b/vendor/github.com/go-pkgz/rest/.golangci.yml @@ -1,66 +1,63 @@ +version: "2" run: timeout: 5m tests: false - -linters-settings: - govet: - enable: - - shadow - goconst: - min-len: 2 - min-occurrences: 2 - misspell: - locale: US - lll: - line-length: 140 - gocritic: - enabled-tags: - - performance - - style - - experimental - disabled-checks: - - wrapperFunc - - hugeParam - - rangeValCopy - - singleCaseSwitch - - ifElseChain - + concurrency: 4 linters: + default: none enable: - - revive - - govet - - unconvert - - staticcheck - - unused - - gosec - dupl - - misspell - - unparam - - typecheck - - ineffassign - - stylecheck - gochecknoinits - - copyloopvar - gocritic + - govet + - ineffassign + - misspell - nakedret - - gosimple - prealloc - fast: false - disable-all: true - -issues: - exclude-dirs: - - vendor - exclude-rules: - - text: "at least one file in a package should have a package comment" - linters: - - stylecheck - - text: "should have a package comment" - linters: - - revive - - path: _test\.go - linters: - - gosec - - dupl - exclude-use-default: false + - revive + - staticcheck + - unconvert + - unparam + - unused + settings: + goconst: + min-len: 2 + min-occurrences: 2 + gocritic: + disabled-checks: + - wrapperFunc + - hugeParam + - rangeValCopy + - singleCaseSwitch + - ifElseChain + enabled-tags: + - performance + - style + - experimental + govet: + enable-all: true + disable: + - fieldalignment + lll: + line-length: 140 + misspell: + locale: US + exclusions: + generated: lax + rules: + - linters: + - staticcheck + text: "at least one file in a package should have a package comment" + - linters: + - revive + text: "should have a package comment" + - linters: + - gosec + - dupl + path: _test\.go + paths: + - vendor + - third_party$ + - builtin$ + - examples$ diff --git a/vendor/github.com/go-pkgz/rest/README.md b/vendor/github.com/go-pkgz/rest/README.md index d72d478..1260cf8 100644 --- a/vendor/github.com/go-pkgz/rest/README.md +++ b/vendor/github.com/go-pkgz/rest/README.md @@ -157,7 +157,14 @@ Compresses response with gzip. ### RealIP middleware -RealIP is a middleware that sets a http.Request's RemoteAddr to the results of parsing either the X-Forwarded-For or X-Real-IP headers. +RealIP is a middleware that sets a http.Request's RemoteAddr to the results of parsing various headers that contain the client's real IP address. It checks headers in the following priority order: + +1. `X-Real-IP` - trusted proxy (nginx/reproxy) sets this to actual client +2. `CF-Connecting-IP` - Cloudflare's header for original client +3. `X-Forwarded-For` - leftmost public IP (original client in CDN/proxy chain) +4. `RemoteAddr` - fallback for direct connections + +Only public IPs are accepted from headers; private/loopback/link-local IPs are skipped. This makes the middleware compatible with CDN setups like Cloudflare where the leftmost IP in `X-Forwarded-For` is the actual client. ### Maybe middleware diff --git a/vendor/github.com/go-pkgz/rest/blackwords.go b/vendor/github.com/go-pkgz/rest/blackwords.go index 01c8502..8954db3 100644 --- a/vendor/github.com/go-pkgz/rest/blackwords.go +++ b/vendor/github.com/go-pkgz/rest/blackwords.go @@ -20,8 +20,7 @@ func BlackWords(words ...string) func(http.Handler) http.Handler { if body != "" { for _, word := range words { if strings.Contains(body, strings.ToLower(word)) { - w.WriteHeader(http.StatusForbidden) - RenderJSON(w, JSON{"error": "one of blacklisted words detected"}) + _ = EncodeJSON(w, http.StatusForbidden, JSON{"error": "one of blacklisted words detected"}) return } } diff --git a/vendor/github.com/go-pkgz/rest/file_server.go b/vendor/github.com/go-pkgz/rest/file_server.go index ccf47df..088ac9d 100644 --- a/vendor/github.com/go-pkgz/rest/file_server.go +++ b/vendor/github.com/go-pkgz/rest/file_server.go @@ -68,12 +68,14 @@ func NewFileServer(public, local string, options ...FsOpt) (*FS, error) { } // FileServer is a shortcut for making FS with listing disabled and the custom noFound reader (can be nil). +// // Deprecated: the method is for back-compatibility only and user should use the universal NewFileServer instead func FileServer(public, local string, notFound io.Reader) (http.Handler, error) { return NewFileServer(public, local, FsOptCustom404(notFound)) } // FileServerSPA is a shortcut for making FS with SPA-friendly handling of 404, listing disabled and the custom noFound reader (can be nil). +// // Deprecated: the method is for back-compatibility only and user should use the universal NewFileServer instead func FileServerSPA(public, local string, notFound io.Reader) (http.Handler, error) { return NewFileServer(public, local, FsOptCustom404(notFound), FsOptSPA) diff --git a/vendor/github.com/go-pkgz/rest/logger/logger.go b/vendor/github.com/go-pkgz/rest/logger/logger.go index e814663..d93342c 100644 --- a/vendor/github.com/go-pkgz/rest/logger/logger.go +++ b/vendor/github.com/go-pkgz/rest/logger/logger.go @@ -220,8 +220,8 @@ func (l *Middleware) getBody(r *http.Request) string { // "The Server will close the request body. The ServeHTTP Handler does not need to." // https://golang.org/pkg/net/http/#Request - // So we can use ioutil.NopCloser() to make io.ReadCloser. - // Note that below assignment is not approved by the docs: + // so we can use ioutil.NopCloser() to make io.ReadCloser. + // note that below assignment is not approved by the docs: // "Except for reading the body, handlers should not modify the provided Request." // https://golang.org/pkg/net/http/#Handler r.Body = io.NopCloser(reader) @@ -355,5 +355,5 @@ func (c *customResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { if hj, ok := c.ResponseWriter.(http.Hijacker); ok { return hj.Hijack() } - return nil, nil, fmt.Errorf("ResponseWriter does not implement the Hijacker interface") //nolint:golint //capital letter is OK here + return nil, nil, fmt.Errorf("ResponseWriter does not implement the Hijacker interface") } diff --git a/vendor/github.com/go-pkgz/rest/metrics.go b/vendor/github.com/go-pkgz/rest/metrics.go index 1cf0a41..6edc2da 100644 --- a/vendor/github.com/go-pkgz/rest/metrics.go +++ b/vendor/github.com/go-pkgz/rest/metrics.go @@ -13,8 +13,7 @@ func Metrics(onlyIps ...string) func(http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" && strings.HasSuffix(strings.ToLower(r.URL.Path), "/metrics") { if matched, ip, err := matchSourceIP(r, onlyIps); !matched || err != nil { - w.WriteHeader(http.StatusForbidden) - RenderJSON(w, JSON{"error": fmt.Sprintf("ip %s rejected", ip)}) + _ = EncodeJSON(w, http.StatusForbidden, JSON{"error": fmt.Sprintf("ip %s rejected", ip)}) return } expvar.Handler().ServeHTTP(w, r) diff --git a/vendor/github.com/go-pkgz/rest/middleware.go b/vendor/github.com/go-pkgz/rest/middleware.go index 86b3cc1..c130a46 100644 --- a/vendor/github.com/go-pkgz/rest/middleware.go +++ b/vendor/github.com/go-pkgz/rest/middleware.go @@ -19,7 +19,6 @@ func Wrap(handler http.Handler, mws ...func(http.Handler) http.Handler) http.Han return handler } - // AppInfo adds custom app-info to the response header func AppInfo(app, author, version string) func(http.Handler) http.Handler { f := func(h http.Handler) http.Handler { @@ -82,12 +81,11 @@ func Health(path string, checkers ...func(ctx context.Context) (name string, err } resp = append(resp, hh) } + status := http.StatusOK if anyError { - w.WriteHeader(http.StatusServiceUnavailable) - } else { - w.WriteHeader(http.StatusOK) + status = http.StatusServiceUnavailable } - RenderJSON(w, resp) + _ = EncodeJSON(w, status, resp) } return http.HandlerFunc(fn) } @@ -147,13 +145,19 @@ func Maybe(mw func(http.Handler) http.Handler, maybeFn func(r *http.Request) boo } } -// RealIP is a middleware that sets a http.Request's RemoteAddr to the results -// of parsing either the X-Forwarded-For or X-Real-IP headers. +// RealIP is a middleware that sets a http.Request's RemoteAddr to the client's real IP. +// It checks headers in the following priority order: +// 1. X-Real-IP - trusted proxy (nginx/reproxy) sets this to actual client +// 2. CF-Connecting-IP - Cloudflare's header for original client +// 3. X-Forwarded-For - leftmost public IP (original client in CDN/proxy chain) +// 4. RemoteAddr - fallback for direct connections +// +// Only public IPs are accepted from headers; private/loopback/link-local IPs are skipped. // // This middleware should only be used if user can trust the headers sent with request. // If reverse proxies are configured to pass along arbitrary header values from the client, // or if this middleware used without a reverse proxy, malicious clients could set anything -// as X-Forwarded-For header and attack the server in various ways. +// as these headers and spoof their IP address. func RealIP(h http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { if rip, err := realip.Get(r); err == nil { diff --git a/vendor/github.com/go-pkgz/rest/nocache.go b/vendor/github.com/go-pkgz/rest/nocache.go index aa0fa94..9808a0e 100644 --- a/vendor/github.com/go-pkgz/rest/nocache.go +++ b/vendor/github.com/go-pkgz/rest/nocache.go @@ -29,21 +29,22 @@ var etagHeaders = []string{ // a router (or subrouter) from being cached by an upstream proxy and/or client. // // As per http://wiki.nginx.org/HttpProxyModule - NoCache sets: -// Expires: Thu, 01 Jan 1970 00:00:00 UTC -// Cache-Control: no-cache, private, max-age=0 -// X-Accel-Expires: 0 -// Pragma: no-cache (for HTTP/1.0 proxies/clients) +// +// Expires: Thu, 01 Jan 1970 00:00:00 UTC +// Cache-Control: no-cache, private, max-age=0 +// X-Accel-Expires: 0 +// Pragma: no-cache (for HTTP/1.0 proxies/clients) func NoCache(h http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { - // Delete any ETag headers that may have been set + // delete any ETag headers that may have been set for _, v := range etagHeaders { if r.Header.Get(v) != "" { r.Header.Del(v) } } - // Set our NoCache headers + // set our NoCache headers for k, v := range noCacheHeaders { w.Header().Set(k, v) } diff --git a/vendor/github.com/go-pkgz/rest/onlyfrom.go b/vendor/github.com/go-pkgz/rest/onlyfrom.go index cccd77e..acc8ff2 100644 --- a/vendor/github.com/go-pkgz/rest/onlyfrom.go +++ b/vendor/github.com/go-pkgz/rest/onlyfrom.go @@ -21,8 +21,7 @@ func OnlyFrom(onlyIps ...string) func(http.Handler) http.Handler { } matched, ip, err := matchSourceIP(r, onlyIps) if err != nil { - w.WriteHeader(http.StatusInternalServerError) - RenderJSON(w, JSON{"error": fmt.Sprintf("can't get realip: %s", err)}) + _ = EncodeJSON(w, http.StatusInternalServerError, JSON{"error": fmt.Sprintf("can't get realip: %s", err)}) return } if matched { @@ -31,8 +30,7 @@ func OnlyFrom(onlyIps ...string) func(http.Handler) http.Handler { return } - w.WriteHeader(http.StatusForbidden) - RenderJSON(w, JSON{"error": fmt.Sprintf("ip %q rejected", ip)}) + _ = EncodeJSON(w, http.StatusForbidden, JSON{"error": fmt.Sprintf("ip %q rejected", ip)}) } return http.HandlerFunc(fn) } diff --git a/vendor/github.com/go-pkgz/rest/realip/real.go b/vendor/github.com/go-pkgz/rest/realip/real.go index eb7a952..ad10149 100644 --- a/vendor/github.com/go-pkgz/rest/realip/real.go +++ b/vendor/github.com/go-pkgz/rest/realip/real.go @@ -33,49 +33,73 @@ var privateRanges = []ipRange{ {start: net.ParseIP("fe80::"), end: net.ParseIP("febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff")}, } -// Get returns real ip from the given request -// Prioritize public IPs over private IPs +// Get returns real IP from the given request. +// It checks headers in the following priority order: +// 1. X-Real-IP - trusted proxy (nginx/reproxy) sets this to actual client +// 2. CF-Connecting-IP - Cloudflare's header for original client +// 3. X-Forwarded-For - leftmost public IP (original client in CDN chain) +// 4. RemoteAddr - fallback for direct connections +// +// Only public IPs are accepted from headers; private/loopback/link-local IPs are skipped. func Get(r *http.Request) (string, error) { - var firstIP string - for _, h := range []string{"X-Forwarded-For", "X-Real-Ip"} { - addresses := strings.Split(r.Header.Get(h), ",") - for i := len(addresses) - 1; i >= 0; i-- { - ip := strings.TrimSpace(addresses[i]) - realIP := net.ParseIP(ip) - if firstIP == "" && realIP != nil { - firstIP = ip - } - // Guard against nil realIP - if realIP == nil || !realIP.IsGlobalUnicast() || isPrivateSubnet(realIP) { - continue + // check X-Real-IP first (single value, set by trusted proxy) + if xRealIP := strings.TrimSpace(r.Header.Get("X-Real-IP")); xRealIP != "" { + if ip := net.ParseIP(xRealIP); isPublicIP(ip) { + return xRealIP, nil + } + } + + // check CF-Connecting-IP (Cloudflare's header) + if cfIP := strings.TrimSpace(r.Header.Get("CF-Connecting-IP")); cfIP != "" { + if ip := net.ParseIP(cfIP); isPublicIP(ip) { + return cfIP, nil + } + } + + // check X-Forwarded-For, find leftmost public IP + if xff := r.Header.Get("X-Forwarded-For"); xff != "" { + addresses := strings.Split(xff, ",") + for _, addr := range addresses { + ip := strings.TrimSpace(addr) + if parsedIP := net.ParseIP(ip); isPublicIP(parsedIP) { + return ip, nil } - return ip, nil } } - if firstIP != "" { - return firstIP, nil + // fall back to RemoteAddr + return parseRemoteAddr(r.RemoteAddr) +} + +// isPublicIP checks if the IP is a valid public (globally routable) IP address. +func isPublicIP(ip net.IP) bool { + if ip == nil { + return false + } + if !ip.IsGlobalUnicast() { + return false } + return !isPrivateSubnet(ip) +} - // handle RemoteAddr which may be just an IP or IP:port - remoteIP := r.RemoteAddr +// parseRemoteAddr extracts and validates IP from RemoteAddr (handles both "ip" and "ip:port" formats). +func parseRemoteAddr(remoteAddr string) (string, error) { + if remoteAddr == "" { + return "", fmt.Errorf("empty remote address") + } // try to extract host from host:port format - host, _, err := net.SplitHostPort(remoteIP) + host, _, err := net.SplitHostPort(remoteAddr) if err == nil { - remoteIP = host + remoteAddr = host } - // at this point remoteIP could be either: - // 1. the host part extracted from host:port - // 2. yhe original RemoteAddr if it doesn't contain a port - - // try to parse it as a valid IP address - if netIP := net.ParseIP(remoteIP); netIP == nil { - return "", fmt.Errorf("no valid ip found in %q", r.RemoteAddr) + // validate it's a proper IP address + if netIP := net.ParseIP(remoteAddr); netIP == nil { + return "", fmt.Errorf("no valid ip found in %q", remoteAddr) } - return remoteIP, nil + return remoteAddr, nil } // isPrivateSubnet - check to see if this ip is in a private subnet diff --git a/vendor/modules.txt b/vendor/modules.txt index 1b0f479..913d1fe 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -38,8 +38,8 @@ github.com/go-pkgz/lgr # github.com/go-pkgz/repeater/v2 v2.2.0 ## explicit; go 1.23 github.com/go-pkgz/repeater/v2 -# github.com/go-pkgz/rest v1.20.4 -## explicit; go 1.23.0 +# github.com/go-pkgz/rest v1.20.6 +## explicit; go 1.24.0 github.com/go-pkgz/rest github.com/go-pkgz/rest/logger github.com/go-pkgz/rest/realip