Skip to content

Commit 2c44f28

Browse files
authored
Merge pull request #222 from nhooyr/dev
Update master with dev
2 parents 66eb672 + 1d80cf3 commit 2c44f28

28 files changed

+781
-291
lines changed

README.md

+21-20
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# websocket
22

3-
[![godoc](https://godoc.org/nhooyr.io/websocket?status.svg)](https://godoc.org/nhooyr.io/websocket)
3+
[![godoc](https://godoc.org/nhooyr.io/websocket?status.svg)](https://pkg.go.dev/nhooyr.io/websocket)
44

55
websocket is a minimal and idiomatic WebSocket library for Go.
66

@@ -15,26 +15,27 @@ go get nhooyr.io/websocket
1515
- Minimal and idiomatic API
1616
- First class [context.Context](https://blog.golang.org/context) support
1717
- Fully passes the WebSocket [autobahn-testsuite](https://github.com/crossbario/autobahn-testsuite)
18-
- Thorough unit tests with [90% coverage](https://coveralls.io/github/nhooyr/websocket)
19-
- [Minimal dependencies](https://godoc.org/nhooyr.io/websocket?imports)
20-
- JSON and protobuf helpers in the [wsjson](https://godoc.org/nhooyr.io/websocket/wsjson) and [wspb](https://godoc.org/nhooyr.io/websocket/wspb) subpackages
18+
- Thorough tests with [90% coverage](https://coveralls.io/github/nhooyr/websocket)
19+
- [Minimal dependencies](https://pkg.go.dev/nhooyr.io/websocket?tab=imports)
20+
- JSON and protobuf helpers in the [wsjson](https://pkg.go.dev/nhooyr.io/websocket/wsjson) and [wspb](https://pkg.go.dev/nhooyr.io/websocket/wspb) subpackages
2121
- Zero alloc reads and writes
2222
- Concurrent writes
23-
- [Close handshake](https://godoc.org/nhooyr.io/websocket#Conn.Close)
24-
- [net.Conn](https://godoc.org/nhooyr.io/websocket#NetConn) wrapper
25-
- [Ping pong](https://godoc.org/nhooyr.io/websocket#Conn.Ping) API
23+
- [Close handshake](https://pkg.go.dev/nhooyr.io/websocket#Conn.Close)
24+
- [net.Conn](https://pkg.go.dev/nhooyr.io/websocket#NetConn) wrapper
25+
- [Ping pong](https://pkg.go.dev/nhooyr.io/websocket#Conn.Ping) API
2626
- [RFC 7692](https://tools.ietf.org/html/rfc7692) permessage-deflate compression
27-
- Compile to [Wasm](https://godoc.org/nhooyr.io/websocket#hdr-Wasm)
27+
- Compile to [Wasm](https://pkg.go.dev/nhooyr.io/websocket#hdr-Wasm)
2828

2929
## Roadmap
3030

3131
- [ ] HTTP/2 [#4](https://github.com/nhooyr/websocket/issues/4)
3232

3333
## Examples
3434

35-
For a production quality example that demonstrates the complete API, see the [echo example](https://godoc.org/nhooyr.io/websocket#example-package--Echo).
35+
For a production quality example that demonstrates the complete API, see the
36+
[echo example](./examples/echo).
3637

37-
For a full stack example, see [./chat-example](./chat-example).
38+
For a full stack example, see the [chat example](./examples/chat).
3839

3940
### Server
4041

@@ -88,39 +89,39 @@ c.Close(websocket.StatusNormalClosure, "")
8889
Advantages of [gorilla/websocket](https://github.com/gorilla/websocket):
8990

9091
- Mature and widely used
91-
- [Prepared writes](https://godoc.org/github.com/gorilla/websocket#PreparedMessage)
92-
- Configurable [buffer sizes](https://godoc.org/github.com/gorilla/websocket#hdr-Buffers)
92+
- [Prepared writes](https://pkg.go.dev/github.com/gorilla/websocket#PreparedMessage)
93+
- Configurable [buffer sizes](https://pkg.go.dev/github.com/gorilla/websocket#hdr-Buffers)
9394

9495
Advantages of nhooyr.io/websocket:
9596

9697
- Minimal and idiomatic API
97-
- Compare godoc of [nhooyr.io/websocket](https://godoc.org/nhooyr.io/websocket) with [gorilla/websocket](https://godoc.org/github.com/gorilla/websocket) side by side.
98-
- [net.Conn](https://godoc.org/nhooyr.io/websocket#NetConn) wrapper
98+
- Compare godoc of [nhooyr.io/websocket](https://pkg.go.dev/nhooyr.io/websocket) with [gorilla/websocket](https://pkg.go.dev/github.com/gorilla/websocket) side by side.
99+
- [net.Conn](https://pkg.go.dev/nhooyr.io/websocket#NetConn) wrapper
99100
- Zero alloc reads and writes ([gorilla/websocket#535](https://github.com/gorilla/websocket/issues/535))
100101
- Full [context.Context](https://blog.golang.org/context) support
101102
- Dial uses [net/http.Client](https://golang.org/pkg/net/http/#Client)
102103
- Will enable easy HTTP/2 support in the future
103104
- Gorilla writes directly to a net.Conn and so duplicates features of net/http.Client.
104105
- Concurrent writes
105106
- Close handshake ([gorilla/websocket#448](https://github.com/gorilla/websocket/issues/448))
106-
- Idiomatic [ping pong](https://godoc.org/nhooyr.io/websocket#Conn.Ping) API
107+
- Idiomatic [ping pong](https://pkg.go.dev/nhooyr.io/websocket#Conn.Ping) API
107108
- Gorilla requires registering a pong callback before sending a Ping
108109
- Can target Wasm ([gorilla/websocket#432](https://github.com/gorilla/websocket/issues/432))
109-
- Transparent message buffer reuse with [wsjson](https://godoc.org/nhooyr.io/websocket/wsjson) and [wspb](https://godoc.org/nhooyr.io/websocket/wspb) subpackages
110+
- Transparent message buffer reuse with [wsjson](https://pkg.go.dev/nhooyr.io/websocket/wsjson) and [wspb](https://pkg.go.dev/nhooyr.io/websocket/wspb) subpackages
110111
- [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster WebSocket masking implementation in pure Go
111112
- Gorilla's implementation is slower and uses [unsafe](https://golang.org/pkg/unsafe/).
112113
- Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support
113114
- Gorilla only supports no context takeover mode
114-
- We use [klauspost/compress](https://github.com/klauspost/compress) for much lower memory usage ([gorilla/websocket#203](https://github.com/gorilla/websocket/issues/203))
115-
- [CloseRead](https://godoc.org/nhooyr.io/websocket#Conn.CloseRead) helper ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492))
115+
- We use a vendored [klauspost/compress](https://github.com/klauspost/compress) for much lower memory usage ([gorilla/websocket#203](https://github.com/gorilla/websocket/issues/203))
116+
- [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492))
116117
- Actively maintained ([gorilla/websocket#370](https://github.com/gorilla/websocket/issues/370))
117118

118119
#### golang.org/x/net/websocket
119120

120-
[golang.org/x/net/websocket](https://godoc.org/golang.org/x/net/websocket) is deprecated.
121+
[golang.org/x/net/websocket](https://pkg.go.dev/golang.org/x/net/websocket) is deprecated.
121122
See [golang/go/issues/18152](https://github.com/golang/go/issues/18152).
122123

123-
The [net.Conn](https://godoc.org/nhooyr.io/websocket#NetConn) wrapper will ease in transitioning
124+
The [net.Conn](https://pkg.go.dev/nhooyr.io/websocket#NetConn) can help in transitioning
124125
to nhooyr.io/websocket.
125126

126127
#### gobwas/ws

accept.go

+47-26
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,11 @@ import (
99
"errors"
1010
"fmt"
1111
"io"
12+
"log"
1213
"net/http"
1314
"net/textproto"
1415
"net/url"
15-
"strconv"
16+
"path/filepath"
1617
"strings"
1718

1819
"nhooyr.io/websocket/internal/errd"
@@ -25,18 +26,27 @@ type AcceptOptions struct {
2526
// reject it, close the connection when c.Subprotocol() == "".
2627
Subprotocols []string
2728

28-
// InsecureSkipVerify disables Accept's origin verification behaviour. By default,
29-
// the connection will only be accepted if the request origin is equal to the request
30-
// host.
29+
// InsecureSkipVerify is used to disable Accept's origin verification behaviour.
3130
//
32-
// This is only required if you want javascript served from a different domain
33-
// to access your WebSocket server.
31+
// Deprecated: Use OriginPatterns with a match all pattern of * instead to control
32+
// origin authorization yourself.
33+
InsecureSkipVerify bool
34+
35+
// OriginPatterns lists the host patterns for authorized origins.
36+
// The request host is always authorized.
37+
// Use this to enable cross origin WebSockets.
38+
//
39+
// i.e javascript running on example.com wants to access a WebSocket server at chat.example.com.
40+
// In such a case, example.com is the origin and chat.example.com is the request host.
41+
// One would set this field to []string{"example.com"} to authorize example.com to connect.
3442
//
35-
// See https://stackoverflow.com/a/37837709/4283659
43+
// Each pattern is matched case insensitively against the request origin host
44+
// with filepath.Match.
45+
// See https://golang.org/pkg/path/filepath/#Match
3646
//
3747
// Please ensure you understand the ramifications of enabling this.
3848
// If used incorrectly your WebSocket server will be open to CSRF attacks.
39-
InsecureSkipVerify bool
49+
OriginPatterns []string
4050

4151
// CompressionMode controls the compression mode.
4252
// Defaults to CompressionNoContextTakeover.
@@ -77,8 +87,12 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con
7787
}
7888

7989
if !opts.InsecureSkipVerify {
80-
err = authenticateOrigin(r)
90+
err = authenticateOrigin(r, opts.OriginPatterns)
8191
if err != nil {
92+
if errors.Is(err, filepath.ErrBadPattern) {
93+
log.Printf("websocket: %v", err)
94+
err = errors.New(http.StatusText(http.StatusForbidden))
95+
}
8296
http.Error(w, err.Error(), http.StatusForbidden)
8397
return nil, err
8498
}
@@ -165,18 +179,35 @@ func verifyClientRequest(w http.ResponseWriter, r *http.Request) (errCode int, _
165179
return 0, nil
166180
}
167181

168-
func authenticateOrigin(r *http.Request) error {
182+
func authenticateOrigin(r *http.Request, originHosts []string) error {
169183
origin := r.Header.Get("Origin")
170-
if origin != "" {
171-
u, err := url.Parse(origin)
184+
if origin == "" {
185+
return nil
186+
}
187+
188+
u, err := url.Parse(origin)
189+
if err != nil {
190+
return fmt.Errorf("failed to parse Origin header %q: %w", origin, err)
191+
}
192+
193+
if strings.EqualFold(r.Host, u.Host) {
194+
return nil
195+
}
196+
197+
for _, hostPattern := range originHosts {
198+
matched, err := match(hostPattern, u.Host)
172199
if err != nil {
173-
return fmt.Errorf("failed to parse Origin header %q: %w", origin, err)
200+
return fmt.Errorf("failed to parse filepath pattern %q: %w", hostPattern, err)
174201
}
175-
if !strings.EqualFold(u.Host, r.Host) {
176-
return fmt.Errorf("request Origin %q is not authorized for Host %q", origin, r.Host)
202+
if matched {
203+
return nil
177204
}
178205
}
179-
return nil
206+
return fmt.Errorf("request Origin %q is not authorized for Host %q", origin, r.Host)
207+
}
208+
209+
func match(pattern, s string) (bool, error) {
210+
return filepath.Match(strings.ToLower(pattern), strings.ToLower(s))
180211
}
181212

182213
func selectSubprotocol(r *http.Request, subprotocols []string) string {
@@ -235,16 +266,6 @@ func acceptDeflate(w http.ResponseWriter, ext websocketExtension, mode Compressi
235266
return copts, nil
236267
}
237268

238-
// parseExtensionParameter parses the value in the extension parameter p.
239-
func parseExtensionParameter(p string) (int, bool) {
240-
ps := strings.Split(p, "=")
241-
if len(ps) == 1 {
242-
return 0, false
243-
}
244-
i, e := strconv.Atoi(strings.Trim(ps[1], `"`))
245-
return i, e == nil
246-
}
247-
248269
func acceptWebkitDeflate(w http.ResponseWriter, ext websocketExtension, mode CompressionMode) (*compressionOptions, error) {
249270
copts := mode.opts()
250271
// The peer must explicitly request it.

accept_js.go

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
type AcceptOptions struct {
1010
Subprotocols []string
1111
InsecureSkipVerify bool
12+
OriginPatterns []string
1213
CompressionMode CompressionMode
1314
CompressionThreshold int
1415
}

accept_test.go

+26-5
Original file line numberDiff line numberDiff line change
@@ -244,10 +244,11 @@ func Test_authenticateOrigin(t *testing.T) {
244244
t.Parallel()
245245

246246
testCases := []struct {
247-
name string
248-
origin string
249-
host string
250-
success bool
247+
name string
248+
origin string
249+
host string
250+
originPatterns []string
251+
success bool
251252
}{
252253
{
253254
name: "none",
@@ -278,6 +279,26 @@ func Test_authenticateOrigin(t *testing.T) {
278279
host: "example.com",
279280
success: true,
280281
},
282+
{
283+
name: "originPatterns",
284+
origin: "https://two.examplE.com",
285+
host: "example.com",
286+
originPatterns: []string{
287+
"*.example.com",
288+
"bar.com",
289+
},
290+
success: true,
291+
},
292+
{
293+
name: "originPatternsUnauthorized",
294+
origin: "https://two.examplE.com",
295+
host: "example.com",
296+
originPatterns: []string{
297+
"exam3.com",
298+
"bar.com",
299+
},
300+
success: false,
301+
},
281302
}
282303

283304
for _, tc := range testCases {
@@ -288,7 +309,7 @@ func Test_authenticateOrigin(t *testing.T) {
288309
r := httptest.NewRequest("GET", "http://"+tc.host+"/", nil)
289310
r.Header.Set("Origin", tc.origin)
290311

291-
err := authenticateOrigin(r)
312+
err := authenticateOrigin(r, tc.originPatterns)
292313
if tc.success {
293314
assert.Success(t, err)
294315
} else {

0 commit comments

Comments
 (0)