refactor: consolidate RemoteAddr/trust helpers and host-port mapping
- Export ExtractRemoteIP, add IsTrustedOrLocal and HTTPSAuthority in middleware/cidr.go - Remove duplicated extractDirectIP and isTrustedOrLocal from run.go - Rewrite httpsRedirectTarget and onboarding publicHTTPSURL to use shared HTTPSAuthority helper - Remove plan-artifact comments from test files
- commit
- 29b95785c74c049b00f7cc421b3164717a53b1c4
- author
- brice · · 2026-04-01 12:00 UTC
- parent
- 8739892
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +39 -65
M internal/adapters/in/http/middleware/cidr.go +33 -21
| @@ -51,8 +51,8 @@ func cidrAllowlist(allowedNets []*net.IPNet, ipExtractor func(*http.Request) str | |||
| 51 | 51 | } | |
| 52 | 52 | } | |
| 53 | 53 | ||
| 54 | - | // extractRemoteIP returns the IP portion of r.RemoteAddr, stripping the port. | |
| 55 | - | func extractRemoteIP(remoteAddr string) string { | |
| 54 | + | // ExtractRemoteIP returns the IP portion of a RemoteAddr, stripping the port. | |
| 55 | + | func ExtractRemoteIP(remoteAddr string) string { | |
| 56 | 56 | host, _, err := net.SplitHostPort(remoteAddr) | |
| 57 | 57 | if err != nil { | |
| 58 | 58 | return remoteAddr | |
| @@ -60,6 +60,34 @@ func extractRemoteIP(remoteAddr string) string { | |||
| 60 | 60 | return host | |
| 61 | 61 | } | |
| 62 | 62 | ||
| 63 | + | // IsTrustedOrLocal reports whether an IP belongs to localhost or trusted proxy nets. | |
| 64 | + | func IsTrustedOrLocal(ip string, proxyNets []*net.IPNet) bool { | |
| 65 | + | return IsTrustedProxy(ip, localhostNets) || IsTrustedProxy(ip, proxyNets) | |
| 66 | + | } | |
| 67 | + | ||
| 68 | + | // HTTPSAuthority converts an incoming Host header plus httpPort/tlsPort into the | |
| 69 | + | // correct HTTPS authority (host or host:port). | |
| 70 | + | // | |
| 71 | + | // Rules: | |
| 72 | + | // - No port in Host → omit TLS port (public reverse-proxy assumed on :443). | |
| 73 | + | // - Host port == httpPort → map to tlsPort. | |
| 74 | + | // - Any other explicit port → preserve it. | |
| 75 | + | func HTTPSAuthority(host string, httpPort, tlsPort int) string { | |
| 76 | + | hostname, portStr, err := net.SplitHostPort(host) | |
| 77 | + | if err != nil { | |
| 78 | + | // No port in Host header — omit TLS port from URL. | |
| 79 | + | return host | |
| 80 | + | } | |
| 81 | + | ||
| 82 | + | port, _ := strconv.Atoi(portStr) | |
| 83 | + | if port == httpPort { | |
| 84 | + | return net.JoinHostPort(hostname, strconv.Itoa(tlsPort)) | |
| 85 | + | } | |
| 86 | + | ||
| 87 | + | // Unknown explicit port — preserve it. | |
| 88 | + | return net.JoinHostPort(hostname, portStr) | |
| 89 | + | } | |
| 90 | + | ||
| 63 | 91 | // RegistryCIDRAllowlist returns middleware that restricts access to the given CIDR ranges. | |
| 64 | 92 | // Localhost is always allowed so Gordon can pull from its own registry. | |
| 65 | 93 | // An empty allowedNets slice is a no-op (all traffic passes through). | |
| @@ -93,7 +121,7 @@ func HTTPSRedirect(proxyNets []*net.IPNet, httpPort, tlsPort int, forceAll bool, | |||
| 93 | 121 | } | |
| 94 | 122 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 95 | 123 | if !forceAll { | |
| 96 | - | remoteIP := extractRemoteIP(r.RemoteAddr) | |
| 124 | + | remoteIP := ExtractRemoteIP(r.RemoteAddr) | |
| 97 | 125 | if IsTrustedProxy(remoteIP, localhostNets) || IsTrustedProxy(remoteIP, proxyNets) { | |
| 98 | 126 | next.ServeHTTP(w, r) | |
| 99 | 127 | return | |
| @@ -112,29 +140,13 @@ func HTTPSRedirect(proxyNets []*net.IPNet, httpPort, tlsPort int, forceAll bool, | |||
| 112 | 140 | } | |
| 113 | 141 | ||
| 114 | 142 | // httpsRedirectTarget derives the HTTPS redirect URL from the request Host. | |
| 115 | - | // | |
| 116 | - | // - No port in Host → omit TLS port (public reverse-proxy assumed on :443). | |
| 117 | - | // - Host port == httpPort → map to tlsPort. | |
| 118 | - | // - Any other explicit port → preserve it. | |
| 119 | 143 | func httpsRedirectTarget(host, requestURI string, httpPort, tlsPort int) string { | |
| 120 | 144 | path := requestURI | |
| 121 | 145 | if !strings.HasPrefix(path, "/") { | |
| 122 | 146 | path = "/" | |
| 123 | 147 | } | |
| 124 | 148 | ||
| 125 | - | hostname, portStr, err := net.SplitHostPort(host) | |
| 126 | - | if err != nil { | |
| 127 | - | // No port in Host header — omit TLS port from URL. | |
| 128 | - | return fmt.Sprintf("https://%s%s", host, path) | |
| 129 | - | } | |
| 130 | - | ||
| 131 | - | port, _ := strconv.Atoi(portStr) | |
| 132 | - | if port == httpPort { | |
| 133 | - | return fmt.Sprintf("https://%s:%d%s", hostname, tlsPort, path) | |
| 134 | - | } | |
| 135 | - | ||
| 136 | - | // Unknown explicit port — preserve it. | |
| 137 | - | return fmt.Sprintf("https://%s:%s%s", hostname, portStr, path) | |
| 149 | + | return fmt.Sprintf("https://%s%s", HTTPSAuthority(host, httpPort, tlsPort), path) | |
| 138 | 150 | } | |
| 139 | 151 | ||
| 140 | 152 | // ProxyCIDRAllowlist returns middleware that restricts proxy access to the given CIDR ranges. | |
| @@ -143,6 +155,6 @@ func httpsRedirectTarget(host, requestURI string, httpPort, tlsPort int) string | |||
| 143 | 155 | // An empty allowedNets slice is a no-op (all traffic passes through). | |
| 144 | 156 | func ProxyCIDRAllowlist(allowedNets []*net.IPNet, log zerowrap.Logger) func(http.Handler) http.Handler { | |
| 145 | 157 | return cidrAllowlist(allowedNets, func(r *http.Request) string { | |
| 146 | - | return extractRemoteIP(r.RemoteAddr) | |
| 158 | + | return ExtractRemoteIP(r.RemoteAddr) | |
| 147 | 159 | }, "proxy origin", log) | |
| 148 | 160 | } | |
| @@ -51,8 +51,8 @@ func cidrAllowlist(allowedNets []*net.IPNet, ipExtractor func(*http.Request) str | |||||
| 51 | } | 51 | } | ||
| 52 | } | 52 | } | ||
| 53 | 53 | ||||
| 54 | - | // extractRemoteIP returns the IP portion of r.RemoteAddr, stripping the port. | 54 | + | // ExtractRemoteIP returns the IP portion of a RemoteAddr, stripping the port. |
| 55 | - | func extractRemoteIP(remoteAddr string) string { | 55 | + | func ExtractRemoteIP(remoteAddr string) string { |
| 56 | host, _, err := net.SplitHostPort(remoteAddr) | 56 | host, _, err := net.SplitHostPort(remoteAddr) | ||
| 57 | if err != nil { | 57 | if err != nil { | ||
| 58 | return remoteAddr | 58 | return remoteAddr | ||
| @@ -60,6 +60,34 @@ func extractRemoteIP(remoteAddr string) string { | |||||
| 60 | return host | 60 | return host | ||
| 61 | } | 61 | } | ||
| 62 | 62 | ||||
| 63 | + | // IsTrustedOrLocal reports whether an IP belongs to localhost or trusted proxy nets. | |||
| 64 | + | func IsTrustedOrLocal(ip string, proxyNets []*net.IPNet) bool { | |||
| 65 | + | return IsTrustedProxy(ip, localhostNets) || IsTrustedProxy(ip, proxyNets) | |||
| 66 | + | } | |||
| 67 | + | ||||
| 68 | + | // HTTPSAuthority converts an incoming Host header plus httpPort/tlsPort into the | |||
| 69 | + | // correct HTTPS authority (host or host:port). | |||
| 70 | + | // | |||
| 71 | + | // Rules: | |||
| 72 | + | // - No port in Host → omit TLS port (public reverse-proxy assumed on :443). | |||
| 73 | + | // - Host port == httpPort → map to tlsPort. | |||
| 74 | + | // - Any other explicit port → preserve it. | |||
| 75 | + | func HTTPSAuthority(host string, httpPort, tlsPort int) string { | |||
| 76 | + | hostname, portStr, err := net.SplitHostPort(host) | |||
| 77 | + | if err != nil { | |||
| 78 | + | // No port in Host header — omit TLS port from URL. | |||
| 79 | + | return host | |||
| 80 | + | } | |||
| 81 | + | ||||
| 82 | + | port, _ := strconv.Atoi(portStr) | |||
| 83 | + | if port == httpPort { | |||
| 84 | + | return net.JoinHostPort(hostname, strconv.Itoa(tlsPort)) | |||
| 85 | + | } | |||
| 86 | + | ||||
| 87 | + | // Unknown explicit port — preserve it. | |||
| 88 | + | return net.JoinHostPort(hostname, portStr) | |||
| 89 | + | } | |||
| 90 | + | ||||
| 63 | // RegistryCIDRAllowlist returns middleware that restricts access to the given CIDR ranges. | 91 | // RegistryCIDRAllowlist returns middleware that restricts access to the given CIDR ranges. | ||
| 64 | // Localhost is always allowed so Gordon can pull from its own registry. | 92 | // Localhost is always allowed so Gordon can pull from its own registry. | ||
| 65 | // An empty allowedNets slice is a no-op (all traffic passes through). | 93 | // An empty allowedNets slice is a no-op (all traffic passes through). | ||
| @@ -93,7 +121,7 @@ func HTTPSRedirect(proxyNets []*net.IPNet, httpPort, tlsPort int, forceAll bool, | |||||
| 93 | } | 121 | } | ||
| 94 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | 122 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| 95 | if !forceAll { | 123 | if !forceAll { | ||
| 96 | - | remoteIP := extractRemoteIP(r.RemoteAddr) | 124 | + | remoteIP := ExtractRemoteIP(r.RemoteAddr) |
| 97 | if IsTrustedProxy(remoteIP, localhostNets) || IsTrustedProxy(remoteIP, proxyNets) { | 125 | if IsTrustedProxy(remoteIP, localhostNets) || IsTrustedProxy(remoteIP, proxyNets) { | ||
| 98 | next.ServeHTTP(w, r) | 126 | next.ServeHTTP(w, r) | ||
| 99 | return | 127 | return | ||
| @@ -112,29 +140,13 @@ func HTTPSRedirect(proxyNets []*net.IPNet, httpPort, tlsPort int, forceAll bool, | |||||
| 112 | } | 140 | } | ||
| 113 | 141 | ||||
| 114 | // httpsRedirectTarget derives the HTTPS redirect URL from the request Host. | 142 | // httpsRedirectTarget derives the HTTPS redirect URL from the request Host. | ||
| 115 | - | // | |||
| 116 | - | // - No port in Host → omit TLS port (public reverse-proxy assumed on :443). | |||
| 117 | - | // - Host port == httpPort → map to tlsPort. | |||
| 118 | - | // - Any other explicit port → preserve it. | |||
| 119 | func httpsRedirectTarget(host, requestURI string, httpPort, tlsPort int) string { | 143 | func httpsRedirectTarget(host, requestURI string, httpPort, tlsPort int) string { | ||
| 120 | path := requestURI | 144 | path := requestURI | ||
| 121 | if !strings.HasPrefix(path, "/") { | 145 | if !strings.HasPrefix(path, "/") { | ||
| 122 | path = "/" | 146 | path = "/" | ||
| 123 | } | 147 | } | ||
| 124 | 148 | ||||
| 125 | - | hostname, portStr, err := net.SplitHostPort(host) | 149 | + | return fmt.Sprintf("https://%s%s", HTTPSAuthority(host, httpPort, tlsPort), path) |
| 126 | - | if err != nil { | |||
| 127 | - | // No port in Host header — omit TLS port from URL. | |||
| 128 | - | return fmt.Sprintf("https://%s%s", host, path) | |||
| 129 | - | } | |||
| 130 | - | ||||
| 131 | - | port, _ := strconv.Atoi(portStr) | |||
| 132 | - | if port == httpPort { | |||
| 133 | - | return fmt.Sprintf("https://%s:%d%s", hostname, tlsPort, path) | |||
| 134 | - | } | |||
| 135 | - | ||||
| 136 | - | // Unknown explicit port — preserve it. | |||
| 137 | - | return fmt.Sprintf("https://%s:%s%s", hostname, portStr, path) | |||
| 138 | } | 150 | } | ||
| 139 | 151 | ||||
| 140 | // ProxyCIDRAllowlist returns middleware that restricts proxy access to the given CIDR ranges. | 152 | // ProxyCIDRAllowlist returns middleware that restricts proxy access to the given CIDR ranges. | ||
| @@ -143,6 +155,6 @@ func httpsRedirectTarget(host, requestURI string, httpPort, tlsPort int) string | |||||
| 143 | // An empty allowedNets slice is a no-op (all traffic passes through). | 155 | // An empty allowedNets slice is a no-op (all traffic passes through). | ||
| 144 | func ProxyCIDRAllowlist(allowedNets []*net.IPNet, log zerowrap.Logger) func(http.Handler) http.Handler { | 156 | func ProxyCIDRAllowlist(allowedNets []*net.IPNet, log zerowrap.Logger) func(http.Handler) http.Handler { | ||
| 145 | return cidrAllowlist(allowedNets, func(r *http.Request) string { | 157 | return cidrAllowlist(allowedNets, func(r *http.Request) string { | ||
| 146 | - | return extractRemoteIP(r.RemoteAddr) | 158 | + | return ExtractRemoteIP(r.RemoteAddr) |
| 147 | }, "proxy origin", log) | 159 | }, "proxy origin", log) | ||
| 148 | } | 160 | } | ||
M internal/adapters/in/http/onboarding/handler.go +4 -19
| @@ -5,7 +5,8 @@ import ( | |||
| 5 | 5 | "html" | |
| 6 | 6 | "net" | |
| 7 | 7 | "net/http" | |
| 8 | - | "strconv" | |
| 8 | + | ||
| 9 | + | "github.com/bnema/gordon/internal/adapters/in/http/middleware" | |
| 9 | 10 | ) | |
| 10 | 11 | ||
| 11 | 12 | // Handler serves CA onboarding endpoints for direct/Tailnet clients. | |
| @@ -46,26 +47,10 @@ func (h *Handler) ServeMobileconfig(w http.ResponseWriter, _ *http.Request) { | |||
| 46 | 47 | } | |
| 47 | 48 | ||
| 48 | 49 | // publicHTTPSURL derives the public HTTPS URL from the request Host header. | |
| 49 | - | // | |
| 50 | - | // Rules: | |
| 51 | - | // - no port in Host => https://host/ (omit internal TLS port) | |
| 52 | - | // - Host port == httpPort => https://host:tlsPort/ | |
| 53 | - | // - any other explicit port => preserve it | |
| 54 | - | // | |
| 55 | 50 | // The result is HTML-escaped to prevent XSS via a crafted Host header. | |
| 56 | 51 | func (h *Handler) publicHTTPSURL(r *http.Request) string { | |
| 57 | - | host, portStr, err := net.SplitHostPort(r.Host) | |
| 58 | - | if err != nil { | |
| 59 | - | // No port in Host header — use bare hostname. | |
| 60 | - | return html.EscapeString("https://" + r.Host + "/") | |
| 61 | - | } | |
| 62 | - | ||
| 63 | - | // Ignore error: an invalid explicit port falls through to the preserve-as-is branch. | |
| 64 | - | port, _ := strconv.Atoi(portStr) | |
| 65 | - | if port == h.httpPort { | |
| 66 | - | return html.EscapeString(fmt.Sprintf("https://%s:%d/", host, h.tlsPort)) | |
| 67 | - | } | |
| 68 | - | return html.EscapeString(fmt.Sprintf("https://%s:%s/", host, portStr)) | |
| 52 | + | authority := middleware.HTTPSAuthority(r.Host, h.httpPort, h.tlsPort) | |
| 53 | + | return html.EscapeString("https://" + authority + "/") | |
| 69 | 54 | } | |
| 70 | 55 | ||
| 71 | 56 | // isRawIP reports whether the host (without port) is a raw IP literal. | |
| @@ -5,7 +5,8 @@ import ( | |||||
| 5 | "html" | 5 | "html" | ||
| 6 | "net" | 6 | "net" | ||
| 7 | "net/http" | 7 | "net/http" | ||
| 8 | - | "strconv" | 8 | + | |
| 9 | + | "github.com/bnema/gordon/internal/adapters/in/http/middleware" | |||
| 9 | ) | 10 | ) | ||
| 10 | 11 | ||||
| 11 | // Handler serves CA onboarding endpoints for direct/Tailnet clients. | 12 | // Handler serves CA onboarding endpoints for direct/Tailnet clients. | ||
| @@ -46,26 +47,10 @@ func (h *Handler) ServeMobileconfig(w http.ResponseWriter, _ *http.Request) { | |||||
| 46 | } | 47 | } | ||
| 47 | 48 | ||||
| 48 | // publicHTTPSURL derives the public HTTPS URL from the request Host header. | 49 | // publicHTTPSURL derives the public HTTPS URL from the request Host header. | ||
| 49 | - | // | |||
| 50 | - | // Rules: | |||
| 51 | - | // - no port in Host => https://host/ (omit internal TLS port) | |||
| 52 | - | // - Host port == httpPort => https://host:tlsPort/ | |||
| 53 | - | // - any other explicit port => preserve it | |||
| 54 | - | // | |||
| 55 | // The result is HTML-escaped to prevent XSS via a crafted Host header. | 50 | // The result is HTML-escaped to prevent XSS via a crafted Host header. | ||
| 56 | func (h *Handler) publicHTTPSURL(r *http.Request) string { | 51 | func (h *Handler) publicHTTPSURL(r *http.Request) string { | ||
| 57 | - | host, portStr, err := net.SplitHostPort(r.Host) | 52 | + | authority := middleware.HTTPSAuthority(r.Host, h.httpPort, h.tlsPort) |
| 58 | - | if err != nil { | 53 | + | return html.EscapeString("https://" + authority + "/") |
| 59 | - | // No port in Host header — use bare hostname. | |||
| 60 | - | return html.EscapeString("https://" + r.Host + "/") | |||
| 61 | - | } | |||
| 62 | - | ||||
| 63 | - | // Ignore error: an invalid explicit port falls through to the preserve-as-is branch. | |||
| 64 | - | port, _ := strconv.Atoi(portStr) | |||
| 65 | - | if port == h.httpPort { | |||
| 66 | - | return html.EscapeString(fmt.Sprintf("https://%s:%d/", host, h.tlsPort)) | |||
| 67 | - | } | |||
| 68 | - | return html.EscapeString(fmt.Sprintf("https://%s:%s/", host, portStr)) | |||
| 69 | } | 54 | } | ||
| 70 | 55 | ||||
| 71 | // isRawIP reports whether the host (without port) is a raw IP literal. | 56 | // isRawIP reports whether the host (without port) is a raw IP literal. | ||
M internal/adapters/in/http/onboarding/handler_test.go +0 -2
| @@ -107,8 +107,6 @@ func TestHandler_OnboardingPage(t *testing.T) { | |||
| 107 | 107 | assert.Contains(t, string(body), "/ca.mobileconfig") | |
| 108 | 108 | } | |
| 109 | 109 | ||
| 110 | - | // --- New tests per plan Task 1 --- | |
| 111 | - | ||
| 112 | 110 | func TestHandler_OnboardingPage_DefaultHTTPSURLOmitsInternalTLSPort(t *testing.T) { | |
| 113 | 111 | srv, _, _ := newTestServer(t) | |
| 114 | 112 | body := getOnboardingBody(t, srv, "o2.bnema.dev") | |
| @@ -107,8 +107,6 @@ func TestHandler_OnboardingPage(t *testing.T) { | |||||
| 107 | assert.Contains(t, string(body), "/ca.mobileconfig") | 107 | assert.Contains(t, string(body), "/ca.mobileconfig") | ||
| 108 | } | 108 | } | ||
| 109 | 109 | ||||
| 110 | - | // --- New tests per plan Task 1 --- | |||
| 111 | - | ||||
| 112 | func TestHandler_OnboardingPage_DefaultHTTPSURLOmitsInternalTLSPort(t *testing.T) { | 110 | func TestHandler_OnboardingPage_DefaultHTTPSURLOmitsInternalTLSPort(t *testing.T) { | ||
| 113 | srv, _, _ := newTestServer(t) | 111 | srv, _, _ := newTestServer(t) | ||
| 114 | body := getOnboardingBody(t, srv, "o2.bnema.dev") | 112 | body := getOnboardingBody(t, srv, "o2.bnema.dev") | ||
M internal/app/run.go +2 -19
| @@ -1732,8 +1732,8 @@ func directHTTPOnboardingGate(ob *onboarding.Handler, proxyNets []*net.IPNet, pr | |||
| 1732 | 1732 | )(onboardingMux) | |
| 1733 | 1733 | ||
| 1734 | 1734 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | |
| 1735 | - | remoteIP := extractDirectIP(r.RemoteAddr) | |
| 1736 | - | if isTrustedOrLocal(remoteIP, proxyNets) { | |
| 1735 | + | remoteIP := middleware.ExtractRemoteIP(r.RemoteAddr) | |
| 1736 | + | if middleware.IsTrustedOrLocal(remoteIP, proxyNets) { | |
| 1737 | 1737 | proxyChain.ServeHTTP(w, r) | |
| 1738 | 1738 | return | |
| 1739 | 1739 | } | |
| @@ -1751,23 +1751,6 @@ func directHTTPForbidden(w http.ResponseWriter, r *http.Request) { | |||
| 1751 | 1751 | } | |
| 1752 | 1752 | } | |
| 1753 | 1753 | ||
| 1754 | - | // extractDirectIP returns the IP portion of a RemoteAddr, stripping the port. | |
| 1755 | - | func extractDirectIP(remoteAddr string) string { | |
| 1756 | - | host, _, err := net.SplitHostPort(remoteAddr) | |
| 1757 | - | if err != nil { | |
| 1758 | - | return remoteAddr | |
| 1759 | - | } | |
| 1760 | - | return host | |
| 1761 | - | } | |
| 1762 | - | ||
| 1763 | - | // isTrustedOrLocal checks whether an IP belongs to trusted proxy nets or localhost. | |
| 1764 | - | func isTrustedOrLocal(ip string, proxyNets []*net.IPNet) bool { | |
| 1765 | - | if parsed := net.ParseIP(ip); parsed != nil && parsed.IsLoopback() { | |
| 1766 | - | return true | |
| 1767 | - | } | |
| 1768 | - | return middleware.IsTrustedProxy(ip, proxyNets) | |
| 1769 | - | } | |
| 1770 | - | ||
| 1771 | 1754 | func buildRegistryHandlerWithMiddleware( | |
| 1772 | 1755 | svc *services, | |
| 1773 | 1756 | cfg Config, | |
| @@ -1732,8 +1732,8 @@ func directHTTPOnboardingGate(ob *onboarding.Handler, proxyNets []*net.IPNet, pr | |||||
| 1732 | )(onboardingMux) | 1732 | )(onboardingMux) | ||
| 1733 | 1733 | ||||
| 1734 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | 1734 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
| 1735 | - | remoteIP := extractDirectIP(r.RemoteAddr) | 1735 | + | remoteIP := middleware.ExtractRemoteIP(r.RemoteAddr) |
| 1736 | - | if isTrustedOrLocal(remoteIP, proxyNets) { | 1736 | + | if middleware.IsTrustedOrLocal(remoteIP, proxyNets) { |
| 1737 | proxyChain.ServeHTTP(w, r) | 1737 | proxyChain.ServeHTTP(w, r) | ||
| 1738 | return | 1738 | return | ||
| 1739 | } | 1739 | } | ||
| @@ -1751,23 +1751,6 @@ func directHTTPForbidden(w http.ResponseWriter, r *http.Request) { | |||||
| 1751 | } | 1751 | } | ||
| 1752 | } | 1752 | } | ||
| 1753 | 1753 | ||||
| 1754 | - | // extractDirectIP returns the IP portion of a RemoteAddr, stripping the port. | |||
| 1755 | - | func extractDirectIP(remoteAddr string) string { | |||
| 1756 | - | host, _, err := net.SplitHostPort(remoteAddr) | |||
| 1757 | - | if err != nil { | |||
| 1758 | - | return remoteAddr | |||
| 1759 | - | } | |||
| 1760 | - | return host | |||
| 1761 | - | } | |||
| 1762 | - | ||||
| 1763 | - | // isTrustedOrLocal checks whether an IP belongs to trusted proxy nets or localhost. | |||
| 1764 | - | func isTrustedOrLocal(ip string, proxyNets []*net.IPNet) bool { | |||
| 1765 | - | if parsed := net.ParseIP(ip); parsed != nil && parsed.IsLoopback() { | |||
| 1766 | - | return true | |||
| 1767 | - | } | |||
| 1768 | - | return middleware.IsTrustedProxy(ip, proxyNets) | |||
| 1769 | - | } | |||
| 1770 | - | ||||
| 1771 | func buildRegistryHandlerWithMiddleware( | 1754 | func buildRegistryHandlerWithMiddleware( | ||
| 1772 | svc *services, | 1755 | svc *services, | ||
| 1773 | cfg Config, | 1756 | cfg Config, | ||
M internal/app/run_http_handlers_test.go +0 -4
| @@ -71,8 +71,6 @@ func TestCreateHTTPHandlers_LocalMode_RestrictsRegistryToLoopback(t *testing.T) | |||
| 71 | 71 | } | |
| 72 | 72 | } | |
| 73 | 73 | ||
| 74 | - | // --- Task 3: Direct HTTP onboarding gate integration tests --- | |
| 75 | - | ||
| 76 | 74 | // newTestCA creates a temporary CA adapter for integration tests. | |
| 77 | 75 | func newTestCA(t *testing.T) *pkiadapter.CA { | |
| 78 | 76 | t.Helper() | |
| @@ -323,8 +321,6 @@ func TestCreateHTTPHandlers_TLSConfiguredWithoutCA_FailsStartup(t *testing.T) { | |||
| 323 | 321 | assert.Error(t, err, "initPKI should fail when CA cannot be initialized") | |
| 324 | 322 | } | |
| 325 | 323 | ||
| 326 | - | // --- End Task 3 tests --- | |
| 327 | - | ||
| 328 | 324 | func TestBuildRegistryCIDRAllowlistMiddleware_InvalidEntries_DenyAll(t *testing.T) { | |
| 329 | 325 | t.Parallel() | |
| 330 | 326 | ||
| @@ -71,8 +71,6 @@ func TestCreateHTTPHandlers_LocalMode_RestrictsRegistryToLoopback(t *testing.T) | |||||
| 71 | } | 71 | } | ||
| 72 | } | 72 | } | ||
| 73 | 73 | ||||
| 74 | - | // --- Task 3: Direct HTTP onboarding gate integration tests --- | |||
| 75 | - | ||||
| 76 | // newTestCA creates a temporary CA adapter for integration tests. | 74 | // newTestCA creates a temporary CA adapter for integration tests. | ||
| 77 | func newTestCA(t *testing.T) *pkiadapter.CA { | 75 | func newTestCA(t *testing.T) *pkiadapter.CA { | ||
| 78 | t.Helper() | 76 | t.Helper() | ||
| @@ -323,8 +321,6 @@ func TestCreateHTTPHandlers_TLSConfiguredWithoutCA_FailsStartup(t *testing.T) { | |||||
| 323 | assert.Error(t, err, "initPKI should fail when CA cannot be initialized") | 321 | assert.Error(t, err, "initPKI should fail when CA cannot be initialized") | ||
| 324 | } | 322 | } | ||
| 325 | 323 | ||||
| 326 | - | // --- End Task 3 tests --- | |||
| 327 | - | ||||
| 328 | func TestBuildRegistryCIDRAllowlistMiddleware_InvalidEntries_DenyAll(t *testing.T) { | 324 | func TestBuildRegistryCIDRAllowlistMiddleware_InvalidEntries_DenyAll(t *testing.T) { | ||
| 329 | t.Parallel() | 325 | t.Parallel() | ||
| 330 | 326 | ||||