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, nil, 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}