detached b33054e

Merge branch 'main' into fix/podman-rootless-friction

commit
b33054e252456257479d945ed890b75651d0775c
author
bnema · · 2026-04-03 18:23 UTC
committer
GitHub · · 2026-04-03 18:23 UTC
merge of
e0293e4 + 8f1e1e6 · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
4 files · +206 -95
M go.mod +26 -27
@@ -13,18 +13,18 @@ require (
1313 github.com/golang-jwt/jwt/v5 v5.3.1
1414 github.com/google/go-containerregistry v0.21.3
1515 github.com/google/uuid v1.6.0
16- github.com/mattn/go-runewidth v0.0.21
16+ github.com/mattn/go-runewidth v0.0.22
1717 github.com/muesli/termenv v0.16.0
1818 github.com/rivo/uniseg v0.4.7
1919 github.com/smallstep/truststore v0.13.0
2020 github.com/spf13/cobra v1.10.2
2121 github.com/spf13/viper v1.21.0
2222 github.com/stretchr/testify v1.11.1
23- go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.0
24- go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.0
25- go.opentelemetry.io/otel/sdk v1.42.0
26- go.opentelemetry.io/otel/sdk/log v0.18.0
27- go.opentelemetry.io/otel/sdk/metric v1.42.0
23+ go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0
24+ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0
25+ go.opentelemetry.io/otel/sdk v1.43.0
26+ go.opentelemetry.io/otel/sdk/log v0.19.0
27+ go.opentelemetry.io/otel/sdk/metric v1.43.0
2828 golang.org/x/mod v0.34.0
2929 golang.org/x/sync v0.20.0
3030 gopkg.in/natefinch/lumberjack.v2 v2.2.1
@@ -35,47 +35,46 @@ require (
3535 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
3636 github.com/cenkalti/backoff/v5 v5.0.3 // indirect
3737 github.com/cespare/xxhash/v2 v2.3.0 // indirect
38- github.com/charmbracelet/colorprofile v0.4.1 // indirect
38+ github.com/charmbracelet/colorprofile v0.4.3 // indirect
3939 github.com/charmbracelet/x/ansi v0.11.6 // indirect
4040 github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
4141 github.com/charmbracelet/x/term v0.2.2 // indirect
42- github.com/clipperhouse/displaywidth v0.9.0 // indirect
43- github.com/clipperhouse/stringish v0.1.1 // indirect
44- github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
42+ github.com/clipperhouse/displaywidth v0.11.0 // indirect
43+ github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
4544 github.com/containerd/log v0.1.0 // indirect
4645 github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect
47- github.com/docker/cli v29.3.0+incompatible // indirect
48- github.com/docker/docker-credential-helpers v0.9.3 // indirect
46+ github.com/docker/cli v29.3.1+incompatible // indirect
47+ github.com/docker/docker-credential-helpers v0.9.5 // indirect
4948 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
5049 github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
51- github.com/klauspost/compress v1.18.4 // indirect
52- github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
50+ github.com/klauspost/compress v1.18.5 // indirect
51+ github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
5352 github.com/mattn/go-localereader v0.0.1 // indirect
5453 github.com/mitchellh/go-homedir v1.1.0 // indirect
5554 github.com/moby/sys/atomicwriter v0.1.0 // indirect
5655 github.com/morikuni/aec v1.1.0 // indirect
5756 github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
5857 github.com/muesli/cancelreader v0.2.2 // indirect
59- github.com/rs/zerolog v1.34.0 // indirect
58+ github.com/rs/zerolog v1.35.0 // indirect
6059 github.com/sirupsen/logrus v1.9.4 // indirect
6160 github.com/vbatts/tar-split v0.12.2 // indirect
6261 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
63- go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect
64- go.opentelemetry.io/otel/log v0.18.0 // indirect
65- go.opentelemetry.io/proto/otlp v1.9.0 // indirect
62+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
63+ go.opentelemetry.io/otel/log v0.19.0 // indirect
64+ go.opentelemetry.io/proto/otlp v1.10.0 // indirect
6665 go.yaml.in/yaml/v3 v3.0.4 // indirect
6766 golang.org/x/net v0.52.0 // indirect
68- google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect
69- google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect
70- google.golang.org/grpc v1.79.3 // indirect
67+ google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
68+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
69+ google.golang.org/grpc v1.80.0 // indirect
7170 google.golang.org/protobuf v1.36.11 // indirect
72- howett.net/plist v1.0.0 // indirect
71+ howett.net/plist v1.0.1 // indirect
7372 )
7473
7574 require (
7675 github.com/davecgh/go-spew v1.1.1 // indirect
7776 github.com/pmezard/go-difflib v1.0.0 // indirect
78- github.com/stretchr/objx v0.5.2 // indirect
77+ github.com/stretchr/objx v0.5.3 // indirect
7978 go.uber.org/mock v0.6.0 // direct
8079 golang.org/x/time v0.15.0
8180 )
@@ -107,10 +106,10 @@ require (
107106 github.com/subosito/gotenv v1.6.0 // indirect
108107 go.opentelemetry.io/auto/sdk v1.2.1 // indirect
109108 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0
110- go.opentelemetry.io/otel v1.42.0
111- go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0
112- go.opentelemetry.io/otel/metric v1.42.0
113- go.opentelemetry.io/otel/trace v1.42.0
109+ go.opentelemetry.io/otel v1.43.0
110+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0
111+ go.opentelemetry.io/otel/metric v1.43.0
112+ go.opentelemetry.io/otel/trace v1.43.0
114113 golang.org/x/sys v0.42.0 // indirect
115114 golang.org/x/text v0.35.0 // indirect
116115 gopkg.in/yaml.v3 v3.0.1 // indirect
@@ -13,18 +13,18 @@ require (
13 github.com/golang-jwt/jwt/v5 v5.3.113 github.com/golang-jwt/jwt/v5 v5.3.1
14 github.com/google/go-containerregistry v0.21.314 github.com/google/go-containerregistry v0.21.3
15 github.com/google/uuid v1.6.015 github.com/google/uuid v1.6.0
16- github.com/mattn/go-runewidth v0.0.2116+ github.com/mattn/go-runewidth v0.0.22
17 github.com/muesli/termenv v0.16.017 github.com/muesli/termenv v0.16.0
18 github.com/rivo/uniseg v0.4.718 github.com/rivo/uniseg v0.4.7
19 github.com/smallstep/truststore v0.13.019 github.com/smallstep/truststore v0.13.0
20 github.com/spf13/cobra v1.10.220 github.com/spf13/cobra v1.10.2
21 github.com/spf13/viper v1.21.021 github.com/spf13/viper v1.21.0
22 github.com/stretchr/testify v1.11.122 github.com/stretchr/testify v1.11.1
23- go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.18.023+ go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.19.0
24- go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.42.024+ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.43.0
25- go.opentelemetry.io/otel/sdk v1.42.025+ go.opentelemetry.io/otel/sdk v1.43.0
26- go.opentelemetry.io/otel/sdk/log v0.18.026+ go.opentelemetry.io/otel/sdk/log v0.19.0
27- go.opentelemetry.io/otel/sdk/metric v1.42.027+ go.opentelemetry.io/otel/sdk/metric v1.43.0
28 golang.org/x/mod v0.34.028 golang.org/x/mod v0.34.0
29 golang.org/x/sync v0.20.029 golang.org/x/sync v0.20.0
30 gopkg.in/natefinch/lumberjack.v2 v2.2.130 gopkg.in/natefinch/lumberjack.v2 v2.2.1
@@ -35,47 +35,46 @@ require (
35 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect35 github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
36 github.com/cenkalti/backoff/v5 v5.0.3 // indirect36 github.com/cenkalti/backoff/v5 v5.0.3 // indirect
37 github.com/cespare/xxhash/v2 v2.3.0 // indirect37 github.com/cespare/xxhash/v2 v2.3.0 // indirect
38- github.com/charmbracelet/colorprofile v0.4.1 // indirect38+ github.com/charmbracelet/colorprofile v0.4.3 // indirect
39 github.com/charmbracelet/x/ansi v0.11.6 // indirect39 github.com/charmbracelet/x/ansi v0.11.6 // indirect
40 github.com/charmbracelet/x/cellbuf v0.0.15 // indirect40 github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
41 github.com/charmbracelet/x/term v0.2.2 // indirect41 github.com/charmbracelet/x/term v0.2.2 // indirect
42- github.com/clipperhouse/displaywidth v0.9.0 // indirect42+ github.com/clipperhouse/displaywidth v0.11.0 // indirect
43- github.com/clipperhouse/stringish v0.1.1 // indirect43+ github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
44- github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
45 github.com/containerd/log v0.1.0 // indirect44 github.com/containerd/log v0.1.0 // indirect
46 github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect45 github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect
47- github.com/docker/cli v29.3.0+incompatible // indirect46+ github.com/docker/cli v29.3.1+incompatible // indirect
48- github.com/docker/docker-credential-helpers v0.9.3 // indirect47+ github.com/docker/docker-credential-helpers v0.9.5 // indirect
49 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect48 github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
50 github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect49 github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect
51- github.com/klauspost/compress v1.18.4 // indirect50+ github.com/klauspost/compress v1.18.5 // indirect
52- github.com/lucasb-eyer/go-colorful v1.3.0 // indirect51+ github.com/lucasb-eyer/go-colorful v1.4.0 // indirect
53 github.com/mattn/go-localereader v0.0.1 // indirect52 github.com/mattn/go-localereader v0.0.1 // indirect
54 github.com/mitchellh/go-homedir v1.1.0 // indirect53 github.com/mitchellh/go-homedir v1.1.0 // indirect
55 github.com/moby/sys/atomicwriter v0.1.0 // indirect54 github.com/moby/sys/atomicwriter v0.1.0 // indirect
56 github.com/morikuni/aec v1.1.0 // indirect55 github.com/morikuni/aec v1.1.0 // indirect
57 github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect56 github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
58 github.com/muesli/cancelreader v0.2.2 // indirect57 github.com/muesli/cancelreader v0.2.2 // indirect
59- github.com/rs/zerolog v1.34.0 // indirect58+ github.com/rs/zerolog v1.35.0 // indirect
60 github.com/sirupsen/logrus v1.9.4 // indirect59 github.com/sirupsen/logrus v1.9.4 // indirect
61 github.com/vbatts/tar-split v0.12.2 // indirect60 github.com/vbatts/tar-split v0.12.2 // indirect
62 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect61 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
63- go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.42.0 // indirect62+ go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
64- go.opentelemetry.io/otel/log v0.18.0 // indirect63+ go.opentelemetry.io/otel/log v0.19.0 // indirect
65- go.opentelemetry.io/proto/otlp v1.9.0 // indirect64+ go.opentelemetry.io/proto/otlp v1.10.0 // indirect
66 go.yaml.in/yaml/v3 v3.0.4 // indirect65 go.yaml.in/yaml/v3 v3.0.4 // indirect
67 golang.org/x/net v0.52.0 // indirect66 golang.org/x/net v0.52.0 // indirect
68- google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 // indirect67+ google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect
69- google.golang.org/genproto/googleapis/rpc v0.0.0-20260217215200-42d3e9bedb6d // indirect68+ google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect
70- google.golang.org/grpc v1.79.3 // indirect69+ google.golang.org/grpc v1.80.0 // indirect
71 google.golang.org/protobuf v1.36.11 // indirect70 google.golang.org/protobuf v1.36.11 // indirect
72- howett.net/plist v1.0.0 // indirect71+ howett.net/plist v1.0.1 // indirect
73 )72 )
74 73
75 require (74 require (
76 github.com/davecgh/go-spew v1.1.1 // indirect75 github.com/davecgh/go-spew v1.1.1 // indirect
77 github.com/pmezard/go-difflib v1.0.0 // indirect76 github.com/pmezard/go-difflib v1.0.0 // indirect
78- github.com/stretchr/objx v0.5.2 // indirect77+ github.com/stretchr/objx v0.5.3 // indirect
79 go.uber.org/mock v0.6.0 // direct78 go.uber.org/mock v0.6.0 // direct
80 golang.org/x/time v0.15.079 golang.org/x/time v0.15.0
81 )80 )
@@ -107,10 +106,10 @@ require (
107 github.com/subosito/gotenv v1.6.0 // indirect106 github.com/subosito/gotenv v1.6.0 // indirect
108 go.opentelemetry.io/auto/sdk v1.2.1 // indirect107 go.opentelemetry.io/auto/sdk v1.2.1 // indirect
109 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0108 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0
110- go.opentelemetry.io/otel v1.42.0109+ go.opentelemetry.io/otel v1.43.0
111- go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.42.0110+ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0
112- go.opentelemetry.io/otel/metric v1.42.0111+ go.opentelemetry.io/otel/metric v1.43.0
113- go.opentelemetry.io/otel/trace v1.42.0112+ go.opentelemetry.io/otel/trace v1.43.0
114 golang.org/x/sys v0.42.0 // indirect113 golang.org/x/sys v0.42.0 // indirect
115 golang.org/x/text v0.35.0 // indirect114 golang.org/x/text v0.35.0 // indirect
116 gopkg.in/yaml.v3 v3.0.1 // indirect115 gopkg.in/yaml.v3 v3.0.1 // indirect
M go.sum +56 -66 generated file: collapsed
M internal/adapters/in/http/proxy/forwarder.go +28 -2
@@ -15,6 +15,7 @@ import (
1515
1616 "github.com/bnema/zerowrap"
1717
18+ "github.com/bnema/gordon/internal/adapters/in/http/middleware"
1819 "github.com/bnema/gordon/internal/domain"
1920 )
2021
@@ -95,7 +96,7 @@ func (h *Handler) forwardToTarget(w http.ResponseWriter, r *http.Request, target
9596
9697 transport := h.transportForTarget(target)
9798
98- proxy := newReverseProxy(targetURL, originalHost, transport,
99+ proxy := newReverseProxy(targetURL, originalHost, transport, r, h.trustedNets,
99100 func(w http.ResponseWriter, _ *http.Request, proxyErr error) {
100101 var maxBytesErr *http.MaxBytesError
101102 if errors.As(proxyErr, &maxBytesErr) {
@@ -183,9 +184,13 @@ func (h *Handler) forwardToRegistry(w http.ResponseWriter, r *http.Request, regi
183184 // originalHost, when non-empty, overrides the Host header sent to the backend.
184185 // This is used for DNS-pinned targets where the proxy dials an IP but needs to
185186 // send the original hostname for virtual-hosted upstreams.
186-func newReverseProxy(targetURL *url.URL, originalHost string, transport http.RoundTripper, errorHandler func(http.ResponseWriter, *http.Request, error), modifyResp func(*http.Response) error) *httputil.ReverseProxy {
187+//
188+// incomingReq and trustedNets are used to preserve X-Forwarded-Proto when the
189+// request originates from a trusted upstream proxy.
190+func newReverseProxy(targetURL *url.URL, originalHost string, transport http.RoundTripper, incomingReq *http.Request, trustedNets []*net.IPNet, errorHandler func(http.ResponseWriter, *http.Request, error), modifyResp func(*http.Response) error) *httputil.ReverseProxy {
187191 return &httputil.ReverseProxy{
188192 Rewrite: func(pr *httputil.ProxyRequest) {
193+ existingProto := pr.In.Header.Get("X-Forwarded-Proto")
189194 pr.SetURL(targetURL)
190195 pr.SetXForwarded()
191196 if originalHost != "" {
@@ -193,6 +198,13 @@ func newReverseProxy(targetURL *url.URL, originalHost string, transport http.Rou
193198 } else {
194199 pr.Out.Host = targetURL.Host
195200 }
201+ // Preserve X-Forwarded-Proto from trusted upstream proxies.
202+ // SetXForwarded() unconditionally sets it based on the incoming scheme
203+ // (HTTP between proxies), but when a trusted proxy already sent the
204+ // real client proto, we must honor it.
205+ if existingProto != "" && isTrustedSource(incomingReq, trustedNets) {
206+ pr.Out.Header.Set("X-Forwarded-Proto", existingProto)
207+ }
196208 },
197209 Transport: transport,
198210 ErrorHandler: errorHandler,
@@ -200,6 +212,20 @@ func newReverseProxy(targetURL *url.URL, originalHost string, transport http.Rou
200212 }
201213 }
202214
215+// isTrustedSource reports whether the request's remote address is in trustedNets.
216+func isTrustedSource(r *http.Request, trustedNets []*net.IPNet) bool {
217+ if len(trustedNets) == 0 {
218+ return false
219+ }
220+ host, _, err := net.SplitHostPort(r.RemoteAddr)
221+ if err != nil {
222+ // RemoteAddr may be a bare IP without a port (e.g. from Unix sockets
223+ // or certain proxy setups). Fall back to using it directly.
224+ host = strings.TrimSuffix(strings.TrimPrefix(r.RemoteAddr, "["), "]")
225+ }
226+ return middleware.IsTrustedProxy(host, trustedNets)
227+}
228+
203229 // modifyResponse returns a function that adds proxy headers and enforces response size limits.
204230 func modifyResponse(maxResponseSize int64) func(*http.Response) error {
205231 return func(resp *http.Response) error {
@@ -15,6 +15,7 @@ import (
15 15
16 "github.com/bnema/zerowrap"16 "github.com/bnema/zerowrap"
17 17
18+ "github.com/bnema/gordon/internal/adapters/in/http/middleware"
18 "github.com/bnema/gordon/internal/domain"19 "github.com/bnema/gordon/internal/domain"
19 )20 )
20 21
@@ -95,7 +96,7 @@ func (h *Handler) forwardToTarget(w http.ResponseWriter, r *http.Request, target
95 96
96 transport := h.transportForTarget(target)97 transport := h.transportForTarget(target)
97 98
98- proxy := newReverseProxy(targetURL, originalHost, transport,99+ proxy := newReverseProxy(targetURL, originalHost, transport, r, h.trustedNets,
99 func(w http.ResponseWriter, _ *http.Request, proxyErr error) {100 func(w http.ResponseWriter, _ *http.Request, proxyErr error) {
100 var maxBytesErr *http.MaxBytesError101 var maxBytesErr *http.MaxBytesError
101 if errors.As(proxyErr, &maxBytesErr) {102 if errors.As(proxyErr, &maxBytesErr) {
@@ -183,9 +184,13 @@ func (h *Handler) forwardToRegistry(w http.ResponseWriter, r *http.Request, regi
183 // originalHost, when non-empty, overrides the Host header sent to the backend.184 // originalHost, when non-empty, overrides the Host header sent to the backend.
184 // This is used for DNS-pinned targets where the proxy dials an IP but needs to185 // This is used for DNS-pinned targets where the proxy dials an IP but needs to
185 // send the original hostname for virtual-hosted upstreams.186 // send the original hostname for virtual-hosted upstreams.
186-func newReverseProxy(targetURL *url.URL, originalHost string, transport http.RoundTripper, errorHandler func(http.ResponseWriter, *http.Request, error), modifyResp func(*http.Response) error) *httputil.ReverseProxy {187+//
188+// incomingReq and trustedNets are used to preserve X-Forwarded-Proto when the
189+// request originates from a trusted upstream proxy.
190+func newReverseProxy(targetURL *url.URL, originalHost string, transport http.RoundTripper, incomingReq *http.Request, trustedNets []*net.IPNet, errorHandler func(http.ResponseWriter, *http.Request, error), modifyResp func(*http.Response) error) *httputil.ReverseProxy {
187 return &httputil.ReverseProxy{191 return &httputil.ReverseProxy{
188 Rewrite: func(pr *httputil.ProxyRequest) {192 Rewrite: func(pr *httputil.ProxyRequest) {
193+ existingProto := pr.In.Header.Get("X-Forwarded-Proto")
189 pr.SetURL(targetURL)194 pr.SetURL(targetURL)
190 pr.SetXForwarded()195 pr.SetXForwarded()
191 if originalHost != "" {196 if originalHost != "" {
@@ -193,6 +198,13 @@ func newReverseProxy(targetURL *url.URL, originalHost string, transport http.Rou
193 } else {198 } else {
194 pr.Out.Host = targetURL.Host199 pr.Out.Host = targetURL.Host
195 }200 }
201+ // Preserve X-Forwarded-Proto from trusted upstream proxies.
202+ // SetXForwarded() unconditionally sets it based on the incoming scheme
203+ // (HTTP between proxies), but when a trusted proxy already sent the
204+ // real client proto, we must honor it.
205+ if existingProto != "" && isTrustedSource(incomingReq, trustedNets) {
206+ pr.Out.Header.Set("X-Forwarded-Proto", existingProto)
207+ }
196 },208 },
197 Transport: transport,209 Transport: transport,
198 ErrorHandler: errorHandler,210 ErrorHandler: errorHandler,
@@ -200,6 +212,20 @@ func newReverseProxy(targetURL *url.URL, originalHost string, transport http.Rou
200 }212 }
201 }213 }
202 214
215+// isTrustedSource reports whether the request's remote address is in trustedNets.
216+func isTrustedSource(r *http.Request, trustedNets []*net.IPNet) bool {
217+ if len(trustedNets) == 0 {
218+ return false
219+ }
220+ host, _, err := net.SplitHostPort(r.RemoteAddr)
221+ if err != nil {
222+ // RemoteAddr may be a bare IP without a port (e.g. from Unix sockets
223+ // or certain proxy setups). Fall back to using it directly.
224+ host = strings.TrimSuffix(strings.TrimPrefix(r.RemoteAddr, "["), "]")
225+ }
226+ return middleware.IsTrustedProxy(host, trustedNets)
227+}
228+
203 // modifyResponse returns a function that adds proxy headers and enforces response size limits.229 // modifyResponse returns a function that adds proxy headers and enforces response size limits.
204 func modifyResponse(maxResponseSize int64) func(*http.Response) error {230 func modifyResponse(maxResponseSize int64) func(*http.Response) error {
205 return func(resp *http.Response) error {231 return func(resp *http.Response) error {
M internal/adapters/in/http/proxy/forwarder_test.go +96 -0
@@ -5,6 +5,7 @@ import (
55 "net"
66 "net/http"
77 "net/http/httptest"
8+ "net/url"
89 "strconv"
910 "testing"
1011
@@ -13,6 +14,7 @@ import (
1314 "github.com/stretchr/testify/mock"
1415 "github.com/stretchr/testify/require"
1516
17+ "github.com/bnema/gordon/internal/adapters/in/http/middleware"
1618 "github.com/bnema/gordon/internal/boundaries/in"
1719 inmocks "github.com/bnema/gordon/internal/boundaries/in/mocks"
1820 "github.com/bnema/gordon/internal/domain"
@@ -198,3 +200,97 @@ func TestForwardToTarget_ProxyHeaderSet(t *testing.T) {
198200
199201 assert.Equal(t, http.StatusOK, w.Code)
200202 }
203+func TestNewReverseProxy_XForwardedProto(t *testing.T) {
204+ trustedNets := middleware.ParseTrustedProxies([]string{"10.0.0.0/8"})
205+
206+ tests := []struct {
207+ name string
208+ remoteAddr string
209+ incomingProto string // X-Forwarded-Proto on incoming request, empty = not set
210+ wantProto string // expected X-Forwarded-Proto at backend
211+ nets []*net.IPNet
212+ }{
213+ {
214+ name: "trusted proxy with https preserved",
215+ remoteAddr: "10.0.0.1:12345",
216+ incomingProto: "https",
217+ wantProto: "https",
218+ nets: trustedNets,
219+ },
220+ {
221+ name: "untrusted source proto overwritten",
222+ remoteAddr: "1.2.3.4:12345",
223+ incomingProto: "https",
224+ wantProto: "http",
225+ nets: trustedNets,
226+ },
227+ {
228+ name: "trusted proxy without proto set normally",
229+ remoteAddr: "10.0.0.1:12345",
230+ incomingProto: "",
231+ wantProto: "http",
232+ nets: trustedNets,
233+ },
234+ {
235+ name: "no trusted nets proto not preserved",
236+ remoteAddr: "10.0.0.1:12345",
237+ incomingProto: "https",
238+ wantProto: "http",
239+ nets: nil,
240+ },
241+ {
242+ name: "bare IPv4 no port trusted source preserves proto",
243+ remoteAddr: "10.0.0.1",
244+ incomingProto: "https",
245+ wantProto: "https",
246+ nets: trustedNets,
247+ },
248+ {
249+ name: "bare IPv6 with brackets trusted source preserves proto",
250+ remoteAddr: "[::1]",
251+ incomingProto: "https",
252+ wantProto: "https",
253+ nets: middleware.ParseTrustedProxies([]string{"::1/128"}),
254+ },
255+ {
256+ name: "bare IPv4 no port untrusted source strips proto",
257+ remoteAddr: "1.2.3.4",
258+ incomingProto: "https",
259+ wantProto: "http",
260+ nets: trustedNets,
261+ },
262+ }
263+
264+ for _, tt := range tests {
265+ t.Run(tt.name, func(t *testing.T) {
266+ var gotProto string
267+ backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
268+ gotProto = r.Header.Get("X-Forwarded-Proto")
269+ w.WriteHeader(http.StatusOK)
270+ }))
271+ defer backend.Close()
272+
273+ backendURL, err := url.Parse(backend.URL)
274+ require.NoError(t, err)
275+
276+ // Build incoming request (http scheme, no TLS)
277+ incomingReq := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
278+ incomingReq.RemoteAddr = tt.remoteAddr
279+ if tt.incomingProto != "" {
280+ incomingReq.Header.Set("X-Forwarded-Proto", tt.incomingProto)
281+ }
282+
283+ proxy := newReverseProxy(backendURL, "", http.DefaultTransport, incomingReq, tt.nets,
284+ func(w http.ResponseWriter, _ *http.Request, err error) {
285+ http.Error(w, err.Error(), http.StatusBadGateway)
286+ },
287+ nil,
288+ )
289+
290+ rec := httptest.NewRecorder()
291+ proxy.ServeHTTP(rec, incomingReq)
292+
293+ assert.Equal(t, tt.wantProto, gotProto)
294+ })
295+ }
296+}
@@ -5,6 +5,7 @@ import (
5 "net"5 "net"
6 "net/http"6 "net/http"
7 "net/http/httptest"7 "net/http/httptest"
8+ "net/url"
8 "strconv"9 "strconv"
9 "testing"10 "testing"
10 11
@@ -13,6 +14,7 @@ import (
13 "github.com/stretchr/testify/mock"14 "github.com/stretchr/testify/mock"
14 "github.com/stretchr/testify/require"15 "github.com/stretchr/testify/require"
15 16
17+ "github.com/bnema/gordon/internal/adapters/in/http/middleware"
16 "github.com/bnema/gordon/internal/boundaries/in"18 "github.com/bnema/gordon/internal/boundaries/in"
17 inmocks "github.com/bnema/gordon/internal/boundaries/in/mocks"19 inmocks "github.com/bnema/gordon/internal/boundaries/in/mocks"
18 "github.com/bnema/gordon/internal/domain"20 "github.com/bnema/gordon/internal/domain"
@@ -198,3 +200,97 @@ func TestForwardToTarget_ProxyHeaderSet(t *testing.T) {
198 200
199 assert.Equal(t, http.StatusOK, w.Code)201 assert.Equal(t, http.StatusOK, w.Code)
200 }202 }
203+func TestNewReverseProxy_XForwardedProto(t *testing.T) {
204+ trustedNets := middleware.ParseTrustedProxies([]string{"10.0.0.0/8"})
205+
206+ tests := []struct {
207+ name string
208+ remoteAddr string
209+ incomingProto string // X-Forwarded-Proto on incoming request, empty = not set
210+ wantProto string // expected X-Forwarded-Proto at backend
211+ nets []*net.IPNet
212+ }{
213+ {
214+ name: "trusted proxy with https preserved",
215+ remoteAddr: "10.0.0.1:12345",
216+ incomingProto: "https",
217+ wantProto: "https",
218+ nets: trustedNets,
219+ },
220+ {
221+ name: "untrusted source proto overwritten",
222+ remoteAddr: "1.2.3.4:12345",
223+ incomingProto: "https",
224+ wantProto: "http",
225+ nets: trustedNets,
226+ },
227+ {
228+ name: "trusted proxy without proto set normally",
229+ remoteAddr: "10.0.0.1:12345",
230+ incomingProto: "",
231+ wantProto: "http",
232+ nets: trustedNets,
233+ },
234+ {
235+ name: "no trusted nets proto not preserved",
236+ remoteAddr: "10.0.0.1:12345",
237+ incomingProto: "https",
238+ wantProto: "http",
239+ nets: nil,
240+ },
241+ {
242+ name: "bare IPv4 no port trusted source preserves proto",
243+ remoteAddr: "10.0.0.1",
244+ incomingProto: "https",
245+ wantProto: "https",
246+ nets: trustedNets,
247+ },
248+ {
249+ name: "bare IPv6 with brackets trusted source preserves proto",
250+ remoteAddr: "[::1]",
251+ incomingProto: "https",
252+ wantProto: "https",
253+ nets: middleware.ParseTrustedProxies([]string{"::1/128"}),
254+ },
255+ {
256+ name: "bare IPv4 no port untrusted source strips proto",
257+ remoteAddr: "1.2.3.4",
258+ incomingProto: "https",
259+ wantProto: "http",
260+ nets: trustedNets,
261+ },
262+ }
263+
264+ for _, tt := range tests {
265+ t.Run(tt.name, func(t *testing.T) {
266+ var gotProto string
267+ backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
268+ gotProto = r.Header.Get("X-Forwarded-Proto")
269+ w.WriteHeader(http.StatusOK)
270+ }))
271+ defer backend.Close()
272+
273+ backendURL, err := url.Parse(backend.URL)
274+ require.NoError(t, err)
275+
276+ // Build incoming request (http scheme, no TLS)
277+ incomingReq := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
278+ incomingReq.RemoteAddr = tt.remoteAddr
279+ if tt.incomingProto != "" {
280+ incomingReq.Header.Set("X-Forwarded-Proto", tt.incomingProto)
281+ }
282+
283+ proxy := newReverseProxy(backendURL, "", http.DefaultTransport, incomingReq, tt.nets,
284+ func(w http.ResponseWriter, _ *http.Request, err error) {
285+ http.Error(w, err.Error(), http.StatusBadGateway)
286+ },
287+ nil,
288+ )
289+
290+ rec := httptest.NewRecorder()
291+ proxy.ServeHTTP(rec, incomingReq)
292+
293+ assert.Equal(t, tt.wantProto, gotProto)
294+ })
295+ }
296+}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close