v2.31.0-dev12 6a38a35

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.Equal(t, net.JoinHostPort("upstream.example.com", strconv.Itoa(backendPort)), receivedHost)
149}
150
151func TestForwardToTarget_RouteHostPreservedForManagedTarget(t *testing.T) {
152	var receivedHost string
153	var receivedForwardedHost string
154	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
155		receivedHost = r.Host
156		receivedForwardedHost = r.Header.Get("X-Forwarded-Host")
157		w.WriteHeader(http.StatusOK)
158	}))
159	defer backend.Close()
160
161	backendPort := backend.Listener.Addr().(*net.TCPAddr).Port
162
163	proxySvc := inmocks.NewMockProxyService(t)
164
165	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{})
166	proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false)
167	proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(&domain.ProxyTarget{
168		Host:        "127.0.0.1",
169		Port:        backendPort,
170		ContainerID: "c-1",
171		Scheme:      "http",
172		RouteHost:   "app.example.com",
173	}, nil)
174	proxySvc.EXPECT().TrackInFlight("c-1").Return(func() {})
175
176	handler := NewHandler(proxySvc, nil, zerowrap.Default())
177
178	req := httptest.NewRequest(http.MethodGet, "http://app.example.com/", nil)
179	req.Host = "App.Example.com:443"
180	w := httptest.NewRecorder()
181	handler.ServeHTTP(w, req)
182
183	assert.Equal(t, http.StatusOK, w.Code)
184	assert.Equal(t, "app.example.com", receivedHost)
185	assert.Equal(t, "app.example.com", receivedForwardedHost)
186}
187
188func TestForwardToTarget_BackendDown_Returns503(t *testing.T) {
189	proxySvc := inmocks.NewMockProxyService(t)
190
191	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{})
192	proxySvc.EXPECT().IsRegistryDomain("down.example.com").Return(false)
193	proxySvc.EXPECT().GetTarget(mock.Anything, "down.example.com").Return(&domain.ProxyTarget{
194		Host:        "127.0.0.1",
195		Port:        1,
196		ContainerID: "c-down",
197		Scheme:      "http",
198	}, nil)
199	proxySvc.EXPECT().TrackInFlight("c-down").Return(func() {})
200
201	handler := NewHandler(proxySvc, nil, zerowrap.Default())
202
203	req := httptest.NewRequest(http.MethodGet, "http://down.example.com/", nil)
204	req.Host = "down.example.com"
205	w := httptest.NewRecorder()
206	handler.ServeHTTP(w, req)
207
208	assert.Equal(t, http.StatusServiceUnavailable, w.Code)
209}
210
211func TestForwardToTarget_ProxyHeaderSet(t *testing.T) {
212	backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
213		w.WriteHeader(http.StatusOK)
214	}))
215	defer backend.Close()
216
217	backendPort := backend.Listener.Addr().(*net.TCPAddr).Port
218
219	proxySvc := inmocks.NewMockProxyService(t)
220
221	proxySvc.EXPECT().ProxyConfig().Return(in.ProxyServiceConfig{})
222	proxySvc.EXPECT().IsRegistryDomain("app.example.com").Return(false)
223	proxySvc.EXPECT().GetTarget(mock.Anything, "app.example.com").Return(&domain.ProxyTarget{
224		Host:        "127.0.0.1",
225		Port:        backendPort,
226		ContainerID: "c-1",
227		Scheme:      "http",
228	}, nil)
229	proxySvc.EXPECT().TrackInFlight("c-1").Return(func() {})
230
231	handler := NewHandler(proxySvc, nil, zerowrap.Default())
232
233	req := httptest.NewRequest(http.MethodGet, "http://app.example.com/", nil)
234	req.Host = "app.example.com"
235	w := httptest.NewRecorder()
236	handler.ServeHTTP(w, req)
237
238	assert.Equal(t, http.StatusOK, w.Code)
239}
240func TestNewReverseProxy_XForwardedProto(t *testing.T) {
241	trustedNets := gordonhttp.ParseTrustedProxies([]string{"10.0.0.0/8"})
242
243	tests := []struct {
244		name          string
245		remoteAddr    string
246		incomingProto string // X-Forwarded-Proto on incoming request, empty = not set
247		wantProto     string // expected X-Forwarded-Proto at backend
248		nets          []*net.IPNet
249	}{
250		{
251			name:          "trusted proxy with https preserved",
252			remoteAddr:    "10.0.0.1:12345",
253			incomingProto: "https",
254			wantProto:     "https",
255			nets:          trustedNets,
256		},
257		{
258			name:          "untrusted source proto overwritten",
259			remoteAddr:    "1.2.3.4:12345",
260			incomingProto: "https",
261			wantProto:     "http",
262			nets:          trustedNets,
263		},
264		{
265			name:          "trusted proxy without proto set normally",
266			remoteAddr:    "10.0.0.1:12345",
267			incomingProto: "",
268			wantProto:     "http",
269			nets:          trustedNets,
270		},
271		{
272			name:          "no trusted nets proto not preserved",
273			remoteAddr:    "10.0.0.1:12345",
274			incomingProto: "https",
275			wantProto:     "http",
276			nets:          nil,
277		},
278		{
279			name:          "bare IPv4 no port trusted source preserves proto",
280			remoteAddr:    "10.0.0.1",
281			incomingProto: "https",
282			wantProto:     "https",
283			nets:          trustedNets,
284		},
285		{
286			name:          "bare IPv6 with brackets trusted source preserves proto",
287			remoteAddr:    "[::1]",
288			incomingProto: "https",
289			wantProto:     "https",
290			nets:          gordonhttp.ParseTrustedProxies([]string{"::1/128"}),
291		},
292		{
293			name:          "bare IPv4 no port untrusted source strips proto",
294			remoteAddr:    "1.2.3.4",
295			incomingProto: "https",
296			wantProto:     "http",
297			nets:          trustedNets,
298		},
299	}
300
301	for _, tt := range tests {
302		t.Run(tt.name, func(t *testing.T) {
303			var gotProto string
304			backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
305				gotProto = r.Header.Get("X-Forwarded-Proto")
306				w.WriteHeader(http.StatusOK)
307			}))
308			defer backend.Close()
309
310			backendURL, err := url.Parse(backend.URL)
311			require.NoError(t, err)
312
313			// Build incoming request (http scheme, no TLS)
314			incomingReq := httptest.NewRequest(http.MethodGet, "http://example.com/", nil)
315			incomingReq.RemoteAddr = tt.remoteAddr
316			if tt.incomingProto != "" {
317				incomingReq.Header.Set("X-Forwarded-Proto", tt.incomingProto)
318			}
319
320			proxy := newReverseProxy(reverseProxyOptions{
321				targetURL:   backendURL,
322				transport:   http.DefaultTransport,
323				incomingReq: incomingReq,
324				trustedNets: tt.nets,
325				errorHandler: func(w http.ResponseWriter, _ *http.Request, err error) {
326					http.Error(w, err.Error(), http.StatusBadGateway)
327				},
328			})
329
330			rec := httptest.NewRecorder()
331			proxy.ServeHTTP(rec, incomingReq)
332
333			assert.Equal(t, tt.wantProto, gotProto)
334		})
335	}
336}

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 8

Tags 106

↑↓ moveenter openesc close