main bac21c6

start_proxy_servers_test.go RawHistoryFolder

1package app
2
3import (
4	"context"
5	"crypto/tls"
6	"errors"
7	"fmt"
8	"io"
9	"net"
10	"net/http"
11	"path/filepath"
12	"strconv"
13	"testing"
14	"time"
15
16	"github.com/bnema/zerowrap"
17	"github.com/stretchr/testify/require"
18
19	trafficadapter "github.com/bnema/gordon/internal/adapters/in/traffic"
20	"github.com/bnema/gordon/internal/adapters/localadmin"
21	pkiadapter "github.com/bnema/gordon/internal/adapters/out/pki"
22	inmocks "github.com/bnema/gordon/internal/boundaries/in/mocks"
23	"github.com/bnema/gordon/internal/domain"
24	pkiusecase "github.com/bnema/gordon/internal/usecase/pki"
25	traffic "github.com/bnema/gordon/internal/usecase/traffic"
26)
27
28func TestWaitForServerReady_NilReadyReturnsImmediately(t *testing.T) {
29	errChan := make(chan error)
30	done := make(chan error, 1)
31
32	go func() {
33		done <- waitForServerReady(context.Background(), nil, errChan)
34	}()
35
36	select {
37	case err := <-done:
38		require.NoError(t, err)
39	case <-time.After(100 * time.Millisecond):
40		t.Fatal("nil ready channel blocked startup readiness")
41	}
42}
43
44func TestWaitForServerReady_NonNilReadyPreservesErrorBehavior(t *testing.T) {
45	expected := errors.New("listen failed")
46	errChan := make(chan error, 1)
47	errChan <- expected
48
49	err := waitForServerReady(context.Background(), make(chan struct{}), errChan)
50	require.ErrorIs(t, err, expected)
51}
52
53func TestWaitForServerReady_ReturnsOnContextCancellation(t *testing.T) {
54	ctx, cancel := context.WithCancel(context.Background())
55	cancel()
56
57	// A canceled startup context must unblock readiness instead of waiting on a
58	// server that will never signal ready.
59	err := waitForServerReady(ctx, make(chan struct{}), make(chan error))
60	require.ErrorIs(t, err, context.Canceled)
61}
62
63func TestCleanupStartupResources_ClosesAdminSocketsServersAndTrafficManager(t *testing.T) {
64	xdg := t.TempDir()
65	t.Setenv("XDG_RUNTIME_DIR", xdg)
66
67	localAdmin, err := startLocalAdminServer(localAdminTestServices(t, inmocks.NewMockAppService(t)), make(chan error, 4), zerowrap.Default())
68	require.NoError(t, err)
69	require.NotNil(t, localAdmin)
70	socketPath := localadmin.SocketPath(filepath.Join(xdg, "gordon"))
71	require.NoError(t, localadmin.ValidateSocket(socketPath))
72
73	ln, err := net.Listen("tcp", "127.0.0.1:0")
74	require.NoError(t, err)
75	started := make(chan struct{})
76	srv := &http.Server{Handler: http.HandlerFunc(func(http.ResponseWriter, *http.Request) {})}
77	go func() {
78		close(started)
79		_ = srv.Serve(ln)
80	}()
81	<-started
82	addr := ln.Addr().String()
83
84	manager := trafficadapter.NewManager()
85	cleanupStartupResources(localAdmin, manager, zerowrap.Default(), srv)
86
87	require.NoFileExists(t, socketPath, "local admin socket must be closed")
88	_, err = net.DialTimeout("tcp", addr, 200*time.Millisecond)
89	require.Error(t, err, "bound server must be shut down")
90	require.Empty(t, manager.Status().EntryPoints)
91}
92
93func TestStartProxyServers_DoesNotStartLegacyHTTPListenerFromServerPort(t *testing.T) {
94	cfg := Config{}
95	cfg.Server.Port = freeTCPPort(t)
96
97	manager := trafficadapter.NewManager()
98	defer func() { require.NoError(t, manager.Shutdown(context.Background())) }()
99
100	httpSrv, httpReady, tlsSrv, tlsReady, err := startProxyServers(
101		cfg,
102		http.NotFoundHandler(),
103		http.NotFoundHandler(),
104		newTestPKIService(t),
105		nil,
106		manager,
107		zerowrap.Default(),
108	)
109	require.NoError(t, err)
110	require.Nil(t, httpSrv)
111	require.Nil(t, httpReady)
112	require.Nil(t, tlsSrv)
113	require.Nil(t, tlsReady)
114	assertTCPPortClosed(t, fmt.Sprintf("127.0.0.1:%d", cfg.Server.Port))
115}
116
117func TestStartProxyServers_DoesNotTreatLegacyTLSPortAsTLSCapable(t *testing.T) {
118	cfg := Config{}
119	cfg.Server.TLSPort = freeTCPPort(t)
120
121	manager := trafficadapter.NewManager()
122	defer func() { require.NoError(t, manager.Shutdown(context.Background())) }()
123
124	httpSrv, httpReady, tlsSrv, tlsReady, err := startProxyServers(
125		cfg,
126		http.NotFoundHandler(),
127		http.NotFoundHandler(),
128		newTestPKIService(t),
129		nil,
130		manager,
131		zerowrap.Default(),
132	)
133	require.NoError(t, err)
134	require.Nil(t, httpSrv)
135	require.Nil(t, httpReady)
136	require.Nil(t, tlsSrv)
137	require.Nil(t, tlsReady)
138	assertTCPPortClosed(t, fmt.Sprintf("127.0.0.1:%d", cfg.Server.TLSPort))
139}
140
141func TestStartProxyServers_ConfiguresSmartTCPHTTPAndHTTPSRoutesWithoutPublicHTTPListener(t *testing.T) {
142	cfg := Config{}
143	cfg.Server.Port = freeTCPPort(t)
144	cfg.EntryPoints = map[string]traffic.EntryPointConfig{
145		traffic.DefaultEdgeEntryPointName: {
146			Address:  freeTCPAddress(t),
147			Protocol: domain.EntryPointProtocolSmartTCP,
148		},
149	}
150
151	manager := trafficadapter.NewManager()
152	defer func() { require.NoError(t, manager.Shutdown(context.Background())) }()
153	pkiSvc := newTestPKIService(t)
154
155	httpSrv, httpReady, tlsSrv, tlsReady, err := startProxyServers(
156		cfg,
157		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("cleartext " + r.Proto)) }),
158		http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("secure")) }),
159		pkiSvc,
160		nil,
161		manager,
162		zerowrap.Default(),
163	)
164	require.NoError(t, err)
165	require.Nil(t, httpSrv)
166	require.Nil(t, httpReady)
167	require.Nil(t, tlsSrv)
168	require.Nil(t, tlsReady)
169	assertTCPPortClosed(t, fmt.Sprintf("127.0.0.1:%d", cfg.Server.Port))
170
171	configSvc := inmocks.NewMockConfigService(t)
172	configSvc.EXPECT().GetExternalRoutes().Return(map[string]string{})
173	require.NoError(t, applyTrafficRuntimeConfig(context.Background(), manager, cfg, configSvc, stubHosts("app.example.com")))
174
175	_, smartPortText, err := net.SplitHostPort(cfg.EntryPoints[traffic.DefaultEdgeEntryPointName].Address)
176	require.NoError(t, err)
177	smartPort, err := strconv.Atoi(smartPortText)
178	require.NoError(t, err)
179	require.Contains(t, httpGetForTest(t, smartPort, "app.example.com"), "cleartext HTTP/1.1")
180	require.Contains(t, httpsGetForTest(t, smartPort, "app.example.com"), "secure")
181}
182
183func TestStartProxyServers_ConfiguresTrafficManagerHTTPSRoute(t *testing.T) {
184	cfg := Config{}
185	cfg.EntryPoints = map[string]traffic.EntryPointConfig{
186		traffic.DefaultEdgeEntryPointName: {
187			Address:  freeTCPAddress(t),
188			Protocol: domain.EntryPointProtocolTLSMux,
189		},
190	}
191
192	manager := trafficadapter.NewManager()
193	defer func() { require.NoError(t, manager.Shutdown(context.Background())) }()
194	pkiSvc := newTestPKIService(t)
195
196	httpSrv, httpReady, tlsSrv, tlsReady, err := startProxyServers(
197		cfg,
198		http.NotFoundHandler(),
199		http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("secure")) }),
200		pkiSvc,
201		nil,
202		manager,
203		zerowrap.Default(),
204	)
205	require.NoError(t, err)
206	require.Nil(t, httpSrv)
207	require.Nil(t, httpReady)
208	require.Nil(t, tlsSrv)
209	require.Nil(t, tlsReady)
210
211	configSvc := inmocks.NewMockConfigService(t)
212	configSvc.EXPECT().GetExternalRoutes().Return(map[string]string{})
213	require.NoError(t, applyTrafficRuntimeConfig(context.Background(), manager, cfg, configSvc, stubHosts("app.example.com")))
214
215	_, portText, err := net.SplitHostPort(cfg.EntryPoints[traffic.DefaultEdgeEntryPointName].Address)
216	require.NoError(t, err)
217	port, err := strconv.Atoi(portText)
218	require.NoError(t, err)
219	body := httpsGetForTest(t, port, "app.example.com")
220	require.Contains(t, body, "secure")
221}
222
223func TestStartProxyServers_ConfiguresCustomTLSMuxHTTPSRoute(t *testing.T) {
224	cfg := Config{}
225	cfg.Server.Port = freeTCPPort(t)
226	cfg.Server.TLSPort = freeTCPPort(t)
227	cfg.EntryPoints = map[string]traffic.EntryPointConfig{
228		"custom-secure": {Address: fmt.Sprintf("127.0.0.1:%d", freeTCPPort(t)), Protocol: domain.EntryPointProtocolTLSMux},
229	}
230	cfg.Traffic.TLS.Routers = []traffic.RouterConfig{{Name: "raw", EntryPoint: "custom-secure", SNI: "raw.example.com", Service: "network_service:raw:tls"}}
231	cfg.NetworkServices = []traffic.NetworkServiceConfig{{Name: "raw", Ports: []traffic.PortConfig{{Name: "tls", Container: 443, Protocol: domain.NetworkProtocolTCP}}}}
232
233	manager := trafficadapter.NewManager()
234	defer func() { require.NoError(t, manager.Shutdown(context.Background())) }()
235	pkiSvc := newTestPKIService(t)
236
237	httpSrv, httpReady, tlsSrv, tlsReady, err := startProxyServers(
238		cfg,
239		http.NotFoundHandler(),
240		http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("custom secure")) }),
241		pkiSvc,
242		nil,
243		manager,
244		zerowrap.Default(),
245	)
246	require.NoError(t, err)
247	require.Nil(t, httpSrv)
248	require.Nil(t, httpReady)
249	require.Nil(t, tlsSrv)
250	require.Nil(t, tlsReady)
251
252	configSvc := inmocks.NewMockConfigService(t)
253	configSvc.EXPECT().GetExternalRoutes().Return(nil)
254	require.NoError(t, applyTrafficRuntimeConfig(context.Background(), manager, cfg, configSvc, stubHosts()))
255
256	_, customPort, err := net.SplitHostPort(cfg.EntryPoints["custom-secure"].Address)
257	require.NoError(t, err)
258	port, err := strconv.Atoi(customPort)
259	require.NoError(t, err)
260	body := httpsGetForTest(t, port, "app.example.com")
261	require.Contains(t, body, "custom secure")
262}
263
264func TestStartProxyServers_TLSSetupFailureStopsHTTPServer(t *testing.T) {
265	cfg := Config{}
266	cfg.EntryPoints = map[string]traffic.EntryPointConfig{
267		traffic.DefaultEdgeEntryPointName: {Address: freeTCPAddress(t), Protocol: domain.EntryPointProtocolSmartTCP},
268	}
269	cfg.Server.TLSCertFile = "/path/that/does/not/exist.crt"
270	cfg.Server.TLSKeyFile = "/path/that/does/not/exist.key"
271
272	httpSrv, httpReady, tlsSrv, tlsReady, err := startProxyServers(
273		cfg,
274		http.NotFoundHandler(),
275		http.NotFoundHandler(),
276		newTestPKIService(t),
277		nil,
278		trafficadapter.NewManager(),
279		zerowrap.Default(),
280	)
281	require.ErrorContains(t, err, "load TLS keypair")
282	require.Nil(t, tlsSrv)
283	require.Nil(t, tlsReady)
284	if httpReady != nil {
285		select {
286		case <-httpReady:
287		case <-time.After(time.Second):
288		}
289	}
290	if httpSrv != nil {
291		_, err = net.DialTimeout("tcp", httpSrv.Addr, 100*time.Millisecond)
292		require.Error(t, err)
293	}
294}
295
296func TestStartProxyServers_TLSRequiresTrafficManager(t *testing.T) {
297	cfg := Config{}
298	cfg.EntryPoints = map[string]traffic.EntryPointConfig{
299		traffic.DefaultEdgeEntryPointName: {Address: freeTCPAddress(t), Protocol: domain.EntryPointProtocolSmartTCP},
300	}
301
302	httpSrv, _, tlsSrv, _, err := startProxyServers(
303		cfg,
304		http.NotFoundHandler(),
305		http.NotFoundHandler(),
306		newTestPKIService(t),
307		nil,
308		nil,
309		zerowrap.Default(),
310	)
311	if httpSrv != nil {
312		shutdownHTTPServer(t, httpSrv)
313	}
314	if tlsSrv != nil {
315		shutdownHTTPServer(t, tlsSrv)
316	}
317	require.ErrorContains(t, err, "traffic manager is required")
318}
319
320func newTestPKIService(t *testing.T) *pkiusecase.Service {
321	t.Helper()
322	routes := newRoutesMock(t, "app.example.com")
323	ca, err := pkiadapter.NewCA(t.TempDir(), zerowrap.Default())
324	require.NoError(t, err)
325	pkiSvc := pkiusecase.NewService(context.Background(), ca, routes, nil, zerowrap.Default())
326	t.Cleanup(pkiSvc.Stop)
327	return pkiSvc
328}
329
330func freeTCPPort(t *testing.T) int {
331	t.Helper()
332	listener, err := net.Listen("tcp", "127.0.0.1:0")
333	require.NoError(t, err)
334	defer listener.Close()
335	_, portValue, err := net.SplitHostPort(listener.Addr().String())
336	require.NoError(t, err)
337	port, err := strconv.Atoi(portValue)
338	require.NoError(t, err)
339	return port
340}
341
342func shutdownHTTPServer(t *testing.T, srv *http.Server) {
343	t.Helper()
344	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
345	defer cancel()
346	require.NoError(t, srv.Shutdown(ctx))
347}
348
349func httpGetForTest(t *testing.T, port int, host string) string {
350	t.Helper()
351	client := &http.Client{Timeout: 2 * time.Second}
352	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://127.0.0.1:%d/", port), nil)
353	require.NoError(t, err)
354	req.Host = host
355	resp, err := client.Do(req)
356	require.NoError(t, err)
357	defer resp.Body.Close()
358	body, err := io.ReadAll(resp.Body)
359	require.NoError(t, err)
360	return string(body)
361}
362
363func assertTCPPortClosed(t *testing.T, address string) {
364	t.Helper()
365	conn, err := net.DialTimeout("tcp", address, 100*time.Millisecond)
366	if err == nil {
367		_ = conn.Close()
368		t.Fatalf("expected %s to be closed", address)
369	}
370}
371
372func httpsGetForTest(t *testing.T, port int, serverName string) string {
373	t.Helper()
374	transport := &http.Transport{
375		TLSClientConfig:   &tls.Config{ServerName: serverName, InsecureSkipVerify: true},
376		ForceAttemptHTTP2: true,
377	}
378	client := &http.Client{Transport: transport, Timeout: 2 * time.Second}
379	defer transport.CloseIdleConnections()
380
381	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://127.0.0.1:%d/", port), nil)
382	require.NoError(t, err)
383	req.Host = serverName
384	resp, err := client.Do(req)
385	require.NoError(t, err)
386	defer resp.Body.Close()
387	body, err := io.ReadAll(resp.Body)
388	require.NoError(t, err)
389	return string(body)
390}

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