@marcel91200 / caddy Mirror · read-only / commits / 7ee4441f
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, 04:45 UTC

reverseproxy: Move websocket header normalization later (#7921)

* Move websocket header normalization later A regression since #6621, these headers can be rewritten by cloneHeaders and by .Add from user and transport ops. Moving this closer to where it's round-tripped feels like the nicest solution. I've put the harness I used to reproduce the issue at https://codeberg.org/nfreya/caddy-websocket-headers-repro * Integration test for normalized websocket headers --------- Co-authored-by: Zen Dodd <mail@steadytao.com> Co-authored-by: Matt Holt <mholt@users.noreply.github.com>

freya committed Sep 23, 2026 at 15:09 UTC 7ee4441f9261d649eed215f27331352c1ab5a746
2 files changed +106 -1
caddytest/integration/reverseproxy_test.go
+101
@@ -1,9 +1,13 @@
1 package integration
2
3 import (
4 + "bufio"
5 + "errors"
6 "fmt"
7 + "io"
8 "net"
9 "net/http"
10 + "net/textproto"
11 "os"
12 "runtime"
13 "strings"
@@ -832,6 +836,103 @@ func TestReverseProxySNIPlaceHolder(t *testing.T) {
836 }
837 }
838
839 +func TestReverseProxyNormalizeWebSocketHeaders(t *testing.T) {
840 + // 503s if it doesn't see "WebSocket" in a request
841 + ln, err := net.Listen("tcp", "127.0.0.1:0")
842 + if err != nil {
843 + t.Fatalf("failed to listen: %v", err)
844 + }
845 + // http.Server would canonicalize these headers, so plain tcp:
846 + errc := make(chan error, 1)
847 + defer func() {
848 + for range errc {
849 + }
850 + }()
851 + defer ln.Close()
852 + handleConn := func(conn net.Conn) error {
853 + defer conn.Close()
854 + rd := textproto.NewReader(bufio.NewReader(io.LimitReader(conn, 4096)))
855 + status := 503
856 + for {
857 + line, err := rd.ReadLine()
858 + if err != nil {
859 + return fmt.Errorf("failed to read line: %v", err)
860 + }
861 + if len(line) == 0 {
862 + break
863 + }
864 + if strings.Contains(line, "WebSocket") {
865 + status = 200
866 + }
867 + }
868 + res := fmt.Sprintf("HTTP/1.1 %d %s\r\nConnection: close\r\n\r\n", status, http.StatusText(status))
869 + if _, err = conn.Write([]byte(res)); err != nil {
870 + return fmt.Errorf("failed to write: %v", err)
871 + }
872 + return nil
873 + }
874 + go func() {
875 + defer close(errc)
876 + for {
877 + conn, err := ln.Accept()
878 + if err != nil {
879 + if !errors.Is(err, net.ErrClosed) {
880 + errc <- fmt.Errorf("failed to accept: %v", err)
881 + }
882 + return
883 + }
884 + if err := handleConn(conn); err != nil {
885 + errc <- fmt.Errorf("failed to handle: %v", err)
886 + }
887 + }
888 + }()
889 + checkServer := func() {
890 + select {
891 + case err := <-errc:
892 + t.Fatalf("failed to serve test request: %v", err)
893 + default:
894 + }
895 + }
896 + tester := caddytest.NewTester(t)
897 + tester.InitServer(fmt.Sprintf(`
898 + {
899 + skip_install_trust
900 + local_certs
901 + admin localhost:2999
902 + http_port 9080
903 + https_port 9443
904 + grace_period 1ns
905 + }
906 + http://localhost:9080 {
907 + reverse_proxy http://%s {
908 + # test compatibility with header manipulations
909 + header_up X-Add-Header "1"
910 + }
911 + }`, ln.Addr()), "caddyfile")
912 + req, err := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil)
913 + if err != nil {
914 + t.Fatalf("failed to create request: %v", err)
915 + }
916 + // Only the first WebSocket header needs the manual assignment,
917 + // but mixing in .Set is likely to be more confusing.
918 + req.Header["Connection"] = []string{"Upgrade"}
919 + req.Header["Upgrade"] = []string{"websocket"}
920 + // 1. resembling a browser via http/1.1
921 + req.Header["Sec-WebSocket-Version"] = []string{"13"}
922 + tester.AssertResponse(req, 200, "")
923 + checkServer()
924 + delete(req.Header, "Sec-WebSocket-Version")
925 + // 2. canonicalized is okay
926 + req.Header["Sec-Websocket-Version"] = []string{"13"}
927 + tester.AssertResponse(req, 200, "")
928 + checkServer()
929 + delete(req.Header, "Sec-Websocket-Version")
930 + // 3. unknown header doesn't receive special handling
931 + req.Header["Sec-Websocket-Unnormalizedkey"] = []string{"13"}
932 + tester.AssertResponse(req, 503, "")
933 + checkServer()
934 +}
935 +
936 func TestWeightedRoundRobinSelectionValidation(t *testing.T) {
937 configTemplate := `
938 {
modules/caddyhttp/reverseproxy/reverseproxy.go
+5 -1
@@ -692,6 +692,11 @@ func (h *Handler) proxyLoopIteration(r *http.Request, origReq *http.Request, w h
692 }
693 }
694
695 + // normalize websocket headers for compatibility with older servers
696 + if upgradeType(r.Header) != "" {
697 + normalizeWebsocketHeaders(r.Header)
698 + }
699 +
700 // proxy the request to that upstream
701 proxyErr = h.reverseProxy(w, r, origReq, repl, dialInfo, next)
702 if proxyErr == nil {
@@ -840,7 +845,6 @@ func (h Handler) prepareRequest(req *http.Request, repl *caddy.Replacer) (*http.
845 if reqUpgradeType != "" {
846 req.Header.Set("Connection", "Upgrade")
847 req.Header.Set("Upgrade", reqUpgradeType)
843 - normalizeWebsocketHeaders(req.Header)
848 }
849
850 // Set up the PROXY protocol info