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