v2.31.0-dev9 062dcae

forwarder_test.go RawHistoryFolder

1package proxy
2
3import (
4	"fmt"
5	"net"
6	"net/http"
7	"net/http/httptest"
8	"net/url"
9	"strconv"
10	"testing"
11
12	"github.com/bnema/zerowrap"
13	"github.com/stretchr/testify/assert"
14	"github.com/stretchr/testify/mock"
15	"github.com/stretchr/testify/require"
16
17	gordonhttp "github.com/bnema/gordon/internal/adapters/in/http/httphelper"
18	"github.com/bnema/gordon/internal/boundaries/in"
19	inmocks "github.com/bnema/gordon/internal/boundaries/in/mocks"
20	"github.com/bnema/gordon/internal/domain"
21)
22
23func TestForwardToTarget_H2CEndToEnd(t *testing.T) {
24	var h2cProtos http.Protocols
25	h2cProtos.SetUnencryptedHTTP2(true)
26
27	h2cBackend := &http.Server{
28		Protocols: &h2cProtos,
29		Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
30			w.Header().Set("X-Proto", r.Proto)
31			w.WriteHeader(http.StatusOK)
32			_, _ = fmt.Fprintf(w, "proto=%s", r.Proto)
33		}),
34	}
35
36	ln, err := net.Listen("tcp", "127.0.0.1:0")
37	require.NoError(t, err)
38	defer ln.Close()
39
40	go func() {
41		_ = h2cBackend.Serve(ln)
42	}()
43	defer h2cBackend.Close()
44
45	_, portStr, splitErr := net.SplitHostPort(ln.Addr().String())
46	require.NoError(t, splitErr)
47	port, atoiErr := strconv.Atoi(portStr)
48	require.NoError(t, atoiErr)
49
50	proxySvc := inmocks.NewMockProxyService(t)
51
52	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{})
53	proxySvc.EXPECT().IsRegistryDomain("grpc.example.com").Return(false)
54	proxySvc.EXPECT().GetTarget(mock.Anything, "grpc.example.com").Return(&domain.ProxyTarget{
55		Host:        "127.0.0.1",
56		Port:        port,
57		ContainerID: "grpc-1",
58		Scheme:      "http",
59		Protocol:    "h2c",
60	}, nil)
61	proxySvc.EXPECT().TrackInFlight("grpc-1").Return(func() {})
62
63	handler := NewHandler(proxySvc, nil, zerowrap.Default())
64
65	req := httptest.NewRequest(http.MethodGet, "http://grpc.example.com/test", nil)
66	req.Host = "grpc.example.com"
67	rec := httptest.NewRecorder()
68
69	handler.ServeHTTP(rec, req)
70
71	assert.Equal(t, http.StatusOK, rec.Code)
72	assert.Contains(t, rec.Body.String(), "HTTP/2")
73}
74
75func TestForwardToTarget_HTTP1StillWorks(t *testing.T) {
76	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
77		_, _ = fmt.Fprintf(w, "proto=%s", r.Proto)
78	}))
79	defer backend.Close()
80
81	backendPort := backend.Listener.Addr().(*net.TCPAddr).Port
82
83	proxySvc := inmocks.NewMockProxyService(t)
84
85	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{})
86	proxySvc.EXPECT().IsRegistryDomain("web.example.com").Return(false)
87	proxySvc.EXPECT().GetTarget(mock.Anything, "web.example.com").Return(&domain.ProxyTarget{
88		Host:        "127.0.0.1",
89		Port:        backendPort,
90		ContainerID: "web-1",
91		Scheme:      "http",
92	}, nil)
93	proxySvc.EXPECT().TrackInFlight("web-1").Return(func() {})
94
95	handler := NewHandler(proxySvc, nil, zerowrap.Default())
96
97	req := httptest.NewRequest(http.MethodGet, "http://web.example.com/", nil)
98	req.Host = "web.example.com"
99	rec := httptest.NewRecorder()
100
101	handler.ServeHTTP(rec, req)
102
103	assert.Equal(t, http.StatusOK, rec.Code)
104	assert.Contains(t, rec.Body.String(), "HTTP/1.1")
105}
106
107func TestTransportForTarget_SelectsCorrectTransport(t *testing.T) {
108	handler := NewHandler(inmocks.NewMockProxyService(t), nil, zerowrap.Default())
109
110	h2cTarget := &domain.ProxyTarget{Protocol: "h2c"}
111	httpTarget := &domain.ProxyTarget{Protocol: ""}
112
113	assert.Equal(t, handler.h2cTransport, handler.transportForTarget(h2cTarget))
114	assert.Equal(t, handler.appTransport, handler.transportForTarget(httpTarget))
115}
116
117func TestForwardToTarget_OriginalHostPreserved(t *testing.T) {
118	var receivedHost string
119	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
120		receivedHost = r.Host
121		w.WriteHeader(http.StatusOK)
122	}))
123	defer backend.Close()
124
125	backendPort := backend.Listener.Addr().(*net.TCPAddr).Port
126
127	proxySvc := inmocks.NewMockProxyService(t)
128
129	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{})
130	proxySvc.EXPECT().IsRegistryDomain("external.example.com").Return(false)
131	proxySvc.EXPECT().GetTarget(mock.Anything, "external.example.com").Return(&domain.ProxyTarget{
132		Host:         "127.0.0.1",
133		Port:         backendPort,
134		ContainerID:  "",
135		Scheme:       "http",
136		OriginalHost: "upstream.example.com",
137	}, nil)
138	proxySvc.EXPECT().TrackInFlight("").Return(func() {})
139
140	handler := NewHandler(proxySvc, nil, zerowrap.Default())
141
142	req := httptest.NewRequest(http.MethodGet, "http://external.example.com/", nil)
143	req.Host = "external.example.com"
144	w := httptest.NewRecorder()
145	handler.ServeHTTP(w, req)
146
147	assert.Equal(t, http.StatusOK, w.Code)
148	assert.Contains(t, receivedHost, "upstream.example.com")
149}
150
151func TestForwardToTarget_BackendDown_Returns503(t *testing.T) {
152	proxySvc := inmocks.NewMockProxyService(t)
153
154	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{})
155	proxySvc.EXPECT().IsRegistryDomain("down.example.com").Return(false)
156	proxySvc.EXPECT().GetTarget(mock.Anything, "down.example.com").Return(&domain.ProxyTarget{
157		Host:        "127.0.0.1",
158		Port:        1,
159		ContainerID: "c-down",
160		Scheme:      "http",
161	}, nil)
162	proxySvc.EXPECT().TrackInFlight("c-down").Return(func() {})
163
164	handler := NewHandler(proxySvc, nil, zerowrap.Default())
165
166	req := httptest.NewRequest(http.MethodGet, "http://down.example.com/", nil)
167	req.Host = "down.example.com"
168	w := httptest.NewRecorder()
169	handler.ServeHTTP(w, req)
170
171	assert.Equal(t, http.StatusServiceUnavailable, w.Code)
172}
173
174func TestForwardToTarget_ProxyHeaderSet(t *testing.T) {
175	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
176		w.WriteHeader(http.StatusOK)
177	}))
178	defer backend.Close()
179
180	backendPort := backend.Listener.Addr().(*net.TCPAddr).Port
181
182	proxySvc := inmocks.NewMockProxyService(t)
183
184	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{})
185	proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false)
186	proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(&domain.ProxyTarget{
187		Host:        "127.0.0.1",
188		Port:        backendPort,
189		ContainerID: "c-1",
190		Scheme:      "http",
191	}, nil)
192	proxySvc.EXPECT().TrackInFlight("c-1").Return(func() {})
193
194	handler := NewHandler(proxySvc, nil, zerowrap.Default())
195
196	req := httptest.NewRequest(http.MethodGet, "http://app.example.com/", nil)
197	req.Host = "app.example.com"
198	w := httptest.NewRecorder()
199	handler.ServeHTTP(w, req)
200
201	assert.Equal(t, http.StatusOK, w.Code)
202}
203func TestNewReverseProxy_XForwardedProto(t *testing.T) {
204	trustedNets := gordonhttp.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:          gordonhttp.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