17
"time"
18
19
"github.com/prometheus/client_golang/prometheus"
20
+ "go.uber.org/zap"
21
22
"github.com/caddyserver/caddy/v2"
23
"github.com/caddyserver/caddy/v2/caddyconfig/caddyfile"
782
})
783
}
784
}
785
+
786
+func TestUpgradeResponseCleanupOnFailure(t *testing.T) {
787
+ body := &closeTrackingBody{Buffer: bytes.NewBufferString("test body")}
788
+
789
+ res := &http.Response{
790
+ StatusCode: http.StatusSwitchingProtocols,
791
+ Header: make(http.Header),
792
+ Body: body,
793
+ }
794
+ res.Header.Set("Upgrade", "websocket")
795
+ res.Header.Set("Connection", "Upgrade")
796
+
797
+ // Request with mismatched upgrade type -> should fail check and close body
798
+ req, err := http.NewRequest("GET", "http://example.com", nil)
799
+ if err != nil {
800
+ t.Fatal(err)
801
+ }
802
+ req.Header.Set("Upgrade", "something-else")
803
+ req.Header.Set("Connection", "Upgrade")
804
+
805
+ recorder := httptest.NewRecorder()
806
+ h := &Handler{}
807
+ logger := zap.NewNop()
808
+
809
+ h.handleUpgradeResponse(logger, recorder, req, res, "127.0.0.1:8080")
810
+
811
+ if !body.closed {
812
+ t.Errorf("expected backend response body to be closed on upgrade protocol mismatch, but it was not")
813
+ }
814
+}
815
+
816
+func TestUpgradeResponseCleanupAfterHijack(t *testing.T) {
817
+ for _, tc := range []struct {
818
+ name string
819
+ flushFailure bool
820
+ }{
821
+ {name: "handshake flush failure", flushFailure: true},
822
+ {name: "buffered backend write failure"},
823
+ } {
824
+ t.Run(tc.name, func(t *testing.T) {
825
+ front, client := net.Pipe()
826
+ back, backend := net.Pipe()
827
+ for _, conn := range []net.Conn{front, client, back, backend} {
828
+ t.Cleanup(func() { conn.Close() })
829
+ }
830
+
831
+ writeFailure := &upgradeWriteErrorConn{writeErr: errors.New("upgrade setup write failed")}
832
+ reader := bufio.NewReader(strings.NewReader(""))
833
+ writer := bufio.NewWriter(front)
834
+ var body io.ReadWriteCloser = back
835
+ var wantWrite string
836
+ if tc.flushFailure {
837
+ writeFailure.Conn = front
838
+ writer = bufio.NewWriter(writeFailure)
839
+ wantWrite = "HTTP/1.1 101 Switching Protocols\r\n\r\n"
840
+ // Flush must have pending bytes; flushing an empty writer
841
+ // never reaches the failing connection's Write method.
842
+ if _, err := writer.WriteString(wantWrite); err != nil {
843
+ t.Fatal(err)
844
+ }
845
+ } else {
846
+ writeFailure.Conn = back
847
+ body = writeFailure
848
+ wantWrite = "buffered client data"
849
+ reader = bufio.NewReader(strings.NewReader(wantWrite))
850
+ // Buffered() must be nonzero before the handler checks it.
851
+ if _, err := reader.Peek(len(wantWrite)); err != nil {
852
+ t.Fatal(err)
853
+ }
854
+ }
855
+
856
+ rw := &upgradeFailureHijacker{
857
+ ResponseRecorder: httptest.NewRecorder(),
858
+ conn: front,
859
+ brw: bufio.NewReadWriter(reader, writer),
860
+ }
861
+ req := prepareTestRequest(httptest.NewRequest(http.MethodGet, "/upgrade", nil))
862
+ req.Header.Set("Connection", "Upgrade")
863
+ req.Header.Set("Upgrade", "test-stream")
864
+ res := &http.Response{StatusCode: http.StatusSwitchingProtocols, Header: req.Header.Clone(), Body: body}
865
+ logger := zap.NewNop()
866
+ h := &Handler{logger: logger, tunnelTracker: newTunnelTracker(logger, 0)}
867
+ for _, peer := range []net.Conn{client, backend} {
868
+ if err := peer.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
869
+ t.Fatal(err)
870
+ }
871
+ }
872
+
873
+ h.handleUpgradeResponse(logger, rw, req, res, "cleanup-test")
874
+
875
+ if !rw.hijacked {
876
+ t.Fatal("setup failed before successful client hijack")
877
+ }
878
+ if writeFailure.writeCalls != 1 || string(writeFailure.written) != wantWrite {
879
+ t.Fatalf("failure injection: got %d writes of %q, want one write of %q",
880
+ writeFailure.writeCalls, writeFailure.written, wantWrite)
881
+ }
882
+ for _, peer := range []struct {
883
+ name string
884
+ conn net.Conn
885
+ }{
886
+ {name: "client", conn: client},
887
+ {name: "backend", conn: backend},
888
+ } {
889
+ var data [1]byte
890
+ if n, err := peer.conn.Read(data[:]); n != 0 || !errors.Is(err, io.EOF) {
891
+ t.Errorf("%s peer read = (%d, %v), want (0, EOF) after setup failure", peer.name, n, err)
892
+ }
893
+ }
894
+ if remaining := len(h.tunnelTracker.connections); remaining != 0 {
895
+ t.Errorf("setup failure registered %d tunnel connections", remaining)
896
+ }
897
+ })
898
+ }
899
+}
900
+
901
+type upgradeFailureHijacker struct {
902
+ *httptest.ResponseRecorder
903
+ conn net.Conn
904
+ brw *bufio.ReadWriter
905
+ hijacked bool
906
+}
907
+
908
+func (w *upgradeFailureHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) {
909
+ w.hijacked = true
910
+ return w.conn, w.brw, nil
911
+}
912
+
913
+type upgradeWriteErrorConn struct {
914
+ net.Conn
915
+ writeErr error
916
+ writeCalls int
917
+ written []byte
918
+}
919
+
920
+func (c *upgradeWriteErrorConn) Write(p []byte) (int, error) {
921
+ c.writeCalls++
922
+ c.written = append(c.written, p...)
923
+ return 0, c.writeErr
924
+}
925
+
926
+type closeTrackingBody struct {
927
+ *bytes.Buffer
928
+ closed bool
929
+}
930
+
931
+func (c *closeTrackingBody) Close() error {
932
+ c.closed = true
933
+ return nil
934
+}