2
3
import (
4
"bufio"
5
+ "bytes"
6
+ "crypto/rand"
7
+ "crypto/sha1"
8
+ "encoding/base64"
9
"errors"
10
"fmt"
11
"io"
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
)
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) {
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) {
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) {
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
+}