@marcel91200 / caddy Mirror · read-only
Fork 0

Fast and extensible multi-platform HTTP/1-2-3 web server with automatic HTTPS

Mirrored from https://github.com/Slazee/caddy.git

Synced Nov 11, 01:00 UTC

master
go 418 lines 14.3 KB
Raw
1 package reverseproxy
2
3 import (
4 "context"
5 "encoding/json"
6 "errors"
7 "fmt"
8 "io"
9 "net"
10 "net/netip"
11 "net/url"
12 "reflect"
13 "testing"
14 "time"
15
16 "github.com/caddyserver/caddy/v2"
17 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
18 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
19 )
20
21 func TestHTTPTransportUnmarshalCaddyFileWithCaPools(t *testing.T) {
22 const test_der_1 = `MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==`
23 type args struct {
24 d *caddyfile.Dispenser
25 }
26 tests := []struct {
27 name string
28 args args
29 expectedTLSConfig TLSConfig
30 wantErr bool
31 }{
32 {
33 name: "tls_trust_pool without a module argument returns an error",
34 args: args{
35 d: caddyfile.NewTestDispenser(
36 `http {
37 tls_trust_pool
38 }`),
39 },
40 wantErr: true,
41 },
42 {
43 name: "providing both 'tls_trust_pool' and 'tls_trusted_ca_certs' returns an error",
44 args: args{
45 d: caddyfile.NewTestDispenser(fmt.Sprintf(
46 `http {
47 tls_trust_pool inline %s
48 tls_trusted_ca_certs %s
49 }`, test_der_1, test_der_1)),
50 },
51 wantErr: true,
52 },
53 {
54 name: "setting 'tls_trust_pool' and 'tls_trusted_ca_certs' produces an error",
55 args: args{
56 d: caddyfile.NewTestDispenser(fmt.Sprintf(
57 `http {
58 tls_trust_pool inline {
59 trust_der %s
60 }
61 tls_trusted_ca_certs %s
62 }`, test_der_1, test_der_1)),
63 },
64 wantErr: true,
65 },
66 {
67 name: "using 'inline' tls_trust_pool loads the module successfully",
68 args: args{
69 d: caddyfile.NewTestDispenser(fmt.Sprintf(
70 `http {
71 tls_trust_pool inline {
72 trust_der %s
73 }
74 }
75 `, test_der_1)),
76 },
77 expectedTLSConfig: TLSConfig{CARaw: json.RawMessage(fmt.Sprintf(`{"provider":"inline","trusted_ca_certs":["%s"]}`, test_der_1))},
78 },
79 {
80 name: "setting 'tls_trusted_ca_certs' and 'tls_trust_pool' produces an error",
81 args: args{
82 d: caddyfile.NewTestDispenser(fmt.Sprintf(
83 `http {
84 tls_trusted_ca_certs %s
85 tls_trust_pool inline {
86 trust_der %s
87 }
88 }`, test_der_1, test_der_1)),
89 },
90 wantErr: true,
91 },
92 }
93 for _, tt := range tests {
94 t.Run(tt.name, func(t *testing.T) {
95 ht := &HTTPTransport{}
96 if err := ht.UnmarshalCaddyfile(tt.args.d); (err != nil) != tt.wantErr {
97 t.Errorf("HTTPTransport.UnmarshalCaddyfile() error = %v, wantErr %v", err, tt.wantErr)
98 return
99 }
100 if !tt.wantErr && !reflect.DeepEqual(&tt.expectedTLSConfig, ht.TLS) {
101 t.Errorf("HTTPTransport.UnmarshalCaddyfile() = %v, want %v", ht, tt.expectedTLSConfig)
102 }
103 })
104 }
105 }
106
107 func TestHTTPTransport_RequestHeaderOps_TLS(t *testing.T) {
108 var ht HTTPTransport
109 // When TLS is nil, expect no header ops
110 if ops := ht.RequestHeaderOps(); ops != nil {
111 t.Fatalf("expected nil HeaderOps when TLS is nil, got: %#v", ops)
112 }
113
114 // When TLS is configured, expect a HeaderOps that sets Host
115 ht.TLS = &TLSConfig{}
116 ops := ht.RequestHeaderOps()
117 if ops == nil {
118 t.Fatal("expected non-nil HeaderOps when TLS is set")
119 }
120 if ops.Set == nil {
121 t.Fatalf("expected ops.Set to be non-nil, got nil")
122 }
123 if got := ops.Set.Get("Host"); got != "{http.reverse_proxy.upstream.hostport}" {
124 t.Fatalf("unexpected Host value; want placeholder, got: %s", got)
125 }
126 }
127
128 // TestHTTPTransport_DialTLSContext_ProxyProtocol verifies that when TLS and
129 // ProxyProtocol are both enabled, DialTLSContext is set. This is critical because
130 // ProxyProtocol modifies req.URL.Host to include client info with "->" separator
131 // (e.g., "[2001:db8::1]:12345->127.0.0.1:443"), which breaks Go's address parsing.
132 // Without a custom DialTLSContext, Go's HTTP library would fail with
133 // "too many colons in address" when trying to parse the mangled host.
134 func TestHTTPTransport_DialTLSContext_ProxyProtocol(t *testing.T) {
135 ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
136 defer cancel()
137
138 tests := []struct {
139 name string
140 tls *TLSConfig
141 proxyProtocol string
142 serverNameHasPlaceholder bool
143 expectDialTLSContext bool
144 }{
145 {
146 name: "no TLS, no proxy protocol",
147 tls: nil,
148 proxyProtocol: "",
149 expectDialTLSContext: false,
150 },
151 {
152 name: "TLS without proxy protocol",
153 tls: &TLSConfig{},
154 proxyProtocol: "",
155 expectDialTLSContext: false,
156 },
157 {
158 name: "TLS with proxy protocol v1",
159 tls: &TLSConfig{},
160 proxyProtocol: "v1",
161 expectDialTLSContext: true,
162 },
163 {
164 name: "TLS with proxy protocol v2",
165 tls: &TLSConfig{},
166 proxyProtocol: "v2",
167 expectDialTLSContext: true,
168 },
169 {
170 name: "TLS with placeholder ServerName",
171 tls: &TLSConfig{ServerName: "{http.request.host}"},
172 proxyProtocol: "",
173 serverNameHasPlaceholder: true,
174 expectDialTLSContext: true,
175 },
176 {
177 name: "TLS with placeholder ServerName and proxy protocol",
178 tls: &TLSConfig{ServerName: "{http.request.host}"},
179 proxyProtocol: "v2",
180 serverNameHasPlaceholder: true,
181 expectDialTLSContext: true,
182 },
183 }
184
185 for _, tt := range tests {
186 t.Run(tt.name, func(t *testing.T) {
187 ht := &HTTPTransport{
188 TLS: tt.tls,
189 ProxyProtocol: tt.proxyProtocol,
190 }
191
192 rt, err := ht.NewTransport(ctx)
193 if err != nil {
194 t.Fatalf("NewTransport() error = %v", err)
195 }
196
197 hasDialTLSContext := rt.DialTLSContext != nil
198 if hasDialTLSContext != tt.expectDialTLSContext {
199 t.Errorf("DialTLSContext set = %v, want %v", hasDialTLSContext, tt.expectDialTLSContext)
200 }
201 })
202 }
203 }
204
205 func TestHTTPTransport_DialContext_ProxyProtocolClosesConnectionOnError(t *testing.T) {
206 ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
207 defer cancel()
208
209 validAddrPort := netip.MustParseAddrPort("192.0.2.1:1234")
210 tests := []struct {
211 name string
212 version string
213 proxyInfo *ProxyProtocolInfo
214 }{
215 {
216 name: "missing proxy protocol info",
217 version: "v1",
218 },
219 {
220 name: "unexpected proxy protocol version",
221 version: "v3",
222 proxyInfo: &ProxyProtocolInfo{AddrPort: validAddrPort},
223 },
224 {
225 name: "unexpected remote address type",
226 version: "v1",
227 proxyInfo: &ProxyProtocolInfo{},
228 },
229 }
230
231 for _, tt := range tests {
232 t.Run(tt.name, func(t *testing.T) {
233 listener, err := net.Listen("tcp", "127.0.0.1:0")
234 if err != nil {
235 t.Fatalf("listen: %v", err)
236 }
237 t.Cleanup(func() { listener.Close() })
238
239 readResult := make(chan error, 1)
240 go func() {
241 conn, err := listener.Accept()
242 if err != nil {
243 readResult <- err
244 return
245 }
246 defer conn.Close()
247 _ = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
248 var buf [1]byte
249 _, err = conn.Read(buf[:])
250 readResult <- err
251 }()
252
253 dialCtx := context.WithValue(context.Background(), caddyhttp.VarsCtxKey, make(map[string]any))
254 if tt.proxyInfo != nil {
255 caddyhttp.SetVar(dialCtx, proxyProtocolInfoVarKey, *tt.proxyInfo)
256 }
257
258 rt, err := (&HTTPTransport{ProxyProtocol: tt.version}).NewTransport(ctx)
259 if err != nil {
260 t.Fatalf("NewTransport: %v", err)
261 }
262 conn, err := rt.DialContext(dialCtx, "tcp", listener.Addr().String())
263 if err == nil {
264 conn.Close()
265 t.Fatal("DialContext succeeded, want error")
266 }
267
268 if readErr := <-readResult; !errors.Is(readErr, io.EOF) {
269 t.Fatalf("server read error = %v, want EOF after client connection close", readErr)
270 }
271 })
272 }
273 }
274
275 // TestHTTPTransport_DialContext_DialInfoOverride is a regression test for
276 // issue #6447: a `tcp4/`-prefixed upstream silently fell back to plain `tcp`
277 // because dialContext only honored DialInfo for unix networks. PR #7300 widened
278 // the condition so DialInfo is honored when no upstream HTTP proxy is in use,
279 // and skipped (for non-unix networks) when one is. Both halves are pinned here.
280 func TestHTTPTransport_DialContext_DialInfoOverride(t *testing.T) {
281 ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
282 defer cancel()
283
284 ln, err := net.Listen("tcp4", "127.0.0.1:0")
285 if err != nil {
286 t.Fatalf("listen: %v", err)
287 }
288 t.Cleanup(func() { ln.Close() })
289 go func() {
290 for {
291 c, err := ln.Accept()
292 if err != nil {
293 return
294 }
295 c.Close()
296 }
297 }()
298
299 ht := &HTTPTransport{}
300 rt, err := ht.NewTransport(ctx)
301 if err != nil {
302 t.Fatalf("NewTransport: %v", err)
303 }
304
305 proxyURL, err := url.Parse("http://proxy.example:8080")
306 if err != nil {
307 t.Fatalf("parse proxy URL: %v", err)
308 }
309
310 tests := []struct {
311 name string
312 proxy bool
313 dialInfo string
314 defaultAddr string
315 }{
316 {
317 // no proxy: DialInfo should be applied, so the dial lands on
318 // the live listener despite the bogus default address.
319 name: "honors DialInfo when no proxy",
320 proxy: false,
321 dialInfo: ln.Addr().String(),
322 defaultAddr: "127.0.0.1:1",
323 },
324 {
325 // proxy active: DialInfo must NOT be applied for non-unix
326 // networks; the default address (the live listener) is used.
327 name: "skips DialInfo when proxy active",
328 proxy: true,
329 dialInfo: "127.0.0.1:1",
330 defaultAddr: ln.Addr().String(),
331 },
332 }
333
334 for _, tt := range tests {
335 t.Run(tt.name, func(t *testing.T) {
336 dialCtx := context.WithValue(context.Background(), caddyhttp.VarsCtxKey, make(map[string]any))
337 dialCtx = context.WithValue(dialCtx, dialInfoCtxKey, DialInfo{
338 Network: "tcp4",
339 Address: tt.dialInfo,
340 })
341 if tt.proxy {
342 caddyhttp.SetVar(dialCtx, proxyVarKey, proxyURL)
343 }
344
345 conn, err := rt.DialContext(dialCtx, "tcp", tt.defaultAddr)
346 if err != nil {
347 t.Fatalf("DialContext: %v", err)
348 }
349 t.Cleanup(func() { conn.Close() })
350 if got := conn.RemoteAddr().String(); got != ln.Addr().String() {
351 t.Fatalf("conn.RemoteAddr() = %s, want %s", got, ln.Addr().String())
352 }
353 })
354 }
355 }
356
357 // TestHTTPTransportH3UsesConfiguredTrustPool ensures that when the transport
358 // is configured for HTTP/3, the h3 transport's TLS client config carries the
359 // trust pool configured via the CA module (tls_trust_pool), not nil root CAs
360 // which would fall back to the system trust store.
361 //
362 // Regression test: NewTransport used to call MakeTLSClientConfig twice, once for
363 // the HTTP/1.1+2 transport and once for the HTTP/3 transport. The first call
364 // loads the CA module through ctx.LoadModule, which zeroes the raw module bytes
365 // afterwards, so the second call built a config with RootCAs == nil.
366 func TestHTTPTransportH3UsesConfiguredTrustPool(t *testing.T) {
367 const testDER = `MIIDSzCCAjOgAwIBAgIUfIRObjWNUA4jxQ/0x8BOCvE2Vw4wDQYJKoZIhvcNAQELBQAwFjEUMBIGA1UEAwwLRWFzeS1SU0EgQ0EwHhcNMTkwODI4MTYyNTU5WhcNMjkwODI1MTYyNTU5WjAWMRQwEgYDVQQDDAtFYXN5LVJTQSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAK5m5elxhQfMp/3aVJ4JnpN9PUSz6LlP6LePAPFU7gqohVVFVtDkChJAG3FNkNQNlieVTja/bgH9IcC6oKbROwdY1h0MvNV8AHHigvl03WuJD8g2ReVFXXwsnrPmKXCFzQyMI6TYk3m2gYrXsZOU1GLnfMRC3KAMRgE2F45twOs9hqG169YJ6mM2eQjzjCHWI6S2/iUYvYxRkCOlYUbLsMD/AhgAf1plzg6LPqNxtdlwxZnA0ytgkmhK67HtzJu0+ovUCsMv0RwcMhsEo9T8nyFAGt9XLZ63X5WpBCTUApaAUhnG0XnerjmUWb6eUWw4zev54sEfY5F3x002iQaW6cECAwEAAaOBkDCBjTAdBgNVHQ4EFgQU4CBUbZsS2GaNIkGRz/cBsD5ivjswUQYDVR0jBEowSIAU4CBUbZsS2GaNIkGRz/cBsD5ivjuhGqQYMBYxFDASBgNVBAMMC0Vhc3ktUlNBIENBghR8hE5uNY1QDiPFD/THwE4K8TZXDjAMBgNVHRMEBTADAQH/MAsGA1UdDwQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAQEAKB3V4HIzoiO/Ch6WMj9bLJ2FGbpkMrcb/Eq01hT5zcfKD66lVS1MlK+cRL446Z2b2KDP1oFyVs+qmrmtdwrWgD+nfe2sBmmIHo9m9KygMkEOfG3MghGTEcS+0cTKEcoHYWYyOqQh6jnedXY8Cdm4GM1hAc9MiL3/sqV8YCVSLNnkoNysmr06/rZ0MCUZPGUtRmfd0heWhrfzAKw2HLgX+RAmpOE2MZqWcjvqKGyaRiaZks4nJkP6521aC2Lgp0HhCz1j8/uQ5ldoDszCnu/iro0NAsNtudTMD+YoLQxLqdleIh6CW+illc2VdXwj7mn6J04yns9jfE2jRjW/yTLFuQ==`
368
369 caddyCtx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
370 defer cancel()
371
372 h := &HTTPTransport{
373 Versions: []string{"3"},
374 TLS: &TLSConfig{
375 ServerName: "backend",
376 CARaw: json.RawMessage(fmt.Sprintf(`{"provider":"inline","trusted_ca_certs":[%q]}`, testDER)),
377 },
378 }
379
380 rt, err := h.NewTransport(caddyCtx)
381 if err != nil {
382 t.Fatalf("NewTransport() error = %v", err)
383 }
384 if rt.TLSClientConfig == nil || rt.TLSClientConfig.RootCAs == nil {
385 t.Fatal("HTTP/1.1+2 transport: expected RootCAs to be set from the configured trust pool")
386 }
387 if h.h3Transport == nil {
388 t.Fatal("expected h3Transport to be configured when versions is [\"3\"]")
389 }
390 h3cfg := h.h3Transport.TLSClientConfig
391 if h3cfg == nil {
392 t.Fatal("HTTP/3 transport: expected non-nil TLS client config")
393 }
394 if h3cfg.RootCAs == nil {
395 t.Fatal("HTTP/3 transport: RootCAs is nil; the configured trust pool was lost and the system roots would be used instead")
396 }
397 if !h3cfg.RootCAs.Equal(rt.TLSClientConfig.RootCAs) {
398 t.Fatal("HTTP/3 transport: RootCAs differs from the HTTP/1.1+2 transport's RootCAs")
399 }
400 if h3cfg.ServerName != "backend" {
401 t.Errorf("HTTP/3 transport: ServerName = %q, want %q", h3cfg.ServerName, "backend")
402 }
403 // the h3 transport must own its own copy of the config, since both
404 // transports mutate their TLS config independently
405 if h3cfg == rt.TLSClientConfig {
406 t.Fatal("HTTP/3 transport: TLS client config must be a clone, not the same pointer as the HTTP/1.1+2 transport's")
407 }
408 }
409
410 func TestTLSConfigRejectsUnknownCurve(t *testing.T) {
411 ctx, cancel := caddy.NewContext(caddy.Context{Context: context.Background()})
412 defer cancel()
413
414 tc := &TLSConfig{Curves: []string{"X25519"}}
415 if _, err := tc.MakeTLSClientConfig(ctx); err == nil {
416 t.Fatal("expected an error for an unknown curve, got nil")
417 }
418 }