v2.31.0-dev22 8d342c4

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	"strconv"
12	"testing"
13	"time"
14
15	"github.com/bnema/zerowrap"
16	"github.com/stretchr/testify/mock"
17	"github.com/stretchr/testify/require"
18
19	trafficadapter "github.com/bnema/gordon/internal/adapters/in/traffic"
20	pkiadapter "github.com/bnema/gordon/internal/adapters/out/pki"
21	inmocks "github.com/bnema/gordon/internal/boundaries/in/mocks"
22	outmocks "github.com/bnema/gordon/internal/boundaries/out/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(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(make(chan struct{}), errChan)
50	require.ErrorIs(t, err, expected)
51}
52
53func TestStartProxyServers_DoesNotStartLegacyHTTPListenerFromServerPort(t *testing.T) {
54	cfg := Config{}
55	cfg.Server.Port = freeTCPPort(t)
56
57	manager := trafficadapter.NewManager()
58	defer func() { require.NoError(t, manager.Shutdown(context.Background())) }()
59
60	httpSrv, httpReady, tlsSrv, tlsReady, err := startProxyServers(
61		cfg,
62		http.NotFoundHandler(),
63		http.NotFoundHandler(),
64		newTestPKIService(t),
65		nil,
66		manager,
67		zerowrap.Default(),
68	)
69	require.NoError(t, err)
70	require.Nil(t, httpSrv)
71	require.Nil(t, httpReady)
72	require.Nil(t, tlsSrv)
73	require.Nil(t, tlsReady)
74	assertTCPPortClosed(t, fmt.Sprintf("127.0.0.1:%d", cfg.Server.Port))
75}
76
77func TestStartProxyServers_DoesNotTreatLegacyTLSPortAsTLSCapable(t *testing.T) {
78	cfg := Config{}
79	cfg.Server.TLSPort = freeTCPPort(t)
80
81	manager := trafficadapter.NewManager()
82	defer func() { require.NoError(t, manager.Shutdown(context.Background())) }()
83
84	httpSrv, httpReady, tlsSrv, tlsReady, err := startProxyServers(
85		cfg,
86		http.NotFoundHandler(),
87		http.NotFoundHandler(),
88		newTestPKIService(t),
89		nil,
90		manager,
91		zerowrap.Default(),
92	)
93	require.NoError(t, err)
94	require.Nil(t, httpSrv)
95	require.Nil(t, httpReady)
96	require.Nil(t, tlsSrv)
97	require.Nil(t, tlsReady)
98	assertTCPPortClosed(t, fmt.Sprintf("127.0.0.1:%d", cfg.Server.TLSPort))
99}
100
101func TestStartProxyServers_ConfiguresSmartTCPHTTPAndHTTPSRoutesWithoutPublicHTTPListener(t *testing.T) {
102	cfg := Config{}
103	cfg.Server.Port = freeTCPPort(t)
104	cfg.EntryPoints = map[string]traffic.EntryPointConfig{
105		traffic.DefaultEdgeEntryPointName: {
106			Address:  freeTCPAddress(t),
107			Protocol: domain.EntryPointProtocolSmartTCP,
108		},
109	}
110
111	manager := trafficadapter.NewManager()
112	defer func() { require.NoError(t, manager.Shutdown(context.Background())) }()
113	pkiSvc := newTestPKIService(t)
114
115	httpSrv, httpReady, tlsSrv, tlsReady, err := startProxyServers(
116		cfg,
117		http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { _, _ = w.Write([]byte("cleartext " + r.Proto)) }),
118		http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("secure")) }),
119		pkiSvc,
120		nil,
121		manager,
122		zerowrap.Default(),
123	)
124	require.NoError(t, err)
125	require.Nil(t, httpSrv)
126	require.Nil(t, httpReady)
127	require.Nil(t, tlsSrv)
128	require.Nil(t, tlsReady)
129	assertTCPPortClosed(t, fmt.Sprintf("127.0.0.1:%d", cfg.Server.Port))
130
131	configSvc := inmocks.NewMockConfigService(t)
132	configSvc.EXPECT().GetRoutes(context.Background()).Return([]domain.Route{{Domain: "app.example.com", HTTPS: true}})
133	configSvc.EXPECT().GetExternalRoutes().Return(map[string]string{})
134	require.NoError(t, applyTrafficRuntimeConfig(context.Background(), manager, cfg, configSvc))
135
136	_, smartPortText, err := net.SplitHostPort(cfg.EntryPoints[traffic.DefaultEdgeEntryPointName].Address)
137	require.NoError(t, err)
138	smartPort, err := strconv.Atoi(smartPortText)
139	require.NoError(t, err)
140	require.Contains(t, httpGetForTest(t, smartPort, "app.example.com"), "cleartext HTTP/1.1")
141	require.Contains(t, httpsGetForTest(t, smartPort, "app.example.com"), "secure")
142}
143
144func TestStartProxyServers_ConfiguresTrafficManagerHTTPSRoute(t *testing.T) {
145	cfg := Config{}
146	cfg.EntryPoints = map[string]traffic.EntryPointConfig{
147		traffic.DefaultEdgeEntryPointName: {
148			Address:  freeTCPAddress(t),
149			Protocol: domain.EntryPointProtocolTLSMux,
150		},
151	}
152
153	manager := trafficadapter.NewManager()
154	defer func() { require.NoError(t, manager.Shutdown(context.Background())) }()
155	pkiSvc := newTestPKIService(t)
156
157	httpSrv, httpReady, tlsSrv, tlsReady, err := startProxyServers(
158		cfg,
159		http.NotFoundHandler(),
160		http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("secure")) }),
161		pkiSvc,
162		nil,
163		manager,
164		zerowrap.Default(),
165	)
166	require.NoError(t, err)
167	require.Nil(t, httpSrv)
168	require.Nil(t, httpReady)
169	require.Nil(t, tlsSrv)
170	require.Nil(t, tlsReady)
171
172	configSvc := inmocks.NewMockConfigService(t)
173	configSvc.EXPECT().GetRoutes(context.Background()).Return([]domain.Route{{Domain: "app.example.com", HTTPS: true}})
174	configSvc.EXPECT().GetExternalRoutes().Return(map[string]string{})
175	require.NoError(t, applyTrafficRuntimeConfig(context.Background(), manager, cfg, configSvc))
176
177	_, portText, err := net.SplitHostPort(cfg.EntryPoints[traffic.DefaultEdgeEntryPointName].Address)
178	require.NoError(t, err)
179	port, err := strconv.Atoi(portText)
180	require.NoError(t, err)
181	body := httpsGetForTest(t, port, "app.example.com")
182	require.Contains(t, body, "secure")
183}
184
185func TestStartProxyServers_ConfiguresCustomTLSMuxHTTPSRoute(t *testing.T) {
186	cfg := Config{}
187	cfg.Server.Port = freeTCPPort(t)
188	cfg.Server.TLSPort = freeTCPPort(t)
189	cfg.EntryPoints = map[string]traffic.EntryPointConfig{
190		"custom-secure": {Address: fmt.Sprintf("127.0.0.1:%d", freeTCPPort(t)), Protocol: domain.EntryPointProtocolTLSMux},
191	}
192	cfg.Traffic.TLS.Routers = []traffic.RouterConfig{{Name: "raw", EntryPoint: "custom-secure", SNI: "raw.example.com", Service: "network_service:raw:tls"}}
193	cfg.NetworkServices = []traffic.NetworkServiceConfig{{Name: "raw", Ports: []traffic.PortConfig{{Name: "tls", Container: 443, Protocol: domain.NetworkProtocolTCP}}}}
194
195	manager := trafficadapter.NewManager()
196	defer func() { require.NoError(t, manager.Shutdown(context.Background())) }()
197	pkiSvc := newTestPKIService(t)
198
199	httpSrv, httpReady, tlsSrv, tlsReady, err := startProxyServers(
200		cfg,
201		http.NotFoundHandler(),
202		http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { _, _ = w.Write([]byte("custom secure")) }),
203		pkiSvc,
204		nil,
205		manager,
206		zerowrap.Default(),
207	)
208	require.NoError(t, err)
209	require.Nil(t, httpSrv)
210	require.Nil(t, httpReady)
211	require.Nil(t, tlsSrv)
212	require.Nil(t, tlsReady)
213
214	configSvc := inmocks.NewMockConfigService(t)
215	configSvc.EXPECT().GetRoutes(context.Background()).Return(nil)
216	configSvc.EXPECT().GetExternalRoutes().Return(nil)
217	require.NoError(t, applyTrafficRuntimeConfig(context.Background(), manager, cfg, configSvc))
218
219	_, customPort, err := net.SplitHostPort(cfg.EntryPoints["custom-secure"].Address)
220	require.NoError(t, err)
221	port, err := strconv.Atoi(customPort)
222	require.NoError(t, err)
223	body := httpsGetForTest(t, port, "app.example.com")
224	require.Contains(t, body, "custom secure")
225}
226
227func TestStartProxyServers_TLSSetupFailureStopsHTTPServer(t *testing.T) {
228	cfg := Config{}
229	cfg.EntryPoints = map[string]traffic.EntryPointConfig{
230		traffic.DefaultEdgeEntryPointName: {Address: freeTCPAddress(t), Protocol: domain.EntryPointProtocolSmartTCP},
231	}
232	cfg.Server.TLSCertFile = "/path/that/does/not/exist.crt"
233	cfg.Server.TLSKeyFile = "/path/that/does/not/exist.key"
234
235	httpSrv, httpReady, tlsSrv, tlsReady, err := startProxyServers(
236		cfg,
237		http.NotFoundHandler(),
238		http.NotFoundHandler(),
239		newTestPKIService(t),
240		nil,
241		trafficadapter.NewManager(),
242		zerowrap.Default(),
243	)
244	require.ErrorContains(t, err, "load TLS keypair")
245	require.Nil(t, tlsSrv)
246	require.Nil(t, tlsReady)
247	if httpReady != nil {
248		select {
249		case <-httpReady:
250		case <-time.After(time.Second):
251		}
252	}
253	if httpSrv != nil {
254		_, err = net.DialTimeout("tcp", httpSrv.Addr, 100*time.Millisecond)
255		require.Error(t, err)
256	}
257}
258
259func TestStartProxyServers_TLSRequiresTrafficManager(t *testing.T) {
260	cfg := Config{}
261	cfg.EntryPoints = map[string]traffic.EntryPointConfig{
262		traffic.DefaultEdgeEntryPointName: {Address: freeTCPAddress(t), Protocol: domain.EntryPointProtocolSmartTCP},
263	}
264
265	httpSrv, _, tlsSrv, _, err := startProxyServers(
266		cfg,
267		http.NotFoundHandler(),
268		http.NotFoundHandler(),
269		newTestPKIService(t),
270		nil,
271		nil,
272		zerowrap.Default(),
273	)
274	if httpSrv != nil {
275		shutdownHTTPServer(t, httpSrv)
276	}
277	if tlsSrv != nil {
278		shutdownHTTPServer(t, tlsSrv)
279	}
280	require.ErrorContains(t, err, "traffic manager is required")
281}
282
283func newTestPKIService(t *testing.T) *pkiusecase.Service {
284	t.Helper()
285	routes := outmocks.NewMockRouteChecker(t)
286	routes.EXPECT().GetRoutes(mock.Anything).Return([]domain.Route{{Domain: "app.example.com", HTTPS: true}}).Maybe()
287	routes.EXPECT().GetExternalRoutes().Return(map[string]string{}).Maybe()
288	ca, err := pkiadapter.NewCA(t.TempDir(), zerowrap.Default())
289	require.NoError(t, err)
290	pkiSvc := pkiusecase.NewService(context.Background(), ca, routes, zerowrap.Default())
291	t.Cleanup(pkiSvc.Stop)
292	return pkiSvc
293}
294
295func freeTCPPort(t *testing.T) int {
296	t.Helper()
297	listener, err := net.Listen("tcp", "127.0.0.1:0")
298	require.NoError(t, err)
299	defer listener.Close()
300	_, portValue, err := net.SplitHostPort(listener.Addr().String())
301	require.NoError(t, err)
302	port, err := strconv.Atoi(portValue)
303	require.NoError(t, err)
304	return port
305}
306
307func shutdownHTTPServer(t *testing.T, srv *http.Server) {
308	t.Helper()
309	ctx, cancel := context.WithTimeout(context.Background(), time.Second)
310	defer cancel()
311	require.NoError(t, srv.Shutdown(ctx))
312}
313
314func httpGetForTest(t *testing.T, port int, host string) string {
315	t.Helper()
316	client := &http.Client{Timeout: 2 * time.Second}
317	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("http://127.0.0.1:%d/", port), nil)
318	require.NoError(t, err)
319	req.Host = host
320	resp, err := client.Do(req)
321	require.NoError(t, err)
322	defer resp.Body.Close()
323	body, err := io.ReadAll(resp.Body)
324	require.NoError(t, err)
325	return string(body)
326}
327
328func assertTCPPortClosed(t *testing.T, address string) {
329	t.Helper()
330	conn, err := net.DialTimeout("tcp", address, 100*time.Millisecond)
331	if err == nil {
332		_ = conn.Close()
333		t.Fatalf("expected %s to be closed", address)
334	}
335}
336
337func httpsGetForTest(t *testing.T, port int, serverName string) string {
338	t.Helper()
339	transport := &http.Transport{
340		TLSClientConfig:   &tls.Config{ServerName: serverName, InsecureSkipVerify: true},
341		ForceAttemptHTTP2: true,
342	}
343	client := &http.Client{Transport: transport, Timeout: 2 * time.Second}
344	defer transport.CloseIdleConnections()
345
346	req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("https://127.0.0.1:%d/", port), nil)
347	require.NoError(t, err)
348	req.Host = serverName
349	resp, err := client.Do(req)
350	require.NoError(t, err)
351	defer resp.Body.Close()
352	body, err := io.ReadAll(resp.Body)
353	require.NoError(t, err)
354	return string(body)
355}

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