@marcel91200 / caddy Mirror · read-only / commits / 9e5d4d42
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, 02:15 UTC

reverseproxy: test WebSocket upgrades over Unix sockets (#7657)

* reverseproxy: Test that WebSockets + unix-sockets works Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Close temp file before reusing its path for the socket Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Strengthen unix socket WebSocket test, reuse tempSocketName helper Check that upstream bytes sent with the 101 are delivered, echo a payload larger than any buffer, and verify the upstream connection closes when the client disconnects. Reuse the existing tempSocketName helper for all unix socket tests, which also closes the temp file descriptor the older tests leaked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

Francis Lavoie committed Oct 31, 2026 at 22:22 UTC 9e5d4d42b1c3995e4b27106b339d7d1188f16dcd
1 file changed +160 -25
caddytest/integration/reverseproxy_test.go
+160 -25
@@ -2,6 +2,10 @@ package integration
2
3 import (
4 "bufio"
5 + "bytes"
6 + "crypto/rand"
7 + "crypto/sha1"
8 + "encoding/base64"
9 "errors"
10 "fmt"
11 "io"
@@ -9,11 +13,11 @@ import (
13 "net/http"
14 "net/http/httptest"
15 "net/textproto"
12 - "os"
16 "runtime"
17 "strings"
18 "sync/atomic"
19 "testing"
20 + "time"
21
22 "github.com/caddyserver/caddy/v2/caddytest"
23 )
@@ -66,14 +70,7 @@ func TestDialWithPlaceholderUnix(t *testing.T) {
70 t.SkipNow()
71 }
72
69 - f, err := os.CreateTemp("", "*.sock")
70 - if err != nil {
71 - t.Errorf("failed to create TempFile: %s", err)
72 - return
73 - }
74 - // a hack to get a file name within a valid path to use as socket
75 - socketName := f.Name()
76 - os.Remove(f.Name())
73 + socketName := tempSocketName(t)
74
75 server := http.Server{
76 Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
@@ -459,14 +456,7 @@ func TestReverseProxyHealthCheckUnixSocket(t *testing.T) {
456 t.SkipNow()
457 }
458 tester := caddytest.NewTester(t)
462 - f, err := os.CreateTemp("", "*.sock")
463 - if err != nil {
464 - t.Errorf("failed to create TempFile: %s", err)
465 - return
466 - }
467 - // a hack to get a file name within a valid path to use as socket
468 - socketName := f.Name()
469 - os.Remove(f.Name())
459 + socketName := tempSocketName(t)
460
461 server := http.Server{
462 Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
@@ -517,14 +507,7 @@ func TestReverseProxyHealthCheckUnixSocketWithoutPort(t *testing.T) {
507 t.SkipNow()
508 }
509 tester := caddytest.NewTester(t)
520 - f, err := os.CreateTemp("", "*.sock")
521 - if err != nil {
522 - t.Errorf("failed to create TempFile: %s", err)
523 - return
524 - }
525 - // a hack to get a file name within a valid path to use as socket
526 - socketName := f.Name()
527 - os.Remove(f.Name())
510 + socketName := tempSocketName(t)
511
512 server := http.Server{
513 Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
@@ -1153,3 +1136,155 @@ func TestReverseProxyUnlimitedRequestBuffersRespectMaxSize(t *testing.T) {
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 +}