v2.31.0-dev22 8d342c4

forwarder_test.go RawHistoryFolder

1package proxy
2
3import (
4	"context"
5	"fmt"
6	"net"
7	"net/http"
8	"net/http/httptest"
9	"net/url"
10	"strconv"
11	"testing"
12
13	"github.com/bnema/zerowrap"
14	"github.com/stretchr/testify/assert"
15	"github.com/stretchr/testify/mock"
16	"github.com/stretchr/testify/require"
17
18	gordonhttp "github.com/bnema/gordon/internal/adapters/in/http/httphelper"
19	"github.com/bnema/gordon/internal/boundaries/in"
20	inmocks "github.com/bnema/gordon/internal/boundaries/in/mocks"
21	"github.com/bnema/gordon/internal/domain"
22)
23
24type roundTripFunc func(*http.Request) (*http.Response, error)
25
26func (f roundTripFunc) RoundTrip(r *http.Request) (*http.Response, error) {
27	return f(r)
28}
29
30func TestForwardToTarget_H2CEndToEnd(t *testing.T) {
31	var h2cProtos http.Protocols
32	h2cProtos.SetUnencryptedHTTP2(true)
33
34	h2cBackend := &http.Server{
35		Protocols: &h2cProtos,
36		Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
37			w.Header().Set("X-Proto", r.Proto)
38			w.WriteHeader(http.StatusOK)
39			_, _ = fmt.Fprintf(w, "proto=%s", r.Proto)
40		}),
41	}
42
43	ln, err := net.Listen("tcp", "127.0.0.1:0")
44	require.NoError(t, err)
45	defer ln.Close()
46
47	go func() {
48		_ = h2cBackend.Serve(ln)
49	}()
50	defer h2cBackend.Close()
51
52	_, portStr, splitErr := net.SplitHostPort(ln.Addr().String())
53	require.NoError(t, splitErr)
54	port, atoiErr := strconv.Atoi(portStr)
55	require.NoError(t, atoiErr)
56
57	proxySvc := inmocks.NewMockProxyService(t)
58
59	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{})
60	proxySvc.EXPECT().IsRegistryDomain("grpc.example.com").Return(false)
61	proxySvc.EXPECT().GetTarget(mock.Anything, "grpc.example.com").Return(&domain.ProxyTarget{
62		Host:        "127.0.0.1",
63		Port:        port,
64		ContainerID: "grpc-1",
65		Scheme:      "http",
66		Protocol:    "h2c",
67	}, nil)
68	proxySvc.EXPECT().TrackInFlight("grpc-1").Return(func() {})
69
70	handler := NewHandler(proxySvc, nil, zerowrap.Default())
71
72	req := httptest.NewRequest(http.MethodGet, "http://grpc.example.com/test", nil)
73	req.Host = "grpc.example.com"
74	rec := httptest.NewRecorder()
75
76	handler.ServeHTTP(rec, req)
77
78	assert.Equal(t, http.StatusOK, rec.Code)
79	assert.Contains(t, rec.Body.String(), "HTTP/2")
80}
81
82func TestForwardToTarget_HTTP1StillWorks(t *testing.T) {
83	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
84		_, _ = fmt.Fprintf(w, "proto=%s", r.Proto)
85	}))
86	defer backend.Close()
87
88	backendPort := backend.Listener.Addr().(*net.TCPAddr).Port
89
90	proxySvc := inmocks.NewMockProxyService(t)
91
92	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{})
93	proxySvc.EXPECT().IsRegistryDomain("web.example.com").Return(false)
94	proxySvc.EXPECT().GetTarget(mock.Anything, "web.example.com").Return(&domain.ProxyTarget{
95		Host:        "127.0.0.1",
96		Port:        backendPort,
97		ContainerID: "web-1",
98		Scheme:      "http",
99	}, nil)
100	proxySvc.EXPECT().TrackInFlight("web-1").Return(func() {})
101
102	handler := NewHandler(proxySvc, nil, zerowrap.Default())
103
104	req := httptest.NewRequest(http.MethodGet, "http://web.example.com/", nil)
105	req.Host = "web.example.com"
106	rec := httptest.NewRecorder()
107
108	handler.ServeHTTP(rec, req)
109
110	assert.Equal(t, http.StatusOK, rec.Code)
111	assert.Contains(t, rec.Body.String(), "HTTP/1.1")
112}
113
114func TestTransportForTarget_SelectsCorrectTransport(t *testing.T) {
115	handler := NewHandler(inmocks.NewMockProxyService(t), nil, zerowrap.Default())
116
117	h2cTarget := &domain.ProxyTarget{Protocol: "h2c"}
118	httpTarget := &domain.ProxyTarget{Protocol: ""}
119
120	assert.Equal(t, handler.h2cTransport, handler.transportForTarget(h2cTarget))
121	assert.Equal(t, handler.appTransport, handler.transportForTarget(httpTarget))
122}
123
124func TestForwardToTarget_OriginalHostPreserved(t *testing.T) {
125	var receivedHost string
126	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
127		receivedHost = r.Host
128		w.WriteHeader(http.StatusOK)
129	}))
130	defer backend.Close()
131
132	backendPort := backend.Listener.Addr().(*net.TCPAddr).Port
133
134	proxySvc := inmocks.NewMockProxyService(t)
135
136	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{})
137	proxySvc.EXPECT().IsRegistryDomain("external.example.com").Return(false)
138	proxySvc.EXPECT().GetTarget(mock.Anything, "external.example.com").Return(&domain.ProxyTarget{
139		Host:         "127.0.0.1",
140		Port:         backendPort,
141		ContainerID:  "",
142		Scheme:       "http",
143		OriginalHost: "upstream.example.com",
144	}, nil)
145	proxySvc.EXPECT().TrackInFlight("").Return(func() {})
146
147	handler := NewHandler(proxySvc, nil, zerowrap.Default())
148
149	req := httptest.NewRequest(http.MethodGet, "http://external.example.com/", nil)
150	req.Host = "external.example.com"
151	w := httptest.NewRecorder()
152	handler.ServeHTTP(w, req)
153
154	assert.Equal(t, http.StatusOK, w.Code)
155	assert.Equal(t, net.JoinHostPort("upstream.example.com", strconv.Itoa(backendPort)), receivedHost)
156}
157
158func TestForwardToTarget_RouteHostPreservedForManagedTarget(t *testing.T) {
159	var receivedHost string
160	var receivedForwardedHost string
161	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
162		receivedHost = r.Host
163		receivedForwardedHost = r.Header.Get("X-Forwarded-Host")
164		w.WriteHeader(http.StatusOK)
165	}))
166	defer backend.Close()
167
168	backendPort := backend.Listener.Addr().(*net.TCPAddr).Port
169
170	proxySvc := inmocks.NewMockProxyService(t)
171
172	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{})
173	proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false)
174	proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(&domain.ProxyTarget{
175		Host:        "127.0.0.1",
176		Port:        backendPort,
177		ContainerID: "c-1",
178		Scheme:      "http",
179		RouteHost:   "app.example.com",
180	}, nil)
181	proxySvc.EXPECT().TrackInFlight("c-1").Return(func() {})
182
183	handler := NewHandler(proxySvc, nil, zerowrap.Default())
184
185	req := httptest.NewRequest(http.MethodGet, "http://app.example.com/", nil)
186	req.Host = "App.Example.com:443"
187	w := httptest.NewRecorder()
188	handler.ServeHTTP(w, req)
189
190	assert.Equal(t, http.StatusOK, w.Code)
191	assert.Equal(t, "app.example.com", receivedHost)
192	assert.Equal(t, "app.example.com", receivedForwardedHost)
193}
194
195func TestForwardToTarget_ContextCanceled_Returns499(t *testing.T) {
196	proxySvc := inmocks.NewMockProxyService(t)
197
198	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{})
199	proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false)
200	proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(&domain.ProxyTarget{
201		Host:        "127.0.0.1",
202		Port:        8080,
203		ContainerID: "c-sse",
204		Scheme:      "http",
205	}, nil)
206	proxySvc.EXPECT().TrackInFlight("c-sse").Return(func() {})
207
208	handler := NewHandler(proxySvc, nil, zerowrap.Default())
209	handler.appTransport = roundTripFunc(func(*http.Request) (*http.Response, error) {
210		return nil, context.Canceled
211	})
212
213	req := httptest.NewRequest(http.MethodGet, "http://app.example.com/api/notifications/sent-events", nil)
214	req.Host = "app.example.com"
215	w := httptest.NewRecorder()
216	handler.ServeHTTP(w, req)
217
218	assert.Equal(t, statusClientClosedRequest, w.Code)
219	assert.Equal(t, "no-store", w.Header().Get("Cache-Control"))
220	assert.Equal(t, "default-src 'none'; frame-ancestors 'none'", w.Header().Get("Content-Security-Policy"))
221	assert.Empty(t, w.Body.String())
222}
223
224func TestForwardToTarget_BackendDown_Returns503(t *testing.T) {
225	proxySvc := inmocks.NewMockProxyService(t)
226
227	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{})
228	proxySvc.EXPECT().IsRegistryDomain("down.example.com").Return(false)
229	proxySvc.EXPECT().GetTarget(mock.Anything, "down.example.com").Return(&domain.ProxyTarget{
230		Host:        "127.0.0.1",
231		Port:        1,
232		ContainerID: "c-down",
233		Scheme:      "http",
234	}, nil)
235	proxySvc.EXPECT().TrackInFlight("c-down").Return(func() {})
236
237	handler := NewHandler(proxySvc, nil, zerowrap.Default())
238
239	req := httptest.NewRequest(http.MethodGet, "http://down.example.com/", nil)
240	req.Host = "down.example.com"
241	w := httptest.NewRecorder()
242	handler.ServeHTTP(w, req)
243
244	assert.Equal(t, http.StatusServiceUnavailable, w.Code)
245}
246
247func TestForwardToTarget_ProxyHeaderSet(t *testing.T) {
248	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
249		w.WriteHeader(http.StatusOK)
250	}))
251	defer backend.Close()
252
253	backendPort := backend.Listener.Addr().(*net.TCPAddr).Port
254
255	proxySvc := inmocks.NewMockProxyService(t)
256
257	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{})
258	proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false)
259	proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(&domain.ProxyTarget{
260		Host:        "127.0.0.1",
261		Port:        backendPort,
262		ContainerID: "c-1",
263		Scheme:      "http",
264	}, nil)
265	proxySvc.EXPECT().TrackInFlight("c-1").Return(func() {})
266
267	handler := NewHandler(proxySvc, nil, zerowrap.Default())
268
269	req := httptest.NewRequest(http.MethodGet, "http://app.example.com/", nil)
270	req.Host = "app.example.com"
271	w := httptest.NewRecorder()
272	handler.ServeHTTP(w, req)
273
274	assert.Equal(t, http.StatusOK, w.Code)
275}
276func TestNewReverseProxy_XForwardedProto(t *testing.T) {
277	trustedNets := gordonhttp.ParseTrustedProxies([]string{"10.0.0.0/8"})
278
279	tests := []struct {
280		name          string
281		remoteAddr    string
282		incomingProto string // X-Forwarded-Proto on incoming request, empty = not set
283		wantProto     string // expected X-Forwarded-Proto at backend
284		nets          []*net.IPNet
285	}{
286		{
287			name:          "trusted proxy with https preserved",
288			remoteAddr:    "10.0.0.1:12345",
289			incomingProto: "https",
290			wantProto:     "https",
291			nets:          trustedNets,
292		},
293		{
294			name:          "untrusted source proto overwritten",
295			remoteAddr:    "1.2.3.4:12345",
296			incomingProto: "https",
297			wantProto:     "http",
298			nets:          trustedNets,
299		},
300		{
301			name:          "trusted proxy without proto set normally",
302			remoteAddr:    "10.0.0.1:12345",
303			incomingProto: "",
304			wantProto:     "http",
305			nets:          trustedNets,
306		},
307		{
308			name:          "no trusted nets proto not preserved",
309			remoteAddr:    "10.0.0.1:12345",
310			incomingProto: "https",
311			wantProto:     "http",
312			nets:          nil,
313		},
314		{
315			name:          "bare IPv4 no port trusted source preserves proto",
316			remoteAddr:    "10.0.0.1",
317			incomingProto: "https",
318			wantProto:     "https",
319			nets:          trustedNets,
320		},
321		{
322			name:          "bare IPv6 with brackets trusted source preserves proto",
323			remoteAddr:    "[::1]",
324			incomingProto: "https",
325			wantProto:     "https",
326			nets:          gordonhttp.ParseTrustedProxies([]string{"::1/128"}),
327		},
328		{
329			name:          "bare IPv4 no port untrusted source strips proto",
330			remoteAddr:    "1.2.3.4",
331			incomingProto: "https",
332			wantProto:     "http",
333			nets:          trustedNets,
334		},
335	}
336
337	for _, tt := range tests {
338		t.Run(tt.name, func(t *testing.T) {
339			var gotProto string
340			backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
341				gotProto = r.Header.Get("X-Forwarded-Proto")
342				w.WriteHeader(http.StatusOK)
343			}))
344			defer backend.Close()
345
346			backendURL, err := url.Parse(backend.URL)
347			require.NoError(t, err)
348
349			// Build incoming request (http scheme, no TLS)
350			incomingReq := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
351			incomingReq.RemoteAddr = tt.remoteAddr
352			if tt.incomingProto != "" {
353				incomingReq.Header.Set("X-Forwarded-Proto", tt.incomingProto)
354			}
355
356			proxy := newReverseProxy(reverseProxyOptions{
357				targetURL:   backendURL,
358				transport:   http.DefaultTransport,
359				incomingReq: incomingReq,
360				trustedNets: tt.nets,
361				errorHandler: func(w http.ResponseWriter, _ *http.Request, err error) {
362					http.Error(w, err.Error(), http.StatusBadGateway)
363				},
364			})
365
366			rec := httptest.NewRecorder()
367			proxy.ServeHTTP(rec, incomingReq)
368
369			assert.Equal(t, tt.wantProto, gotProto)
370		})
371	}
372}

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