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}