@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,206 lines 36.8 KB
Raw
1 package reverseproxy
2
3 import (
4 "bufio"
5 "bytes"
6 "context"
7 "errors"
8 "io"
9 "net"
10 "net/http"
11 "net/http/httptest"
12 "net/url"
13 "os"
14 "runtime"
15 "strings"
16 "sync"
17 "sync/atomic"
18 "testing"
19 "time"
20
21 "github.com/prometheus/client_golang/prometheus"
22 "go.uber.org/zap"
23
24 "github.com/caddyserver/caddy/v2"
25 "github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
26 "github.com/caddyserver/caddy/v2/modules/caddyhttp"
27 )
28
29 func TestHandlerCopyResponse(t *testing.T) {
30 h := Handler{}
31 testdata := []string{
32 "",
33 strings.Repeat("a", defaultBufferSize),
34 strings.Repeat("123456789 123456789 123456789 12", 3000),
35 }
36
37 dst := bytes.NewBuffer(nil)
38 recorder := httptest.NewRecorder()
39 recorder.Body = dst
40
41 for _, d := range testdata {
42 src := bytes.NewBuffer([]byte(d))
43 dst.Reset()
44 err := h.copyResponse(recorder, src, 0, caddy.Log())
45 if err != nil {
46 t.Errorf("failed with error: %v", err)
47 }
48 out := dst.String()
49 if out != d {
50 t.Errorf("bad read: got %q", out)
51 }
52 }
53 }
54
55 func TestSwitchProtocolCopierBufferSize(t *testing.T) {
56 var wg sync.WaitGroup
57 var errc = make(chan error, 1)
58 var dst bytes.Buffer
59 var sent, received int64
60
61 copier := switchProtocolCopier{
62 user: nopReadWriteCloser{Reader: strings.NewReader("hello")},
63 backend: nopReadWriteCloser{Writer: &dst},
64 wg: &wg,
65 bufferSize: 7,
66 sent: &sent,
67 received: &received,
68 }
69
70 buf := copier.buffer()
71 if got := len(buf); got != 7 {
72 t.Fatalf("buffer len = %d, want 7", got)
73 }
74
75 wg.Add(1)
76 go copier.copyToBackend(errc)
77 wg.Wait()
78
79 // The destination cannot be half-closed, so a clean copy reports errCopyDone.
80 if err := <-errc; !errors.Is(err, errCopyDone) {
81 t.Fatalf("copyToBackend() error = %v", err)
82 }
83 if got := dst.String(); got != "hello" {
84 t.Fatalf("copied data = %q, want %q", got, "hello")
85 }
86 }
87
88 // A clean EOF in one direction must be propagated to the destination as a
89 // write-half close instead of tearing the whole tunnel down.
90 // See https://github.com/caddyserver/caddy/issues/8026.
91 func TestSwitchProtocolCopierHalfClose(t *testing.T) {
92 closeWriteErr := errors.New("no half-close")
93
94 for _, tc := range []struct {
95 name string
96 dst io.ReadWriteCloser
97 wantErr error // nil means the half-close was propagated
98 }{
99 {
100 name: "destination supports CloseWrite",
101 dst: &closeWriteRecorder{},
102 wantErr: nil,
103 },
104 {
105 name: "destination does not support CloseWrite",
106 dst: nopReadWriteCloser{Writer: io.Discard},
107 wantErr: errCopyDone,
108 },
109 {
110 name: "CloseWrite fails",
111 dst: &closeWriteRecorder{err: closeWriteErr},
112 wantErr: closeWriteErr,
113 },
114 } {
115 t.Run(tc.name, func(t *testing.T) {
116 var wg sync.WaitGroup
117 var sent, received int64
118 errc := make(chan error, 1)
119 copier := switchProtocolCopier{
120 user: nopReadWriteCloser{Reader: strings.NewReader("hello")},
121 backend: tc.dst,
122 wg: &wg,
123 sent: &sent,
124 received: &received,
125 }
126
127 wg.Add(1)
128 go copier.copyToBackend(errc)
129 wg.Wait()
130
131 if err := <-errc; !errors.Is(err, tc.wantErr) {
132 t.Fatalf("copyToBackend() error = %v, want %v", err, tc.wantErr)
133 }
134 if rec, ok := tc.dst.(*closeWriteRecorder); ok && !rec.called {
135 t.Fatal("CloseWrite() was not called on a destination that supports it")
136 }
137 })
138 }
139 }
140
141 // A copy error must be reported as-is and must not be mistaken for a clean
142 // half-close, so the tunnel is still torn down immediately.
143 func TestSwitchProtocolCopierErrorIsNotHalfClose(t *testing.T) {
144 var wg sync.WaitGroup
145 var sent, received int64
146 errc := make(chan error, 1)
147 wantErr := errors.New("write failed")
148 dst := &closeWriteRecorder{writeErr: wantErr}
149
150 copier := switchProtocolCopier{
151 user: nopReadWriteCloser{Reader: strings.NewReader("hello")},
152 backend: dst,
153 wg: &wg,
154 sent: &sent,
155 received: &received,
156 }
157
158 wg.Add(1)
159 go copier.copyToBackend(errc)
160 wg.Wait()
161
162 if err := <-errc; !errors.Is(err, wantErr) {
163 t.Fatalf("error = %v, want %v", err, wantErr)
164 }
165 if dst.called {
166 t.Fatal("CloseWrite() was called after a copy error")
167 }
168 }
169
170 func TestSwitchProtocolCopierDefaultBufferSize(t *testing.T) {
171 copier := switchProtocolCopier{}
172 buf := copier.buffer()
173 if got := len(buf); got != defaultBufferSize {
174 t.Fatalf("buffer len = %d, want %d", got, defaultBufferSize)
175 }
176 }
177
178 type nopReadWriteCloser struct {
179 io.Reader
180 io.Writer
181 }
182
183 func (nopReadWriteCloser) Close() error { return nil }
184
185 type trackingReadWriteCloser struct {
186 closed chan struct{}
187 one sync.Once
188 }
189
190 func newTrackingReadWriteCloser() *trackingReadWriteCloser {
191 return &trackingReadWriteCloser{closed: make(chan struct{})}
192 }
193
194 func (c *trackingReadWriteCloser) Read(_ []byte) (int, error) { return 0, io.EOF }
195 func (c *trackingReadWriteCloser) Write(p []byte) (int, error) { return len(p), nil }
196 func (c *trackingReadWriteCloser) Close() error {
197 c.one.Do(func() {
198 close(c.closed)
199 })
200 return nil
201 }
202
203 func (c *trackingReadWriteCloser) isClosed() bool {
204 select {
205 case <-c.closed:
206 return true
207 default:
208 return false
209 }
210 }
211
212 func TestHandlerCleanupLegacyModeClosesAllConnections(t *testing.T) {
213 ts := newTunnelTracker(caddy.Log(), 0)
214 connA := newTrackingReadWriteCloser()
215 connB := newTrackingReadWriteCloser()
216 ts.registerConnection(connA, nil, false, "a")
217 ts.registerConnection(connB, nil, false, "b")
218
219 h := &Handler{
220 tunnelTracker: ts,
221 StreamDetached: false,
222 }
223
224 if err := h.Cleanup(); err != nil {
225 t.Fatalf("cleanup failed: %v", err)
226 }
227 if !connA.isClosed() || !connB.isClosed() {
228 t.Fatalf("legacy cleanup should close all upgraded connections")
229 }
230 }
231
232 func TestHandlerCleanupLegacyModeHonorsDelay(t *testing.T) {
233 ts := newTunnelTracker(caddy.Log(), 40*time.Millisecond)
234 conn := newTrackingReadWriteCloser()
235 ts.registerConnection(conn, nil, false, "a")
236
237 h := &Handler{
238 tunnelTracker: ts,
239 StreamDetached: false,
240 }
241
242 if err := h.Cleanup(); err != nil {
243 t.Fatalf("cleanup failed: %v", err)
244 }
245 if conn.isClosed() {
246 t.Fatal("connection should not close immediately when stream_close_delay is set")
247 }
248
249 select {
250 case <-conn.closed:
251 case <-time.After(500 * time.Millisecond):
252 t.Fatal("connection did not close after stream_close_delay elapsed")
253 }
254 }
255
256 func TestHandlerCleanupDetachedModeClosesOnlyRemovedUpstreams(t *testing.T) {
257 const upstreamA = "upstream-a"
258 const upstreamB = "upstream-b"
259
260 // Simulate old+new configs both referencing upstreamA (refcount 2),
261 // while upstreamB is only referenced by the old config (refcount 1).
262 hosts.LoadOrStore(upstreamA, struct{}{})
263 hosts.LoadOrStore(upstreamA, struct{}{})
264 hosts.LoadOrStore(upstreamB, struct{}{})
265 t.Cleanup(func() {
266 _, _ = hosts.Delete(upstreamA)
267 _, _ = hosts.Delete(upstreamA)
268 _, _ = hosts.Delete(upstreamB)
269 })
270
271 ts := newTunnelTracker(caddy.Log(), 0)
272 registerDetachedTunnelTrackers(ts)
273 connA := newTrackingReadWriteCloser()
274 connB := newTrackingReadWriteCloser()
275 ts.registerConnection(connA, nil, true, upstreamA)
276 ts.registerConnection(connB, nil, true, upstreamB)
277
278 h := &Handler{
279 tunnelTracker: ts,
280 StreamDetached: true,
281 Upstreams: UpstreamPool{
282 &Upstream{Dial: upstreamA},
283 &Upstream{Dial: upstreamB},
284 },
285 }
286
287 if err := h.Cleanup(); err != nil {
288 t.Fatalf("cleanup failed: %v", err)
289 }
290
291 if connA.isClosed() {
292 t.Fatal("connection for detached upstream should remain open")
293 }
294 if !connB.isClosed() {
295 t.Fatal("connection for removed upstream should be closed")
296 }
297 }
298
299 func TestHandlerUnmarshalCaddyfileStreamLogsBlock(t *testing.T) {
300 d := caddyfile.NewTestDispenser(`
301 reverse_proxy localhost:9000 {
302 stream_logs {
303 level info
304 logger_name access
305 skip_handshake
306 }
307 }
308 `)
309
310 var h Handler
311 if err := h.UnmarshalCaddyfile(d); err != nil {
312 t.Fatalf("UnmarshalCaddyfile() error = %v", err)
313 }
314 if h.StreamLogs == nil {
315 t.Fatal("expected stream_logs to be configured")
316 }
317 if h.StreamLogs.Level != "info" {
318 t.Fatalf("expected stream_logs.level=info, got %q", h.StreamLogs.Level)
319 }
320 if h.StreamLogs.LoggerName != "access" {
321 t.Fatalf("expected stream_logs.logger_name=access, got %q", h.StreamLogs.LoggerName)
322 }
323 if !h.StreamLogs.SkipHandshake {
324 t.Fatal("expected stream_logs.skip_handshake=true")
325 }
326 }
327
328 // closeWriteRecorder is a destination that supports closing only its write
329 // half, and records whether that happened.
330 type closeWriteRecorder struct {
331 called bool
332 err error
333 writeErr error
334 }
335
336 func (c *closeWriteRecorder) Read([]byte) (int, error) { return 0, io.EOF }
337
338 func (c *closeWriteRecorder) Write(p []byte) (int, error) {
339 if c.writeErr != nil {
340 return 0, c.writeErr
341 }
342 return len(p), nil
343 }
344
345 func (c *closeWriteRecorder) Close() error { return nil }
346
347 func (c *closeWriteRecorder) CloseWrite() error {
348 c.called = true
349 return c.err
350 }
351
352 // TestHandlerUpgradedStreamHalfClose drives a real upgraded stream through the
353 // handler and checks that closing one direction is propagated to the other side
354 // instead of tearing the whole tunnel down.
355 //
356 // It mirrors net/http/httputil's TestReverseProxyWebSocketHalfTCP, which covers
357 // the same class upstream (https://go.dev/issue/35892). Before the fix, the
358 // "close write" cases failed: the handler returned as soon as the first
359 // direction reported, and the deferred closes killed the other direction before
360 // its pending bytes could be delivered.
361 //
362 // See https://github.com/caddyserver/caddy/issues/8026.
363 func TestHandlerUpgradedStreamHalfClose(t *testing.T) {
364 switch runtime.GOOS {
365 case "plan9", "js", "wasip1":
366 t.Skipf("not supported on %s", runtime.GOOS)
367 }
368
369 // Reads carry a deadline so a regression fails the test promptly instead of
370 // blocking until the whole package times out.
371 const readTimeout = 10 * time.Second
372
373 mustRead := func(t *testing.T, conn *net.TCPConn, msg string) {
374 t.Helper()
375 if err := conn.SetReadDeadline(time.Now().Add(readTimeout)); err != nil {
376 t.Fatalf("failed to set read deadline: %v", err)
377 }
378 b := make([]byte, len(msg))
379 if _, err := io.ReadFull(conn, b); err != nil {
380 t.Fatalf("failed to read: %v", err)
381 }
382 if got, want := string(b), msg; got != want {
383 t.Fatalf("got %#q, want %#q", got, want)
384 }
385 }
386
387 mustReadEOF := func(t *testing.T, conn *net.TCPConn) {
388 t.Helper()
389 if err := conn.SetReadDeadline(time.Now().Add(readTimeout)); err != nil {
390 t.Fatalf("failed to set read deadline: %v", err)
391 }
392 b := make([]byte, 1)
393 if _, err := conn.Read(b); !errors.Is(err, io.EOF) {
394 t.Fatalf("read after peer half-close: got %v, want EOF", err)
395 }
396 }
397
398 mustWrite := func(t *testing.T, conn *net.TCPConn, msg string) {
399 t.Helper()
400 if _, err := conn.Write([]byte(msg)); err != nil {
401 t.Fatalf("failed to write: %v", err)
402 }
403 }
404
405 mustCloseRead := func(t *testing.T, conn *net.TCPConn) {
406 t.Helper()
407 if err := conn.CloseRead(); err != nil {
408 t.Fatalf("failed to CloseRead: %v", err)
409 }
410 }
411
412 mustCloseWrite := func(t *testing.T, conn *net.TCPConn) {
413 t.Helper()
414 if err := conn.CloseWrite(); err != nil {
415 t.Fatalf("failed to CloseWrite: %v", err)
416 }
417 }
418
419 tests := map[string]func(t *testing.T, cli, srv *net.TCPConn){
420 "backend close read": func(t *testing.T, cli, srv *net.TCPConn) {
421 mustCloseRead(t, srv)
422 mustWrite(t, srv, "backend sends")
423 mustRead(t, cli, "backend sends")
424 },
425 "backend close write": func(t *testing.T, cli, srv *net.TCPConn) {
426 mustCloseWrite(t, srv)
427 mustReadEOF(t, cli)
428 mustWrite(t, cli, "client sends")
429 mustRead(t, srv, "client sends")
430 },
431 "client close read": func(t *testing.T, cli, srv *net.TCPConn) {
432 mustCloseRead(t, cli)
433 mustWrite(t, cli, "client sends")
434 mustRead(t, srv, "client sends")
435 },
436 "client close write": func(t *testing.T, cli, srv *net.TCPConn) {
437 mustCloseWrite(t, cli)
438 mustReadEOF(t, srv)
439 mustWrite(t, srv, "backend sends")
440 mustRead(t, cli, "backend sends")
441 },
442 }
443
444 for name, test := range tests {
445 t.Run(name, func(t *testing.T) {
446 _, cli, srv := newUpgradedTunnel(t)
447 test(t, cli, srv)
448 })
449 }
450 }
451
452 // newUpgradedTunnel proxies a websocket upgrade from a raw client connection
453 // through a Handler to a hijacking backend and returns both ends of the
454 // tunnel. The Handler's tunnel tracker holds the proxied connections.
455 func newUpgradedTunnel(t *testing.T) (h *Handler, cli, srv *net.TCPConn) {
456 t.Helper()
457 srvc := make(chan *net.TCPConn, 1)
458
459 backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
460 conn, _, err := http.NewResponseController(w).Hijack()
461 if err != nil {
462 t.Errorf("backend hijack failed: %v", err)
463 return
464 }
465 tcp, ok := conn.(*net.TCPConn)
466 if !ok {
467 conn.Close()
468 t.Errorf("backend conn is %T, want *net.TCPConn", conn)
469 return
470 }
471 if _, err := io.WriteString(tcp, "HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n"); err != nil {
472 tcp.Close()
473 t.Errorf("backend upgrade write failed: %v", err)
474 return
475 }
476 srvc <- tcp
477 }))
478 t.Cleanup(backend.Close)
479
480 h = minimalHandler(0, &Upstream{
481 Host: new(Host),
482 Dial: backend.Listener.Addr().String(),
483 })
484 h.tunnelTracker = newTunnelTracker(caddy.Log(), 0)
485 initReverseProxyMetrics(h, prometheus.NewRegistry())
486
487 frontend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
488 // Isolate the tunnel from request cancellation, which has its own
489 // backend-close path.
490 r = r.WithContext(context.WithoutCancel(r.Context()))
491 _ = h.ServeHTTP(w, prepareTestRequest(r), caddyhttp.HandlerFunc(func(http.ResponseWriter, *http.Request) error {
492 return nil
493 }))
494 }))
495 t.Cleanup(frontend.Close)
496
497 frontendURL, err := url.Parse(frontend.URL)
498 if err != nil {
499 t.Fatalf("failed to parse frontend URL: %v", err)
500 }
501 addr, err := net.ResolveTCPAddr("tcp", frontendURL.Host)
502 if err != nil {
503 t.Fatalf("failed to resolve TCP address: %v", err)
504 }
505 cli, err = net.DialTCP("tcp", nil, addr)
506 if err != nil {
507 t.Fatalf("failed to dial frontend: %v", err)
508 }
509 t.Cleanup(func() { cli.Close() })
510
511 req, _ := http.NewRequest(http.MethodGet, frontend.URL, nil)
512 req.Header.Set("Connection", "Upgrade")
513 req.Header.Set("Upgrade", "websocket")
514 if err := req.Write(cli); err != nil {
515 t.Fatalf("failed to write upgrade request: %v", err)
516 }
517
518 resp, err := http.ReadResponse(bufio.NewReader(cli), &http.Request{Method: http.MethodGet})
519 if err != nil {
520 t.Fatalf("failed to read upgrade response: %v", err)
521 }
522 if resp.StatusCode != http.StatusSwitchingProtocols {
523 t.Fatalf("status = %d, want 101", resp.StatusCode)
524 }
525
526 select {
527 case srv = <-srvc:
528 case <-time.After(10 * time.Second):
529 t.Fatal("timed out waiting for the backend connection")
530 }
531 t.Cleanup(func() { srv.Close() })
532 return h, cli, srv
533 }
534
535 // A response carrying the Incremental header field (RFC 10036) must be
536 // forwarded without buffering, whatever its Content-Type and Content-Length.
537 func TestFlushIntervalIncremental(t *testing.T) {
538 h := Handler{FlushInterval: caddy.Duration(time.Second)}
539 req := httptest.NewRequest(http.MethodGet, "/", nil)
540
541 for _, tc := range []struct {
542 name string
543 incremental string
544 receivedIncremental bool
545 want time.Duration
546 }{
547 {name: "incremental", incremental: "?1", want: -1},
548 {name: "not incremental", incremental: "?0", want: time.Second},
549 {name: "absent", want: time.Second},
550 {name: "removed by header operations", receivedIncremental: true, want: -1},
551 } {
552 t.Run(tc.name, func(t *testing.T) {
553 res := &http.Response{
554 Header: http.Header{"Content-Type": []string{"application/json"}},
555 ContentLength: 42,
556 }
557 if tc.incremental != "" {
558 res.Header.Set("Incremental", tc.incremental)
559 }
560 if got := h.flushInterval(req, res, tc.receivedIncremental); got != tc.want {
561 t.Errorf("flushInterval() = %v, want %v", got, tc.want)
562 }
563 })
564 }
565 }
566
567 func TestHandlerCleanupDetachedRemovalWithDelay(t *testing.T) {
568 const upstream = "detached-removal-with-delay"
569 hosts.LoadOrStore(upstream, new(Host))
570 t.Cleanup(func() { _, _ = hosts.Delete(upstream) })
571 ts := newTunnelTracker(caddy.Log(), time.Hour)
572 registerDetachedTunnelTrackers(ts)
573 t.Cleanup(func() { unregisterDetachedTunnelTrackers(ts) })
574 detached := newTrackingReadWriteCloser()
575 attached := newTrackingReadWriteCloser()
576 deleteDetached := ts.registerConnection(detached, nil, true, upstream)
577 deleteAttached := ts.registerConnection(attached, nil, false, upstream)
578 t.Cleanup(deleteDetached)
579 t.Cleanup(deleteAttached)
580 h := &Handler{tunnelTracker: ts, StreamDetached: true, Upstreams: UpstreamPool{&Upstream{Dial: upstream}}}
581 if err := h.Cleanup(); err != nil {
582 t.Fatal(err)
583 }
584 if !detached.isClosed() {
585 t.Error("detached stream remains open after upstream removal")
586 }
587 if attached.isClosed() {
588 t.Error("attached stream closed before stream_close_delay elapsed")
589 }
590 }
591
592 func TestHandlerCleanupEmptyDetachedTracker(t *testing.T) {
593 for _, delay := range []time.Duration{0, time.Hour} {
594 t.Run(delay.String(), func(t *testing.T) {
595 ts := newTunnelTracker(caddy.Log(), delay)
596 registerDetachedTunnelTrackers(ts)
597 t.Cleanup(func() { unregisterDetachedTunnelTrackers(ts) })
598 // Cover a tracker whose last connection closed before handler cleanup.
599 conn := newTrackingReadWriteCloser()
600 ts.registerConnection(conn, nil, true, "upstream")()
601 if err := (&Handler{tunnelTracker: ts, StreamDetached: true}).Cleanup(); err != nil {
602 t.Fatal(err)
603 }
604 detachedTunnelTrackersMu.Lock()
605 _, retained := detachedTunnelTrackers[ts]
606 detachedTunnelTrackersMu.Unlock()
607 if retained {
608 t.Error("empty tracker remains globally registered after cleanup")
609 }
610 })
611 }
612 }
613
614 func TestDetachedTrackerRemovalAllowsConcurrentConnectionDeletion(t *testing.T) {
615 ts := newTunnelTracker(caddy.Log(), 0)
616 registerDetachedTunnelTrackers(ts)
617 t.Cleanup(func() { unregisterDetachedTunnelTrackers(ts) })
618 // An attached stream on another stopped handler can finish while an
619 // upstream-removal notification closes a detached stream. Its deletion
620 // must be able to acquire the registry lock and release its tracker lock.
621 other := newTunnelTracker(caddy.Log(), 0)
622 otherConn := newTrackingReadWriteCloser()
623 deleteOther := other.registerConnection(otherConn, nil, false, "other")
624 if err := other.cleanupAttachedConnections(); err != nil {
625 t.Fatal(err)
626 }
627 deleted := make(chan struct{})
628 conn := newTrackingReadWriteCloser()
629 del := ts.registerConnection(conn, func(time.Time) error {
630 go func() {
631 deleteOther()
632 close(deleted)
633 }()
634 select {
635 case <-deleted:
636 case <-time.After(time.Second):
637 t.Error("upstream removal blocked another connection's deletion")
638 }
639 return nil
640 }, true, "upstream")
641 t.Cleanup(del)
642 if err := ts.cleanupAttachedConnections(); err != nil {
643 t.Fatal(err)
644 }
645 if err := notifyDetachedTunnelTrackersOfUpstreamRemoval("upstream", ts); err != nil {
646 t.Fatal(err)
647 }
648 // On a regression the notification must release its lock before waiting
649 // for deletion, so the failing test does not leave a blocked goroutine.
650 <-deleted
651 if !conn.isClosed() {
652 t.Error("removed upstream connection was not closed")
653 }
654 }
655
656 // newStalledWebsocketPeer returns one end of a pipe whose other end never
657 // reads, with a goroutine stuck writing to it the way a tunnel's copy loop
658 // would be. net.Pipe is unbuffered and lets only one write through at a
659 // time, so a close message written to conn waits behind that stuck write,
660 // just as it does on a socket whose peer stopped reading.
661 func newStalledWebsocketPeer(t *testing.T) net.Conn {
662 t.Helper()
663 conn, peer := net.Pipe()
664 t.Cleanup(func() {
665 conn.Close()
666 peer.Close()
667 })
668 go func() { _, _ = conn.Write([]byte("stalled")) }()
669 return conn
670 }
671
672 // shortenCloseControlWriteTimeout makes a stalled peer time out quickly for
673 // the rest of the test.
674 func shortenCloseControlWriteTimeout(t *testing.T) {
675 t.Helper()
676 saved := closeControlWriteTimeout
677 closeControlWriteTimeout = 10 * time.Millisecond
678 t.Cleanup(func() { closeControlWriteTimeout = saved })
679 }
680
681 func TestHandlerCleanupDoesNotBlockOnStalledWebsocketPeer(t *testing.T) {
682 conn := newStalledWebsocketPeer(t)
683
684 ts := newTunnelTracker(caddy.Log(), 0)
685 shortenCloseControlWriteTimeout(t)
686 ts.registerConnection(conn, func(deadline time.Time) error {
687 return writeCloseControl(conn, false, deadline)
688 }, false, "upstream")
689
690 cleanupDone := make(chan error, 1)
691 go func() { cleanupDone <- ts.cleanupAttachedConnections() }()
692
693 select {
694 case err := <-cleanupDone:
695 if !errors.Is(err, os.ErrDeadlineExceeded) {
696 t.Errorf("cleanup = %v, want %v", err, os.ErrDeadlineExceeded)
697 }
698 case <-time.After(10 * time.Second):
699 t.Fatal("cleanup blocked on a websocket peer that stopped reading")
700 }
701 }
702
703 func TestHandlerCleanupUsesOneDeadlinePerBatch(t *testing.T) {
704 // The close loop is serial and holds the tracker lock, so a deadline per
705 // connection would let every stalled peer add another full timeout.
706 ts := newTunnelTracker(caddy.Log(), 0)
707 shortenCloseControlWriteTimeout(t)
708 var deadlines []time.Time
709 for range 2 {
710 conn := newStalledWebsocketPeer(t)
711 ts.registerConnection(conn, func(deadline time.Time) error {
712 deadlines = append(deadlines, deadline)
713 return writeCloseControl(conn, false, deadline)
714 }, false, "upstream")
715 }
716
717 _ = ts.cleanupAttachedConnections()
718 if len(deadlines) != 2 {
719 t.Fatalf("got %d graceful closes, want 2", len(deadlines))
720 }
721 if !deadlines[0].Equal(deadlines[1]) {
722 t.Errorf("graceful closes got deadlines %v and %v, want one shared deadline", deadlines[0], deadlines[1])
723 }
724 }
725
726 func TestUpstreamRemovalUsesOneDeadlineAcrossTrackers(t *testing.T) {
727 // Removing an upstream notifies every stopped tracker in turn, and each
728 // close loop holds its own lock, so a deadline per tracker would let
729 // every tracker with a stalled peer add another full timeout.
730 const upstream = "upstream-removal-one-deadline"
731 shortenCloseControlWriteTimeout(t)
732 var deadlines []time.Time
733 var trackers []*tunnelTracker
734 for range 2 {
735 ts := newTunnelTracker(caddy.Log(), 0)
736 registerDetachedTunnelTrackers(ts)
737 t.Cleanup(func() { unregisterDetachedTunnelTrackers(ts) })
738 conn := newStalledWebsocketPeer(t)
739 ts.registerConnection(conn, func(deadline time.Time) error {
740 deadlines = append(deadlines, deadline)
741 return writeCloseControl(conn, false, deadline)
742 }, true, upstream)
743 if err := ts.cleanupAttachedConnections(); err != nil {
744 t.Fatal(err)
745 }
746 trackers = append(trackers, ts)
747 }
748
749 _ = notifyDetachedTunnelTrackersOfUpstreamRemoval(upstream, trackers[0])
750 if len(deadlines) != 2 {
751 t.Fatalf("got %d graceful closes, want 2", len(deadlines))
752 }
753 if !deadlines[0].Equal(deadlines[1]) {
754 t.Errorf("trackers got deadlines %v and %v, want one shared deadline", deadlines[0], deadlines[1])
755 }
756 }
757
758 func TestWriteCloseControlSkipsConnectionsWithoutWriteDeadline(t *testing.T) {
759 // A write that cannot be bounded could block forever behind a stalled
760 // peer, so the best-effort close message is skipped instead.
761 var buf bytes.Buffer
762 if err := writeCloseControl(&buf, false, time.Now().Add(time.Second)); err != nil {
763 t.Fatal(err)
764 }
765 if buf.Len() != 0 {
766 t.Errorf("close message %q was written to a connection without write deadlines", buf.Bytes())
767 }
768 }
769
770 func TestExtendedConnectReadWriteCloserSetWriteDeadline(t *testing.T) {
771 rw := &writeDeadlineRecorder{ResponseRecorder: httptest.NewRecorder()}
772 rwc := extendedConnectReadWriteCloser{io.NopCloser(strings.NewReader("")), rw}
773 deadline := time.Now().Add(time.Second)
774 if err := rwc.SetWriteDeadline(deadline); err != nil {
775 t.Fatal(err)
776 }
777 if !rw.writeDeadline.Equal(deadline) {
778 t.Errorf("write deadline = %v, want %v", rw.writeDeadline, deadline)
779 }
780
781 // Writers without deadline support report ErrNotSupported, which
782 // writeCloseControl treats as a reason to skip the close message.
783 plain := extendedConnectReadWriteCloser{io.NopCloser(strings.NewReader("")), httptest.NewRecorder()}
784 if err := plain.SetWriteDeadline(deadline); !errors.Is(err, http.ErrNotSupported) {
785 t.Errorf("SetWriteDeadline on a plain recorder = %v, want ErrNotSupported", err)
786 }
787 }
788
789 type writeDeadlineRecorder struct {
790 *httptest.ResponseRecorder
791 writeDeadline time.Time
792 }
793
794 func (r *writeDeadlineRecorder) SetWriteDeadline(deadline time.Time) error {
795 r.writeDeadline = deadline
796 return nil
797 }
798
799 func TestHandlerUpgradeBoundsUpstreamCloseMessage(t *testing.T) {
800 switch runtime.GOOS {
801 case "plan9", "js", "wasip1":
802 t.Skipf("not supported on %s", runtime.GOOS)
803 }
804
805 // net/http hands upgraded HTTP/1 bodies back inside a type that hides the
806 // socket's deadline methods, so without the connection recorded during
807 // the round trip the close message to the upstream would be skipped.
808 h, cli, srv := newUpgradedTunnel(t)
809 if err := cli.SetDeadline(time.Now().Add(10 * time.Second)); err != nil {
810 t.Fatal(err)
811 }
812 if err := srv.SetDeadline(time.Now().Add(10 * time.Second)); err != nil {
813 t.Fatal(err)
814 }
815
816 // Data reaching the backend proves both connections are registered.
817 if _, err := io.WriteString(cli, "hello"); err != nil {
818 t.Fatal(err)
819 }
820 hello := make([]byte, len("hello"))
821 if _, err := io.ReadFull(srv, hello); err != nil || string(hello) != "hello" {
822 t.Fatalf("backend read %q, %v; want %q", hello, err, "hello")
823 }
824
825 // Closing the client side may tear down the tunnel before cleanup closes
826 // the upstream itself; that must not surface as a cleanup error.
827 if err := h.tunnelTracker.cleanupAttachedConnections(); err != nil {
828 t.Errorf("cleanup = %v, want nil", err)
829 }
830 frame := make([]byte, 2)
831 if _, err := io.ReadFull(srv, frame); err != nil {
832 t.Fatalf("backend did not receive a close message: %v", err)
833 }
834 if frame[0] != 0x88 || frame[1]&0x80 == 0 {
835 t.Fatalf("backend received %x, want a masked close frame", frame)
836 }
837 }
838
839 func TestUpstreamWriteCloser(t *testing.T) {
840 // The HTTP/1 path, including the recorded socket, is covered end to end
841 // by TestHandlerUpgradeBoundsUpstreamCloseMessage. HTTP/2 and HTTP/3 share
842 // their connection between streams, so the close message must never go
843 // to a recorded socket for them.
844 body, _ := net.Pipe()
845 socket, _ := net.Pipe()
846 for _, tc := range []struct {
847 name string
848 protoMajor int
849 recorded net.Conn
850 want io.ReadWriteCloser
851 }{
852 {name: "http/1 with recorded socket", protoMajor: 1, recorded: socket, want: socket},
853 {name: "http/1 without recorded socket", protoMajor: 1, want: body},
854 {name: "http/2", protoMajor: 2, recorded: socket, want: body},
855 {name: "http/3", protoMajor: 3, recorded: socket, want: body},
856 } {
857 t.Run(tc.name, func(t *testing.T) {
858 recorded := new(atomic.Pointer[net.Conn])
859 if tc.recorded != nil {
860 recorded.Store(&tc.recorded)
861 }
862 req := httptest.NewRequest(http.MethodGet, "/", nil)
863 req = req.WithContext(context.WithValue(req.Context(), upstreamConnCtxKey, recorded))
864 res := &http.Response{ProtoMajor: tc.protoMajor}
865 if got := upstreamWriteCloser(req, res, body); got != tc.want {
866 t.Errorf("upstreamWriteCloser() = %v, want %v", got, tc.want)
867 }
868 })
869 }
870 }
871
872 // opaqueUpgradeWriter supports hijacking but hides the recorder from the
873 // detachment helper, as a middleware wrapper without Unwrap would do.
874 type opaqueUpgradeWriter struct {
875 http.ResponseWriter
876 }
877
878 func (w opaqueUpgradeWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
879 return http.NewResponseController(w.ResponseWriter).Hijack()
880 }
881
882 type rejectingUpgradeWriter struct {
883 *caddyhttp.ResponseWriterWrapper
884 }
885
886 func (rejectingUpgradeWriter) DetachAfterHijack(detached bool) bool {
887 return !detached
888 }
889
890 type pipeUpgradeWriter struct {
891 *httptest.ResponseRecorder
892 conn net.Conn
893 }
894
895 func (w pipeUpgradeWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
896 return w.conn, bufio.NewReadWriter(bufio.NewReader(w.conn), bufio.NewWriter(w.conn)), nil
897 }
898
899 func TestUpgradeDetachmentRequiresWriterSupport(t *testing.T) {
900 for _, tc := range []struct {
901 name string
902 wrap func(http.ResponseWriter) http.ResponseWriter
903 detached bool
904 }{
905 {name: "opaque wrapper", wrap: func(w http.ResponseWriter) http.ResponseWriter { return opaqueUpgradeWriter{w} }},
906 {name: "inner writer rejects detachment", wrap: func(w http.ResponseWriter) http.ResponseWriter {
907 rejecting := rejectingUpgradeWriter{&caddyhttp.ResponseWriterWrapper{ResponseWriter: w}}
908 return caddyhttp.NewResponseRecorder(rejecting, nil, nil)
909 }},
910 {name: "supported recorder", wrap: func(w http.ResponseWriter) http.ResponseWriter { return w }, detached: true},
911 } {
912 t.Run(tc.name, func(t *testing.T) {
913 front, client := net.Pipe()
914 back, backend := net.Pipe()
915 defer front.Close()
916 defer client.Close()
917 defer back.Close()
918 defer backend.Close()
919 if err := client.SetDeadline(time.Now().Add(5 * time.Second)); err != nil {
920 t.Fatal(err)
921 }
922 echoDone := make(chan struct{})
923 go func() {
924 defer close(echoDone)
925 _, _ = io.Copy(backend, backend)
926 }()
927 h := &Handler{StreamDetached: true, logger: caddy.Log(), tunnelTracker: newTunnelTracker(caddy.Log(), 0)}
928 initReverseProxyMetrics(h, prometheus.NewRegistry())
929 recorder := caddyhttp.NewResponseRecorder(pipeUpgradeWriter{httptest.NewRecorder(), front}, nil, nil)
930 writer := tc.wrap(recorder)
931 req := caddyhttp.PrepareRequest(httptest.NewRequest(http.MethodGet, "/upgrade", nil), caddy.NewReplacer(), recorder, &caddyhttp.Server{})
932 req.Header.Set("Connection", "Upgrade")
933 req.Header.Set("Upgrade", "test-stream")
934 res := &http.Response{StatusCode: http.StatusSwitchingProtocols, Header: req.Header.Clone(), Body: back}
935 handlerDone := make(chan struct{})
936 go func() {
937 defer close(handlerDone)
938 h.handleUpgradeResponse(caddy.Log(), writer, req, res, "detachment-test")
939 }()
940 const payload = "echo"
941 if _, err := io.WriteString(client, payload); err != nil {
942 t.Fatal(err)
943 }
944 received := make([]byte, len(payload))
945 if _, err := io.ReadFull(client, received); err != nil {
946 t.Fatal(err)
947 }
948 if string(received) != payload {
949 t.Fatalf("echo = %q, want %q", received, payload)
950 }
951 h.tunnelTracker.mu.Lock()
952 for _, conn := range h.tunnelTracker.connections {
953 if conn.detached != tc.detached {
954 t.Errorf("tracked detached = %v, want %v", conn.detached, tc.detached)
955 }
956 }
957 h.tunnelTracker.mu.Unlock()
958 if tc.detached {
959 select {
960 case <-handlerDone:
961 case <-time.After(time.Second):
962 t.Error("supported detached tunnel kept the handler running")
963 }
964 } else {
965 select {
966 case <-handlerDone:
967 t.Error("handler returned while the writer chain remained attached")
968 case <-time.After(100 * time.Millisecond):
969 }
970 }
971 client.Close()
972 backend.Close()
973 select {
974 case <-handlerDone:
975 case <-time.After(time.Second):
976 t.Fatal("handler did not finish after stream close")
977 }
978 <-echoDone
979 // Wait for detached tunnel cleanup too, so no copy goroutine can
980 // still be updating counters after this subtest finishes.
981 deadline := time.Now().Add(time.Second)
982 for {
983 h.tunnelTracker.mu.Lock()
984 remaining := len(h.tunnelTracker.connections)
985 h.tunnelTracker.mu.Unlock()
986 if remaining == 0 {
987 break
988 }
989 if time.Now().After(deadline) {
990 t.Fatal("tunnel connections were not unregistered")
991 }
992 time.Sleep(time.Millisecond)
993 }
994 // Attached byte counters are safe to inspect after the handler has waited
995 // for both copying goroutines. Partial detachment must also be undone.
996 if !tc.detached {
997 recorders := []caddyhttp.ResponseRecorder{recorder}
998 if outer, ok := writer.(caddyhttp.ResponseRecorder); ok {
999 recorders = append(recorders, outer)
1000 }
1001 for _, rr := range recorders {
1002 if rr.Size() != len(payload) {
1003 t.Errorf("attached recorder size = %d, want %d", rr.Size(), len(payload))
1004 }
1005 }
1006 }
1007 })
1008 }
1009 }
1010
1011 func TestUpgradeResponseCleanupOnFailure(t *testing.T) {
1012 body := &closeTrackingBody{Buffer: bytes.NewBufferString("test body")}
1013
1014 res := &http.Response{
1015 StatusCode: http.StatusSwitchingProtocols,
1016 Header: make(http.Header),
1017 Body: body,
1018 }
1019 res.Header.Set("Upgrade", "websocket")
1020 res.Header.Set("Connection", "Upgrade")
1021
1022 // Request with mismatched upgrade type -> should fail check and close body
1023 req, err := http.NewRequest("GET", "http://example.com", nil)
1024 if err != nil {
1025 t.Fatal(err)
1026 }
1027 req.Header.Set("Upgrade", "something-else")
1028 req.Header.Set("Connection", "Upgrade")
1029
1030 recorder := httptest.NewRecorder()
1031 h := &Handler{}
1032 logger := zap.NewNop()
1033
1034 h.handleUpgradeResponse(logger, recorder, req, res, "127.0.0.1:8080")
1035
1036 if !body.closed {
1037 t.Errorf("expected backend response body to be closed on upgrade protocol mismatch, but it was not")
1038 }
1039 }
1040
1041 func TestUpgradeResponseCleanupAfterHijack(t *testing.T) {
1042 for _, tc := range []struct {
1043 name string
1044 flushFailure bool
1045 }{
1046 {name: "handshake flush failure", flushFailure: true},
1047 {name: "buffered backend write failure"},
1048 } {
1049 t.Run(tc.name, func(t *testing.T) {
1050 front, client := net.Pipe()
1051 back, backend := net.Pipe()
1052 for _, conn := range []net.Conn{front, client, back, backend} {
1053 t.Cleanup(func() { conn.Close() })
1054 }
1055
1056 writeFailure := &upgradeWriteErrorConn{writeErr: errors.New("upgrade setup write failed")}
1057 reader := bufio.NewReader(strings.NewReader(""))
1058 writer := bufio.NewWriter(front)
1059 var body io.ReadWriteCloser = back
1060 var wantWrite string
1061 if tc.flushFailure {
1062 writeFailure.Conn = front
1063 writer = bufio.NewWriter(writeFailure)
1064 wantWrite = "HTTP/1.1 101 Switching Protocols\r\n\r\n"
1065 // Flush must have pending bytes; flushing an empty writer
1066 // never reaches the failing connection's Write method.
1067 if _, err := writer.WriteString(wantWrite); err != nil {
1068 t.Fatal(err)
1069 }
1070 } else {
1071 writeFailure.Conn = back
1072 body = writeFailure
1073 wantWrite = "buffered client data"
1074 reader = bufio.NewReader(strings.NewReader(wantWrite))
1075 // Buffered() must be nonzero before the handler checks it.
1076 if _, err := reader.Peek(len(wantWrite)); err != nil {
1077 t.Fatal(err)
1078 }
1079 }
1080
1081 rw := &upgradeFailureHijacker{
1082 ResponseRecorder: httptest.NewRecorder(),
1083 conn: front,
1084 brw: bufio.NewReadWriter(reader, writer),
1085 }
1086 req := prepareTestRequest(httptest.NewRequest(http.MethodGet, "/upgrade", nil))
1087 req.Header.Set("Connection", "Upgrade")
1088 req.Header.Set("Upgrade", "test-stream")
1089 res := &http.Response{StatusCode: http.StatusSwitchingProtocols, Header: req.Header.Clone(), Body: body}
1090 logger := zap.NewNop()
1091 h := &Handler{logger: logger, tunnelTracker: newTunnelTracker(logger, 0)}
1092 for _, peer := range []net.Conn{client, backend} {
1093 if err := peer.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
1094 t.Fatal(err)
1095 }
1096 }
1097
1098 h.handleUpgradeResponse(logger, rw, req, res, "cleanup-test")
1099
1100 if !rw.hijacked {
1101 t.Fatal("setup failed before successful client hijack")
1102 }
1103 if writeFailure.writeCalls != 1 || string(writeFailure.written) != wantWrite {
1104 t.Fatalf("failure injection: got %d writes of %q, want one write of %q",
1105 writeFailure.writeCalls, writeFailure.written, wantWrite)
1106 }
1107 for _, peer := range []struct {
1108 name string
1109 conn net.Conn
1110 }{
1111 {name: "client", conn: client},
1112 {name: "backend", conn: backend},
1113 } {
1114 var data [1]byte
1115 if n, err := peer.conn.Read(data[:]); n != 0 || !errors.Is(err, io.EOF) {
1116 t.Errorf("%s peer read = (%d, %v), want (0, EOF) after setup failure", peer.name, n, err)
1117 }
1118 }
1119 if remaining := len(h.tunnelTracker.connections); remaining != 0 {
1120 t.Errorf("setup failure registered %d tunnel connections", remaining)
1121 }
1122 })
1123 }
1124 }
1125
1126 type upgradeFailureHijacker struct {
1127 *httptest.ResponseRecorder
1128 conn net.Conn
1129 brw *bufio.ReadWriter
1130 hijacked bool
1131 }
1132
1133 func (w *upgradeFailureHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) {
1134 w.hijacked = true
1135 return w.conn, w.brw, nil
1136 }
1137
1138 type upgradeWriteErrorConn struct {
1139 net.Conn
1140 writeErr error
1141 writeCalls int
1142 written []byte
1143 }
1144
1145 func (c *upgradeWriteErrorConn) Write(p []byte) (int, error) {
1146 c.writeCalls++
1147 c.written = append(c.written, p...)
1148 return 0, c.writeErr
1149 }
1150
1151 type closeTrackingBody struct {
1152 *bytes.Buffer
1153 closed bool
1154 }
1155
1156 func (c *closeTrackingBody) Close() error {
1157 c.closed = true
1158 return nil
1159 }
1160
1161 func TestTunnelTrackerSendsCloseMessagesBeforeClosing(t *testing.T) {
1162 // Closing one side of a tunnel tears down the other, so every close
1163 // message in a batch must go out before any connection is closed.
1164 for _, tc := range []struct {
1165 name string
1166 detached bool
1167 close func(*tunnelTracker) error
1168 }{
1169 {name: "attached", close: (*tunnelTracker).cleanupAttachedConnections},
1170 {name: "detached", detached: true, close: func(ts *tunnelTracker) error {
1171 // closeConnectionsForUpstream only acts on a stopped tracker.
1172 ts.stopped = true
1173 return ts.closeConnectionsForUpstream("upstream", time.Now().Add(time.Second))
1174 }},
1175 } {
1176 t.Run(tc.name, func(t *testing.T) {
1177 ts := newTunnelTracker(caddy.Log(), 0)
1178 var events []string
1179 for _, side := range []string{"client", "upstream"} {
1180 ts.registerConnection(closeRecorder{side: side, events: &events}, func(time.Time) error {
1181 events = append(events, "close message to "+side)
1182 return nil
1183 }, tc.detached, "upstream")
1184 }
1185
1186 if err := tc.close(ts); err != nil {
1187 t.Fatal(err)
1188 }
1189 if len(events) != 4 || !strings.HasPrefix(events[0], "close message") || !strings.HasPrefix(events[1], "close message") {
1190 t.Errorf("events = %q, want both close messages before any connection closes", events)
1191 }
1192 })
1193 }
1194 }
1195
1196 // closeRecorder records when it is closed.
1197 type closeRecorder struct {
1198 nopReadWriteCloser
1199 side string
1200 events *[]string
1201 }
1202
1203 func (c closeRecorder) Close() error {
1204 *c.events = append(*c.events, "closed "+c.side)
1205 return nil
1206 }