@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 1,290 lines 30.8 KB
Raw
1 package integration
2
3 import (
4 "bufio"
5 "bytes"
6 "crypto/rand"
7 "crypto/sha1"
8 "encoding/base64"
9 "errors"
10 "fmt"
11 "io"
12 "net"
13 "net/http"
14 "net/http/httptest"
15 "net/textproto"
16 "runtime"
17 "strings"
18 "sync/atomic"
19 "testing"
20 "time"
21
22 "github.com/caddyserver/caddy/v2/caddytest"
23 )
24
25 func TestSRVReverseProxy(t *testing.T) {
26 tester := caddytest.NewTester(t)
27 tester.InitServer(`
28 {
29 "admin": {
30 "listen": "localhost:2999"
31 },
32 "apps": {
33 "pki": {
34 "certificate_authorities": {
35 "local": {
36 "install_trust": false
37 }
38 }
39 },
40 "http": {
41 "grace_period": 1,
42 "servers": {
43 "srv0": {
44 "listen": [
45 ":18080"
46 ],
47 "routes": [
48 {
49 "handle": [
50 {
51 "handler": "reverse_proxy",
52 "dynamic_upstreams": {
53 "source": "srv",
54 "name": "srv.host.service.consul"
55 }
56 }
57 ]
58 }
59 ]
60 }
61 }
62 }
63 }
64 }
65 `, "json")
66 }
67
68 func TestDialWithPlaceholderUnix(t *testing.T) {
69 if runtime.GOOS == "windows" {
70 t.SkipNow()
71 }
72
73 socketName := tempSocketName(t)
74
75 server := http.Server{
76 Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
77 w.Write([]byte("Hello, World!"))
78 }),
79 }
80
81 unixListener, err := net.Listen("unix", socketName)
82 if err != nil {
83 t.Errorf("failed to listen on the socket: %s", err)
84 return
85 }
86 go server.Serve(unixListener)
87 t.Cleanup(func() {
88 server.Close()
89 })
90 runtime.Gosched() // Allow other goroutines to run
91
92 tester := caddytest.NewTester(t)
93 tester.InitServer(`
94 {
95 "admin": {
96 "listen": "localhost:2999"
97 },
98 "apps": {
99 "pki": {
100 "certificate_authorities": {
101 "local": {
102 "install_trust": false
103 }
104 }
105 },
106 "http": {
107 "grace_period": 1,
108 "servers": {
109 "srv0": {
110 "listen": [
111 ":18080"
112 ],
113 "routes": [
114 {
115 "handle": [
116 {
117 "handler": "reverse_proxy",
118 "upstreams": [
119 {
120 "dial": "unix/{http.request.header.X-Caddy-Upstream-Dial}"
121 }
122 ]
123 }
124 ]
125 }
126 ]
127 }
128 }
129 }
130 }
131 }
132 `, "json")
133
134 req, err := http.NewRequest(http.MethodGet, "http://localhost:18080", nil)
135 if err != nil {
136 t.Fail()
137 return
138 }
139 req.Header.Set("X-Caddy-Upstream-Dial", socketName)
140 tester.AssertResponse(req, 200, "Hello, World!")
141 }
142
143 func TestReverseProxyWithPlaceholderDialAddress(t *testing.T) {
144 tester := caddytest.NewTester(t)
145 tester.InitServer(`
146 {
147 "admin": {
148 "listen": "localhost:2999"
149 },
150 "apps": {
151 "pki": {
152 "certificate_authorities": {
153 "local": {
154 "install_trust": false
155 }
156 }
157 },
158 "http": {
159 "grace_period": 1,
160 "servers": {
161 "srv0": {
162 "listen": [
163 ":18080"
164 ],
165 "routes": [
166 {
167 "match": [
168 {
169 "host": [
170 "localhost"
171 ]
172 }
173 ],
174 "handle": [
175 {
176 "handler": "static_response",
177 "body": "Hello, World!"
178 }
179 ],
180 "terminal": true
181 }
182 ],
183 "automatic_https": {
184 "skip": [
185 "localhost"
186 ]
187 }
188 },
189 "srv1": {
190 "listen": [
191 ":9080"
192 ],
193 "routes": [
194 {
195 "match": [
196 {
197 "host": [
198 "localhost"
199 ]
200 }
201 ],
202 "handle": [
203 {
204
205 "handler": "reverse_proxy",
206 "upstreams": [
207 {
208 "dial": "{http.request.header.X-Caddy-Upstream-Dial}"
209 }
210 ]
211 }
212 ],
213 "terminal": true
214 }
215 ],
216 "automatic_https": {
217 "skip": [
218 "localhost"
219 ]
220 }
221 }
222 }
223 }
224 }
225 }
226 `, "json")
227
228 req, err := http.NewRequest(http.MethodGet, "http://localhost:9080", nil)
229 if err != nil {
230 t.Fail()
231 return
232 }
233 req.Header.Set("X-Caddy-Upstream-Dial", "localhost:18080")
234 tester.AssertResponse(req, 200, "Hello, World!")
235 }
236
237 func TestReverseProxyWithPlaceholderTCPDialAddress(t *testing.T) {
238 tester := caddytest.NewTester(t)
239 tester.InitServer(`
240 {
241 "admin": {
242 "listen": "localhost:2999"
243 },
244 "apps": {
245 "pki": {
246 "certificate_authorities": {
247 "local": {
248 "install_trust": false
249 }
250 }
251 },
252 "http": {
253 "grace_period": 1,
254 "servers": {
255 "srv0": {
256 "listen": [
257 ":18080"
258 ],
259 "routes": [
260 {
261 "match": [
262 {
263 "host": [
264 "localhost"
265 ]
266 }
267 ],
268 "handle": [
269 {
270 "handler": "static_response",
271 "body": "Hello, World!"
272 }
273 ],
274 "terminal": true
275 }
276 ],
277 "automatic_https": {
278 "skip": [
279 "localhost"
280 ]
281 }
282 },
283 "srv1": {
284 "listen": [
285 ":9080"
286 ],
287 "routes": [
288 {
289 "match": [
290 {
291 "host": [
292 "localhost"
293 ]
294 }
295 ],
296 "handle": [
297 {
298
299 "handler": "reverse_proxy",
300 "upstreams": [
301 {
302 "dial": "tcp/{http.request.header.X-Caddy-Upstream-Dial}:18080"
303 }
304 ]
305 }
306 ],
307 "terminal": true
308 }
309 ],
310 "automatic_https": {
311 "skip": [
312 "localhost"
313 ]
314 }
315 }
316 }
317 }
318 }
319 }
320 `, "json")
321
322 req, err := http.NewRequest(http.MethodGet, "http://localhost:9080", nil)
323 if err != nil {
324 t.Fail()
325 return
326 }
327 req.Header.Set("X-Caddy-Upstream-Dial", "localhost")
328 tester.AssertResponse(req, 200, "Hello, World!")
329 }
330
331 func TestReverseProxyHealthCheck(t *testing.T) {
332 // Start lightweight backend servers so they're ready before Caddy's
333 // active health checker runs; this avoids a startup race where the
334 // health checker probes backends that haven't yet begun accepting
335 // connections and marks them unhealthy.
336 //
337 // This mirrors how health checks are typically used in practice (to a separate
338 // backend service) and avoids probing the same Caddy instance while it's still
339 // provisioning and not ready to accept connections.
340
341 // backend server that responds to proxied requests
342 helloSrv := &http.Server{
343 Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
344 _, _ = w.Write([]byte("Hello, World!"))
345 }),
346 }
347 ln0, err := net.Listen("tcp", "127.0.0.1:2020")
348 if err != nil {
349 t.Fatalf("failed to listen on 127.0.0.1:2020: %v", err)
350 }
351 go helloSrv.Serve(ln0)
352 t.Cleanup(func() { helloSrv.Close(); ln0.Close() })
353
354 // backend server that serves health checks
355 healthSrv := &http.Server{
356 Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
357 _, _ = w.Write([]byte("ok"))
358 }),
359 }
360 ln1, err := net.Listen("tcp", "127.0.0.1:2021")
361 if err != nil {
362 t.Fatalf("failed to listen on 127.0.0.1:2021: %v", err)
363 }
364 go healthSrv.Serve(ln1)
365 t.Cleanup(func() { healthSrv.Close(); ln1.Close() })
366
367 tester := caddytest.NewTester(t)
368 tester.InitServer(`
369 {
370 skip_install_trust
371 admin localhost:2999
372 http_port 9080
373 https_port 9443
374 grace_period 1ns
375 }
376 http://localhost:9080 {
377 reverse_proxy {
378 to localhost:2020
379
380 health_uri /health
381 health_port 2021
382 health_interval 10ms
383 health_timeout 100ms
384 health_passes 1
385 health_fails 1
386 }
387 }
388 `, "caddyfile")
389 tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
390 }
391
392 // TestReverseProxyHealthCheckPortUsed verifies that health_port is actually
393 // used for active health checks and not the upstream's main port. This is a
394 // regression test for https://github.com/caddyserver/caddy/issues/7524.
395 func TestReverseProxyHealthCheckPortUsed(t *testing.T) {
396 // upstream server: serves proxied requests normally, but returns 503 for
397 // /health so that if health checks mistakenly hit this port the upstream
398 // gets marked unhealthy and the proxy returns 503.
399 upstreamSrv := &http.Server{
400 Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
401 if req.URL.Path == "/health" {
402 w.WriteHeader(http.StatusServiceUnavailable)
403 return
404 }
405 _, _ = w.Write([]byte("Hello, World!"))
406 }),
407 }
408 ln0, err := net.Listen("tcp", "127.0.0.1:2022")
409 if err != nil {
410 t.Fatalf("failed to listen on 127.0.0.1:2022: %v", err)
411 }
412 go upstreamSrv.Serve(ln0)
413 t.Cleanup(func() { upstreamSrv.Close(); ln0.Close() })
414
415 // separate health check server on the configured health_port: returns 200
416 // so the upstream is marked healthy only if health checks go to this port.
417 healthSrv := &http.Server{
418 Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
419 _, _ = w.Write([]byte("ok"))
420 }),
421 }
422 ln1, err := net.Listen("tcp", "127.0.0.1:2023")
423 if err != nil {
424 t.Fatalf("failed to listen on 127.0.0.1:2023: %v", err)
425 }
426 go healthSrv.Serve(ln1)
427 t.Cleanup(func() { healthSrv.Close(); ln1.Close() })
428
429 tester := caddytest.NewTester(t)
430 tester.InitServer(`
431 {
432 skip_install_trust
433 admin localhost:2999
434 http_port 9080
435 https_port 9443
436 grace_period 1ns
437 }
438 http://localhost:9080 {
439 reverse_proxy {
440 to localhost:2022
441
442 health_uri /health
443 health_port 2023
444 health_interval 10ms
445 health_timeout 100ms
446 health_passes 1
447 health_fails 1
448 }
449 }
450 `, "caddyfile")
451 tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
452 }
453
454 func TestReverseProxyHealthCheckUnixSocket(t *testing.T) {
455 if runtime.GOOS == "windows" {
456 t.SkipNow()
457 }
458 tester := caddytest.NewTester(t)
459 socketName := tempSocketName(t)
460
461 server := http.Server{
462 Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
463 if strings.HasPrefix(req.URL.Path, "/health") {
464 w.Write([]byte("ok"))
465 return
466 }
467 w.Write([]byte("Hello, World!"))
468 }),
469 }
470
471 unixListener, err := net.Listen("unix", socketName)
472 if err != nil {
473 t.Errorf("failed to listen on the socket: %s", err)
474 return
475 }
476 go server.Serve(unixListener)
477 t.Cleanup(func() {
478 server.Close()
479 })
480 runtime.Gosched() // Allow other goroutines to run
481
482 tester.InitServer(fmt.Sprintf(`
483 {
484 skip_install_trust
485 admin localhost:2999
486 http_port 9080
487 https_port 9443
488 grace_period 1ns
489 }
490 http://localhost:9080 {
491 reverse_proxy {
492 to unix/%s
493
494 health_uri /health
495 health_port 2021
496 health_interval 2s
497 health_timeout 5s
498 }
499 }
500 `, socketName), "caddyfile")
501
502 tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
503 }
504
505 func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) {
506 if runtime.GOOS == "windows" {
507 t.SkipNow()
508 }
509 tester := caddytest.NewTester(t)
510 socketName := tempSocketName(t)
511
512 server := http.Server{
513 Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
514 if strings.HasPrefix(req.URL.Path, "/health") {
515 w.Write([]byte("ok"))
516 return
517 }
518 w.Write([]byte("Hello, World!"))
519 }),
520 }
521
522 unixListener, err := net.Listen("unix", socketName)
523 if err != nil {
524 t.Errorf("failed to listen on the socket: %s", err)
525 return
526 }
527 go server.Serve(unixListener)
528 t.Cleanup(func() {
529 server.Close()
530 })
531 runtime.Gosched() // Allow other goroutines to run
532
533 tester.InitServer(fmt.Sprintf(`
534 {
535 skip_install_trust
536 admin localhost:2999
537 http_port 9080
538 https_port 9443
539 grace_period 1ns
540 }
541 http://localhost:9080 {
542 reverse_proxy {
543 to unix/%s
544
545 health_uri /health
546 health_interval 2s
547 health_timeout 5s
548 }
549 }
550 `, socketName), "caddyfile")
551
552 tester.AssertGetResponse("http://localhost:9080/", 200, "Hello, World!")
553 }
554
555 // TestReverseProxyRetryMatchStatusCode verifies that lb_retry_match with a
556 // CEL expression matching on {rp.status_code} causes the request to be
557 // retried on the next upstream when the first upstream returns a matching
558 // status code
559 func TestReverseProxyRetryMatchStatusCode(t *testing.T) {
560 // Bad upstream: returns 502
561 badSrv := &http.Server{
562 Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
563 w.WriteHeader(http.StatusBadGateway)
564 }),
565 }
566 badLn, err := net.Listen("tcp", "127.0.0.1:0")
567 if err != nil {
568 t.Fatalf("failed to listen: %v", err)
569 }
570 go badSrv.Serve(badLn)
571 t.Cleanup(func() { badSrv.Close(); badLn.Close() })
572
573 // Good upstream: returns 200
574 goodSrv := &http.Server{
575 Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
576 w.Write([]byte("ok"))
577 }),
578 }
579 goodLn, err := net.Listen("tcp", "127.0.0.1:0")
580 if err != nil {
581 t.Fatalf("failed to listen: %v", err)
582 }
583 go goodSrv.Serve(goodLn)
584 t.Cleanup(func() { goodSrv.Close(); goodLn.Close() })
585
586 tester := caddytest.NewTester(t)
587 tester.InitServer(fmt.Sprintf(`
588 {
589 skip_install_trust
590 admin localhost:2999
591 http_port 9080
592 https_port 9443
593 grace_period 1ns
594 }
595 http://localhost:9080 {
596 reverse_proxy %s %s {
597 lb_policy round_robin
598 lb_retries 1
599 lb_retry_match {
600 expression `+"`{rp.status_code} in [502, 503]`"+`
601 }
602 }
603 }
604 `, goodLn.Addr().String(), badLn.Addr().String()), "caddyfile")
605
606 tester.AssertGetResponse("http://localhost:9080/", 200, "ok")
607 }
608
609 // TestReverseProxyRetryMatchHeader verifies that lb_retry_match with a CEL
610 // expression matching on {rp.header.*} causes the request to be retried when
611 // the upstream sets a matching response header
612 func TestReverseProxyRetryMatchHeader(t *testing.T) {
613 var badHits atomic.Int32
614
615 // Bad upstream: returns 200 but signals retry via header
616 badSrv := &http.Server{
617 Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
618 badHits.Add(1)
619 w.Header().Set("X-Upstream-Retry", "true")
620 w.Write([]byte("bad"))
621 }),
622 }
623 badLn, err := net.Listen("tcp", "127.0.0.1:0")
624 if err != nil {
625 t.Fatalf("failed to listen: %v", err)
626 }
627 go badSrv.Serve(badLn)
628 t.Cleanup(func() { badSrv.Close(); badLn.Close() })
629
630 // Good upstream: returns 200 without retry header
631 goodSrv := &http.Server{
632 Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
633 w.Write([]byte("good"))
634 }),
635 }
636 goodLn, err := net.Listen("tcp", "127.0.0.1:0")
637 if err != nil {
638 t.Fatalf("failed to listen: %v", err)
639 }
640 go goodSrv.Serve(goodLn)
641 t.Cleanup(func() { goodSrv.Close(); goodLn.Close() })
642
643 tester := caddytest.NewTester(t)
644 tester.InitServer(fmt.Sprintf(`
645 {
646 skip_install_trust
647 admin localhost:2999
648 http_port 9080
649 https_port 9443
650 grace_period 1ns
651 }
652 http://localhost:9080 {
653 reverse_proxy %s %s {
654 lb_policy round_robin
655 lb_retries 1
656 lb_retry_match {
657 expression `+"`{rp.header.X-Upstream-Retry} == \"true\"`"+`
658 }
659 }
660 }
661 `, goodLn.Addr().String(), badLn.Addr().String()), "caddyfile")
662
663 tester.AssertGetResponse("http://localhost:9080/", 200, "good")
664
665 if badHits.Load() != 1 {
666 t.Errorf("bad upstream hits: got %d, want 1", badHits.Load())
667 }
668 }
669
670 // TestReverseProxyRetryMatchCombined verifies that a CEL expression combining
671 // request path matching with response status code matching works correctly -
672 // only retrying when both conditions are met
673 func TestReverseProxyRetryMatchCombined(t *testing.T) {
674 // Upstream: returns 502 for all requests
675 srv := &http.Server{
676 Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
677 w.WriteHeader(http.StatusBadGateway)
678 }),
679 }
680 ln, err := net.Listen("tcp", "127.0.0.1:0")
681 if err != nil {
682 t.Fatalf("failed to listen: %v", err)
683 }
684 go srv.Serve(ln)
685 t.Cleanup(func() { srv.Close(); ln.Close() })
686
687 // Good upstream
688 goodSrv := &http.Server{
689 Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
690 w.Write([]byte("ok"))
691 }),
692 }
693 goodLn, err := net.Listen("tcp", "127.0.0.1:0")
694 if err != nil {
695 t.Fatalf("failed to listen: %v", err)
696 }
697 go goodSrv.Serve(goodLn)
698 t.Cleanup(func() { goodSrv.Close(); goodLn.Close() })
699
700 tester := caddytest.NewTester(t)
701 tester.InitServer(fmt.Sprintf(`
702 {
703 skip_install_trust
704 admin localhost:2999
705 http_port 9080
706 https_port 9443
707 grace_period 1ns
708 }
709 http://localhost:9080 {
710 reverse_proxy %s %s {
711 lb_policy round_robin
712 lb_retries 1
713 lb_retry_match {
714 expression `+"`path('/retry*') && {rp.status_code} in [502, 503]`"+`
715 }
716 }
717 }
718 `, goodLn.Addr().String(), ln.Addr().String()), "caddyfile")
719
720 // /retry path matches the expression - should retry to good upstream
721 tester.AssertGetResponse("http://localhost:9080/retry", 200, "ok")
722
723 // /other path does NOT match - should return the 502
724 req, _ := http.NewRequest(http.MethodGet, "http://localhost:9080/other", nil)
725 tester.AssertResponse(req, 502, "")
726 }
727
728 // TestReverseProxyRetryMatchIsTransportError verifies that the
729 // {rp.is_transport_error} == true CEL function correctly identifies transport errors
730 // and allows retrying them alongside response-based matching
731 func TestReverseProxyRetryMatchIsTransportError(t *testing.T) {
732 // Good upstream: returns 200
733 goodSrv := &http.Server{
734 Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
735 w.Write([]byte("ok"))
736 }),
737 }
738 goodLn, err := net.Listen("tcp", "127.0.0.1:0")
739 if err != nil {
740 t.Fatalf("failed to listen: %v", err)
741 }
742 go goodSrv.Serve(goodLn)
743 t.Cleanup(func() { goodSrv.Close(); goodLn.Close() })
744
745 // Broken upstream: accepts connections but closes immediately
746 brokenLn, err := net.Listen("tcp", "127.0.0.1:0")
747 if err != nil {
748 t.Fatalf("failed to listen: %v", err)
749 }
750 t.Cleanup(func() { brokenLn.Close() })
751 go func() {
752 for {
753 conn, err := brokenLn.Accept()
754 if err != nil {
755 return
756 }
757 conn.Close()
758 }
759 }()
760
761 tester := caddytest.NewTester(t)
762 tester.InitServer(fmt.Sprintf(`
763 {
764 skip_install_trust
765 admin localhost:2999
766 http_port 9080
767 https_port 9443
768 grace_period 1ns
769 }
770 http://localhost:9080 {
771 reverse_proxy %s %s {
772 lb_policy round_robin
773 lb_retries 1
774 lb_retry_match {
775 expression `+"`{rp.is_transport_error} || {rp.status_code} in [502, 503]`"+`
776 }
777 }
778 }
779 `, goodLn.Addr().String(), brokenLn.Addr().String()), "caddyfile")
780
781 // Transport error on broken upstream should be retried to good upstream
782 tester.AssertGetResponse("http://localhost:9080/", 200, "ok")
783 }
784
785 func TestReverseProxySNIPlaceHolder(t *testing.T) {
786 configTemplate := `
787 {
788 skip_install_trust
789 local_certs
790 admin localhost:2999
791 http_port 9080
792 https_port 9443
793 grace_period 1ns
794 }
795 localhost example.com {
796 @proxied header X-Transport caddy
797 respond @proxied {http.request.tls.server_name}
798 reverse_proxy 127.0.0.1:9443 {
799 header_up X-Transport caddy
800 header_up Host {host}
801 transport http {
802 versions %s
803 tls_server_name {header.X-SNI}
804 tls_insecure_skip_verify
805 }
806 }
807 }
808 `
809 for _, versions := range []string{"1.1 2", "3"} {
810 tester := caddytest.NewTester(t)
811 tester.InitServer(fmt.Sprintf(configTemplate, versions), "caddyfile")
812 req, err := http.NewRequest("GET", "https://localhost:9443", nil)
813 if err != nil {
814 t.Errorf("failed to create request %s", err)
815 return
816 }
817
818 req.Header.Set("X-SNI", "example.com")
819 tester.AssertResponse(req, 200, "example.com")
820 }
821 }
822
823 func TestReverseProxyNormalizeWebSocketHeaders(t *testing.T) {
824 // 503s if it doesn't see "WebSocket" in a request
825 ln, err := net.Listen("tcp", "127.0.0.1:0")
826 if err != nil {
827 t.Fatalf("failed to listen: %v", err)
828 }
829 // http.Server would canonicalize these headers, so plain tcp:
830 errc := make(chan error, 1)
831 defer func() {
832 for range errc {
833 }
834 }()
835 defer ln.Close()
836 handleConn := func(conn net.Conn) error {
837 defer conn.Close()
838 rd := textproto.NewReader(bufio.NewReader(io.LimitReader(conn, 4096)))
839 status := 503
840 for {
841 line, err := rd.ReadLine()
842 if err != nil {
843 return fmt.Errorf("failed to read line: %v", err)
844 }
845 if len(line) == 0 {
846 break
847 }
848 if strings.Contains(line, "WebSocket") {
849 status = 200
850 }
851 }
852 res := fmt.Sprintf("HTTP/1.1 %d %s\r\nConnection: close\r\n\r\n", status, http.StatusText(status))
853 if _, err = conn.Write([]byte(res)); err != nil {
854 return fmt.Errorf("failed to write: %v", err)
855 }
856 return nil
857 }
858 go func() {
859 defer close(errc)
860 for {
861 conn, err := ln.Accept()
862 if err != nil {
863 if !errors.Is(err, net.ErrClosed) {
864 errc <- fmt.Errorf("failed to accept: %v", err)
865 }
866 return
867 }
868 if err := handleConn(conn); err != nil {
869 errc <- fmt.Errorf("failed to handle: %v", err)
870 }
871 }
872 }()
873 checkServer := func() {
874 select {
875 case err := <-errc:
876 t.Fatalf("failed to serve test request: %v", err)
877 default:
878 }
879 }
880 tester := caddytest.NewTester(t)
881 tester.InitServer(fmt.Sprintf(`
882 {
883 skip_install_trust
884 local_certs
885 admin localhost:2999
886 http_port 9080
887 https_port 9443
888 grace_period 1ns
889 }
890 http://localhost:9080 {
891 reverse_proxy http://%s {
892 # test compatibility with header manipulations
893 header_up X-Add-Header "1"
894 }
895 }`, ln.Addr()), "caddyfile")
896 req, err := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
897 if err != nil {
898 t.Fatalf("failed to create request: %v", err)
899 }
900 // Only the first WebSocket header needs the manual assignment,
901 // but mixing in .Set is likely to be more confusing.
902 req.Header["Connection"] = []string{"Upgrade"}
903 req.Header["Upgrade"] = []string{"websocket"}
904 // 1. resembling a browser via http/1.1
905 req.Header["Sec-WebSocket-Version"] = []string{"13"}
906 tester.AssertResponse(req, 200, "")
907 checkServer()
908 delete(req.Header, "Sec-WebSocket-Version")
909 // 2. canonicalized is okay
910 req.Header["Sec-Websocket-Version"] = []string{"13"}
911 tester.AssertResponse(req, 200, "")
912 checkServer()
913 delete(req.Header, "Sec-Websocket-Version")
914 // 3. unknown header doesn't receive special handling
915 req.Header["Sec-Websocket-Unnormalizedkey"] = []string{"13"}
916 tester.AssertResponse(req, 503, "")
917 checkServer()
918 }
919
920 func TestWeightedRoundRobinSelectionValidation(t *testing.T) {
921 configTemplate := `
922 {
923 "apps": {
924 "http": {
925 "servers": {
926 "srv0": {
927 "listen": [":18080"],
928 "routes": [
929 {
930 "handle": [
931 {
932 "handler": "reverse_proxy",
933 "load_balancing": {
934 "selection_policy": {
935 "policy": "weighted_round_robin",
936 "weights": %s
937 }
938 },
939 "upstreams": [
940 {"dial": "localhost:18081"},
941 {"dial": "localhost:18082"}
942 ]
943 }
944 ]
945 }
946 ]
947 }
948 }
949 }
950 }
951 }`
952
953 tests := []struct {
954 name string
955 weights string
956 errMsg string
957 }{
958 {
959 name: "negative weight",
960 weights: "[-1, 2]",
961 errMsg: "weight of an upstream cannot be negative",
962 },
963 {
964 name: "zero total weight",
965 weights: "[0, 0]",
966 errMsg: "requires at least one upstream with a positive weight",
967 },
968 }
969
970 for _, tc := range tests {
971 t.Run(tc.name, func(t *testing.T) {
972 caddytest.AssertLoadError(
973 t,
974 fmt.Sprintf(configTemplate, tc.weights),
975 "json",
976 tc.errMsg,
977 )
978 })
979 }
980 }
981
982 func TestReverseProxyResponseHandling(t *testing.T) {
983 mux := http.NewServeMux()
984 mux.HandleFunc("/header", func(writer http.ResponseWriter, request *http.Request) {
985 writer.Header().Set("Content-Length", "500")
986 writer.WriteHeader(500)
987 _ = http.NewResponseController(writer).Flush()
988 panic(http.ErrAbortHandler)
989 })
990 mux.HandleFunc("/partial", func(writer http.ResponseWriter, request *http.Request) {
991 writer.Header().Set("Content-Length", "500")
992 writer.WriteHeader(500)
993 _, _ = io.WriteString(writer, "partial")
994 _ = http.NewResponseController(writer).Flush()
995 panic(http.ErrAbortHandler)
996 })
997 mux.HandleFunc("/full", func(writer http.ResponseWriter, request *http.Request) {
998 writer.Header().Set("Content-Length", "500")
999 writer.WriteHeader(500)
1000 for range 125 {
1001 _, _ = io.WriteString(writer, "full")
1002 }
1003 _ = http.NewResponseController(writer).Flush()
1004 })
1005 mux.HandleFunc("/empty", func(writer http.ResponseWriter, request *http.Request) {
1006 panic(http.ErrAbortHandler)
1007 })
1008 ts := httptest.NewUnstartedServer(mux)
1009 ts.Start()
1010 t.Cleanup(func() {
1011 ts.Close()
1012 })
1013
1014 tester := caddytest.NewTester(t)
1015 tester.InitServer(fmt.Sprintf(`
1016 {
1017 skip_install_trust
1018 admin localhost:2999
1019 http_port 9080
1020 https_port 9443
1021 grace_period 1ns
1022 }
1023 http://localhost:9080 {
1024 reverse_proxy %s
1025 }
1026 `, ts.URL), "caddyfile")
1027
1028 for _, tc := range []struct {
1029 endpoint string
1030 status int
1031 bodyType int // 0 = no body, 1 = partial, 2 = full
1032 body string
1033 }{
1034 {
1035 endpoint: "/header",
1036 status: 500,
1037 bodyType: 0,
1038 body: "",
1039 },
1040 {
1041 endpoint: "/partial",
1042 status: 500,
1043 bodyType: 1,
1044 body: "partial",
1045 },
1046 {
1047 endpoint: "/full",
1048 status: 500,
1049 bodyType: 2,
1050 body: strings.Repeat("full", 125),
1051 },
1052 {
1053 endpoint: "/empty",
1054 status: 502,
1055 bodyType: 2,
1056 body: "",
1057 },
1058 } {
1059 req, err := http.NewRequest("GET", "http://localhost:9080"+tc.endpoint, nil)
1060 if err != nil {
1061 t.Fatalf("unable to create request %s for endpoint %s", err, tc.endpoint)
1062 }
1063
1064 resp, err := tester.Client.Do(req)
1065 if err != nil {
1066 t.Fatalf("request failed: %s for endpoint %s", err, tc.endpoint)
1067 }
1068 if resp.StatusCode != tc.status {
1069 t.Fatalf("unexpected status code for %s: got %d, want %d", tc.endpoint, resp.StatusCode, tc.status)
1070 }
1071
1072 body, err := io.ReadAll(resp.Body)
1073 _ = resp.Body.Close()
1074 switch tc.bodyType {
1075 case 0, 1:
1076 if err == nil {
1077 t.Fatalf("expected error reading body for %s, got none", tc.endpoint)
1078 }
1079 case 2:
1080 if err != nil {
1081 t.Fatalf("error reading body for %s: %s", tc.endpoint, err)
1082 }
1083 }
1084 if string(body) != tc.body {
1085 t.Fatalf("unexpected body for %s: got %q, want %q", tc.endpoint, string(body), tc.body)
1086 }
1087 }
1088 }
1089
1090 // TestReverseProxyUnlimitedRequestBuffersRespectMaxSize verifies that a request
1091 // body cut short by request_body max_size is rejected with 413 instead of being
1092 // forwarded to the upstream as if it were complete.
1093 func TestReverseProxyUnlimitedRequestBuffersRespectMaxSize(t *testing.T) {
1094 var forwarded atomic.Int32
1095 srv := &http.Server{
1096 Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1097 if _, err := io.ReadAll(r.Body); err == nil {
1098 forwarded.Add(1)
1099 }
1100 }),
1101 }
1102 ln, err := net.Listen("tcp", "127.0.0.1:0")
1103 if err != nil {
1104 t.Fatalf("failed to listen: %v", err)
1105 }
1106 go srv.Serve(ln)
1107 t.Cleanup(func() { srv.Close(); ln.Close() })
1108
1109 tester := caddytest.NewTester(t)
1110 tester.InitServer(fmt.Sprintf(`
1111 {
1112 skip_install_trust
1113 admin localhost:2999
1114 http_port 9080
1115 https_port 9443
1116 grace_period 1ns
1117 }
1118 http://localhost:9080 {
1119 request_body {
1120 max_size 10
1121 }
1122 reverse_proxy %s {
1123 request_buffers unlimited
1124 }
1125 }
1126 `, ln.Addr().String()), "caddyfile")
1127
1128 req, err := http.NewRequest(http.MethodPost, "http://localhost:9080/", strings.NewReader("abcdefghijklm"))
1129 if err != nil {
1130 t.Fatalf("creating request: %v", err)
1131 }
1132 res := tester.AssertResponseCode(req, http.StatusRequestEntityTooLarge)
1133 res.Body.Close()
1134
1135 if n := forwarded.Load(); n != 0 {
1136 t.Errorf("upstream received %d complete request bodies, want 0", n)
1137 }
1138 }
1139
1140 // TestReverseProxyWebSocketUpgradeUnixSocket verifies that a WebSocket
1141 // upgrade is proxied to a unix socket upstream, that the upgraded
1142 // connection carries data in both directions, and that closing the
1143 // client side tears down the upstream connection
1144 func TestReverseProxyWebSocketUpgradeUnixSocket(t *testing.T) {
1145 if runtime.GOOS == "windows" {
1146 t.SkipNow()
1147 }
1148
1149 socketName := tempSocketName(t)
1150
1151 // sent in the same write as the 101 response, so it may arrive in the
1152 // same read as the response headers and must not be dropped by the proxy
1153 const greeting = "hello from upstream"
1154
1155 // closed once the upstream's echo loop ends, i.e. when the proxy
1156 // closes the upstream connection
1157 upstreamDone := make(chan struct{})
1158
1159 // upstream completes the handshake, then echoes everything it reads
1160 backend := http.Server{
1161 Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
1162 if !strings.EqualFold(req.Header.Get("Upgrade"), "websocket") {
1163 http.Error(w, "missing websocket upgrade headers", http.StatusBadRequest)
1164 return
1165 }
1166 wsKey := req.Header.Get("Sec-WebSocket-Key")
1167 if wsKey == "" {
1168 http.Error(w, "missing Sec-WebSocket-Key", http.StatusBadRequest)
1169 return
1170 }
1171
1172 conn, brw, err := http.NewResponseController(w).Hijack()
1173 if err != nil {
1174 return
1175 }
1176 defer close(upstreamDone)
1177 defer conn.Close()
1178
1179 _, _ = brw.WriteString("HTTP/1.1 101 Switching Protocols\r\n" +
1180 "Upgrade: websocket\r\n" +
1181 "Connection: Upgrade\r\n" +
1182 "Sec-WebSocket-Accept: " + computeWebSocketAccept(wsKey) + "\r\n" +
1183 "\r\n" +
1184 greeting)
1185 if brw.Flush() != nil {
1186 return
1187 }
1188 _, _ = io.Copy(conn, brw)
1189 }),
1190 }
1191
1192 unixListener, err := net.Listen("unix", socketName)
1193 if err != nil {
1194 t.Fatalf("failed to listen on unix socket: %v", err)
1195 }
1196 go backend.Serve(unixListener)
1197 t.Cleanup(func() {
1198 _ = backend.Close()
1199 _ = unixListener.Close()
1200 })
1201
1202 tester := caddytest.NewTester(t)
1203 tester.InitServer(fmt.Sprintf(`
1204 {
1205 skip_install_trust
1206 admin localhost:2999
1207 http_port 9080
1208 https_port 9443
1209 grace_period 1ns
1210 }
1211 http://localhost:9080 {
1212 reverse_proxy unix/%s
1213 }
1214 `, socketName), "caddyfile")
1215
1216 conn, err := net.Dial("tcp", "127.0.0.1:9080")
1217 if err != nil {
1218 t.Fatalf("failed to dial caddy listener: %v", err)
1219 }
1220 defer conn.Close()
1221 _ = conn.SetDeadline(time.Now().Add(10 * time.Second))
1222
1223 const wsKey = "dGhlIHNhbXBsZSBub25jZQ=="
1224 if _, err := io.WriteString(conn, "GET /ws HTTP/1.1\r\n"+
1225 "Host: localhost:9080\r\n"+
1226 "Connection: Upgrade\r\n"+
1227 "Upgrade: websocket\r\n"+
1228 "Sec-WebSocket-Version: 13\r\n"+
1229 "Sec-WebSocket-Key: "+wsKey+"\r\n"+
1230 "\r\n"); err != nil {
1231 t.Fatalf("failed to send websocket handshake request: %v", err)
1232 }
1233
1234 br := bufio.NewReader(conn)
1235 resp, err := http.ReadResponse(br, nil)
1236 if err != nil {
1237 t.Fatalf("failed to read handshake response: %v", err)
1238 }
1239 if resp.StatusCode != http.StatusSwitchingProtocols {
1240 t.Fatalf("unexpected status: got %d, want %d", resp.StatusCode, http.StatusSwitchingProtocols)
1241 }
1242 if got := resp.Header.Get("Upgrade"); !strings.EqualFold(got, "websocket") {
1243 t.Fatalf("unexpected Upgrade header: %q", got)
1244 }
1245 if got, want := resp.Header.Get("Sec-WebSocket-Accept"), computeWebSocketAccept(wsKey); got != want {
1246 t.Fatalf("unexpected Sec-WebSocket-Accept header: got %q, want %q", got, want)
1247 }
1248
1249 gotGreeting := make([]byte, len(greeting))
1250 if _, err := io.ReadFull(br, gotGreeting); err != nil {
1251 t.Fatalf("failed to read upstream greeting: %v", err)
1252 }
1253 if string(gotGreeting) != greeting {
1254 t.Fatalf("unexpected greeting: got %q, want %q", gotGreeting, greeting)
1255 }
1256
1257 // the payload doesn't need to be framed since the proxy just tunnels
1258 // bytes; make it much larger than any single read or write buffer, and
1259 // write it concurrently so the echo can't back up and deadlock
1260 payload := make([]byte, 256*1024)
1261 _, _ = rand.Read(payload)
1262 writeErr := make(chan error, 1)
1263 go func() {
1264 _, err := conn.Write(payload)
1265 writeErr <- err
1266 }()
1267 echo := make([]byte, len(payload))
1268 if _, err := io.ReadFull(br, echo); err != nil {
1269 t.Fatalf("failed to read echo from upgraded connection: %v", err)
1270 }
1271 if err := <-writeErr; err != nil {
1272 t.Fatalf("failed to write to upgraded connection: %v", err)
1273 }
1274 if !bytes.Equal(echo, payload) {
1275 t.Fatal("echoed payload does not match what was sent")
1276 }
1277
1278 // closing the client should make the proxy close the upstream connection
1279 _ = conn.Close()
1280 select {
1281 case <-upstreamDone:
1282 case <-time.After(5 * time.Second):
1283 t.Fatal("upstream connection was not closed after the client disconnected")
1284 }
1285 }
1286
1287 func computeWebSocketAccept(wsKey string) string {
1288 h := sha1.Sum([]byte(wsKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
1289 return base64.StdEncoding.EncodeToString(h[:])
1290 }