-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Expand file tree
/
Copy pathwebsocket.go
More file actions
101 lines (91 loc) · 2.89 KB
/
Copy pathwebsocket.go
File metadata and controls
101 lines (91 loc) · 2.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
package testutil
import (
"bufio"
"context"
"io"
"net"
"net/http"
"cdr.dev/slog/v3"
)
// InMemWebsocketRoundTripper allows you to "dial" an HTTP handler that sets up a websocket using only in-memory
// primitives. No TCP or OS networking needed. CtxMutator gives you explicit control over the context the handler sees.
//
// Example:
//
// rt := testutil.InMemWebsocketRoundTripper{
// Handler: MyHandler,
// CtxMutator: func(ctx context.Context) context.Context {
// ctx = httpmw.WithWorkspaceParam(ctx, ws)
// ctx = dbauthz.As(ctx, mySubject(userID, orgID))
// return ctx
// },
// Logger: logger.Named("roundtripper"),
// }
// clientSock, _, err := websocket.Dial(ctx, "wss://local.test/", &websocket.DialOptions{
// HTTPClient: &http.Client{Transport: rt},
// })
type InMemWebsocketRoundTripper struct {
Logger slog.Logger
Handler http.Handler
CtxMutator func(ctx context.Context) context.Context
}
func (i InMemWebsocketRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
i.Logger.Debug(context.Background(), "round trip start")
defer i.Logger.Debug(context.Background(), "round trip end")
newCtx := i.CtxMutator(request.Context())
request = request.WithContext(newCtx)
serverP, clientP := net.Pipe()
var _ io.ReadWriteCloser = clientP // compile time check that response body is OK for websocket
response := &http.Response{
Header: make(http.Header),
Body: clientP,
}
rw := newInMemWebsocketResponseWriter(response, serverP)
go func() {
i.Handler.ServeHTTP(rw, request)
if !rw.hijacked {
i.Logger.Debug(context.Background(), "closing connection after handler did not hijack")
// If the handler didn't hijack the connection, we should close it when the handler finishes.
// This prevents a 3s delay in websocket.Dial() reading the non-upgraded response.
_ = serverP.Close()
}
}()
select {
case <-newCtx.Done():
return nil, newCtx.Err()
case <-rw.gotHeaders:
return response, nil
}
}
func newInMemWebsocketResponseWriter(resp *http.Response, conn net.Conn) *inMemWebsocketResponseWriter {
r := bufio.NewReader(conn)
w := bufio.NewWriter(conn)
return &inMemWebsocketResponseWriter{
r: resp,
b: bufio.NewReadWriter(r, w),
gotHeaders: make(chan struct{}),
conn: conn,
}
}
type inMemWebsocketResponseWriter struct {
r *http.Response
b *bufio.ReadWriter
gotHeaders chan struct{}
hijacked bool
conn net.Conn
}
func (rw *inMemWebsocketResponseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
rw.hijacked = true
return rw.conn, rw.b, nil
}
func (rw *inMemWebsocketResponseWriter) Header() http.Header {
return rw.r.Header
}
func (rw *inMemWebsocketResponseWriter) Write([]byte) (int, error) {
n, err := rw.b.Write([]byte{})
return n, err
}
func (rw *inMemWebsocketResponseWriter) WriteHeader(statusCode int) {
rw.r.StatusCode = statusCode
close(rw.gotHeaders)
}