From c5e2428533d69c12d6f679fec3b38d172e572b71 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 1 Jun 2019 06:41:41 -0400 Subject: [PATCH 001/519] Improve test coverage --- websocket.go | 5 +++-- websocket_test.go | 6 ------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/websocket.go b/websocket.go index d59812b8..b5ca93a1 100644 --- a/websocket.go +++ b/websocket.go @@ -846,8 +846,9 @@ func (c *Conn) ping(ctx context.Context) error { case <-c.closed: return c.closeErr case <-ctx.Done(): - c.close(xerrors.Errorf("failed to ping: %w", ctx.Err())) - return ctx.Err() + err := xerrors.Errorf("failed to wait for pong: %w", ctx.Err()) + c.close(err) + return err case <-pong: return nil } diff --git a/websocket_test.go b/websocket_test.go index b1c5b9d4..922feeb3 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -263,7 +263,6 @@ func TestHandshake(t *testing.T) { if err != nil { return err } - // Read twice to ensure the un EOFed previous reader works correctly. err = read() if err != nil { return err @@ -843,11 +842,6 @@ func benchConn(b *testing.B, echo, stream bool, size int) { if err != nil { b.Fatal(err) } - - _, err = r.Read([]byte{0}) - if !xerrors.Is(err, io.EOF) { - b.Fatalf("more data in reader than needed") - } } } b.StopTimer() From 3f56add390a5deb000a2ca116d641c9e9230f125 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 1 Jun 2019 06:47:04 -0400 Subject: [PATCH 002/519] Add test for c.Context method --- websocket_test.go | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/websocket_test.go b/websocket_test.go index 922feeb3..23d1b88c 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -455,6 +455,51 @@ func TestHandshake(t *testing.T) { return nil }, }, + { + name: "context", + server: func(w http.ResponseWriter, r *http.Request) error { + c, err := websocket.Accept(w, r, websocket.AcceptOptions{}) + if err != nil { + return err + } + defer c.Close(websocket.StatusInternalError, "") + + ctx, cancel := context.WithTimeout(r.Context(), time.Second) + defer cancel() + + c.Context(ctx) + + for r.Context().Err() == nil { + err = c.Ping(ctx) + if err != nil { + return nil + } + } + + return xerrors.Errorf("all pings succeeded") + }, + client: func(ctx context.Context, u string) error { + c, _, err := websocket.Dial(ctx, u, websocket.DialOptions{}) + if err != nil { + return err + } + defer c.Close(websocket.StatusInternalError, "") + + pctx := c.Context(ctx) + + for ctx.Err() == nil { + err = c.Ping(ctx) + if err != nil { + if pctx.Err() == nil { + return xerrors.Errorf("context from c.Context not cancelled when connection broken") + } + return nil + } + } + + return xerrors.Errorf("all pings succeeded") + }, + }, } for _, tc := range testCases { From 58e26a450a1b7d65d23e5f628d2edece96b47bc3 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 1 Jun 2019 06:49:21 -0400 Subject: [PATCH 003/519] Remove warnings about experimental APIs No need to scare people :) --- websocket.go | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/websocket.go b/websocket.go index b5ca93a1..51a1c540 100644 --- a/websocket.go +++ b/websocket.go @@ -158,9 +158,6 @@ func (c *Conn) timeoutLoop() { // Context returns a context derived from parent that will be cancelled // when the connection is closed or broken. // If the parent context is cancelled, the connection will be closed. -// -// This is an experimental API. -// Please let me know how you feel about it in https://github.com/nhooyr/websocket/issues/79 func (c *Conn) Context(parent context.Context) context.Context { select { case <-c.closed: @@ -542,9 +539,6 @@ func (c *Conn) SetReadLimit(n int64) { // // See the Reader method if you want to be able to reuse buffers or want to stream a message. // The docs on Reader apply to this method as well. -// -// This is an experimental API, please let me know how you feel about it in -// https://github.com/nhooyr/websocket/issues/62 func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { typ, r, err := c.Reader(ctx) if err != nil { @@ -586,9 +580,6 @@ func (c *Conn) writer(ctx context.Context, typ MessageType) (io.WriteCloser, err // // See the Writer method if you want to stream a message. The docs on Writer // regarding concurrency also apply to this method. -// -// This is an experimental API, please let me know how you feel about it in -// https://github.com/nhooyr/websocket/issues/62 func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { err := c.write(ctx, typ, p) if err != nil { @@ -810,9 +801,6 @@ func init() { // Ping sends a ping to the peer and waits for a pong. // Use this to measure latency or ensure the peer is responsive. -// -// This API is experimental. -// Please provide feedback in https://github.com/nhooyr/websocket/issues/1. func (c *Conn) Ping(ctx context.Context) error { err := c.ping(ctx) if err != nil { From 61830e9f920b33059da35b29aa4c6667ed852105 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 1 Jun 2019 18:29:51 -0400 Subject: [PATCH 004/519] Clarify that TCP Keepalive should suffice for most usecases --- websocket.go | 1 + 1 file changed, 1 insertion(+) diff --git a/websocket.go b/websocket.go index 51a1c540..71d505f5 100644 --- a/websocket.go +++ b/websocket.go @@ -801,6 +801,7 @@ func init() { // Ping sends a ping to the peer and waits for a pong. // Use this to measure latency or ensure the peer is responsive. +// TCP Keepalives should suffice for most use cases. func (c *Conn) Ping(ctx context.Context) error { err := c.ping(ctx) if err != nil { From 631c152c881e9f2887cd84c8cba3d63da821f6aa Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 1 Jun 2019 18:45:33 -0400 Subject: [PATCH 005/519] Fix racey conn.Context test --- websocket_test.go | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/websocket_test.go b/websocket_test.go index 23d1b88c..00e510c8 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -485,19 +485,14 @@ func TestHandshake(t *testing.T) { } defer c.Close(websocket.StatusInternalError, "") - pctx := c.Context(ctx) + cctx := c.Context(ctx) - for ctx.Err() == nil { - err = c.Ping(ctx) - if err != nil { - if pctx.Err() == nil { - return xerrors.Errorf("context from c.Context not cancelled when connection broken") - } - return nil - } + select { + case <-ctx.Done(): + return xerrors.Errorf("child context never cancelled") + case <-cctx.Done(): + return nil } - - return xerrors.Errorf("all pings succeeded") }, }, } From e9d8945f39ce09cf9ae099d58e8d2a9d4667db30 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 2 Jun 2019 07:54:42 -0400 Subject: [PATCH 006/519] Pool buffers in wspb and wsjson Closes #71 --- internal/bpool/bpool.go | 24 ++++++++++++++++++ internal/bpool/bpool_test.go | 47 ++++++++++++++++++++++++++++++++++++ wsjson/wsjson.go | 15 ++++++++++-- wspb/wspb.go | 29 +++++++++++++++++++--- 4 files changed, 109 insertions(+), 6 deletions(-) create mode 100644 internal/bpool/bpool.go create mode 100644 internal/bpool/bpool_test.go diff --git a/internal/bpool/bpool.go b/internal/bpool/bpool.go new file mode 100644 index 00000000..4266c236 --- /dev/null +++ b/internal/bpool/bpool.go @@ -0,0 +1,24 @@ +package bpool + +import ( + "bytes" + "sync" +) + +var bpool sync.Pool + +// Get returns a buffer from the pool or creates a new one if +// the pool is empty. +func Get() *bytes.Buffer { + b, ok := bpool.Get().(*bytes.Buffer) + if !ok { + b = &bytes.Buffer{} + } + return b +} + +// Put returns a buffer into the pool. +func Put(b *bytes.Buffer) { + b.Reset() + bpool.Put(b) +} diff --git a/internal/bpool/bpool_test.go b/internal/bpool/bpool_test.go new file mode 100644 index 00000000..2b302a47 --- /dev/null +++ b/internal/bpool/bpool_test.go @@ -0,0 +1,47 @@ +package bpool + +import ( + "strconv" + "sync" + "testing" +) + +func BenchmarkSyncPool(b *testing.B) { + sizes := []int{ + 2, + 16, + 32, + 64, + 128, + 256, + 512, + 4096, + 16384, + } + for _, size := range sizes { + b.Run(strconv.Itoa(size), func(b *testing.B) { + b.Run("allocate", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + buf := make([]byte, size) + _ = buf + } + }) + b.Run("pool", func(b *testing.B) { + b.ReportAllocs() + + p := sync.Pool{} + + b.ResetTimer() + for i := 0; i < b.N; i++ { + buf := p.Get() + if buf == nil { + buf = make([]byte, size) + } + + p.Put(buf) + } + }) + }) + } +} diff --git a/wsjson/wsjson.go b/wsjson/wsjson.go index 994ffad1..fdde2e06 100644 --- a/wsjson/wsjson.go +++ b/wsjson/wsjson.go @@ -8,6 +8,7 @@ import ( "golang.org/x/xerrors" "nhooyr.io/websocket" + "nhooyr.io/websocket/internal/bpool" ) // Read reads a json message from c into v. @@ -22,7 +23,7 @@ func Read(ctx context.Context, c *websocket.Conn, v interface{}) error { } func read(ctx context.Context, c *websocket.Conn, v interface{}) error { - typ, b, err := c.Read(ctx) + typ, r, err := c.Reader(ctx) if err != nil { return err } @@ -32,7 +33,17 @@ func read(ctx context.Context, c *websocket.Conn, v interface{}) error { return xerrors.Errorf("unexpected frame type for json (expected %v): %v", websocket.MessageText, typ) } - err = json.Unmarshal(b, v) + b := bpool.Get() + defer func() { + bpool.Put(b) + }() + + _, err = b.ReadFrom(r) + if err != nil { + return err + } + + err = json.Unmarshal(b.Bytes(), v) if err != nil { return xerrors.Errorf("failed to unmarshal json: %w", err) } diff --git a/wspb/wspb.go b/wspb/wspb.go index e6c91693..49c2ae54 100644 --- a/wspb/wspb.go +++ b/wspb/wspb.go @@ -2,12 +2,15 @@ package wspb import ( + "bytes" "context" + "sync" "github.com/golang/protobuf/proto" "golang.org/x/xerrors" "nhooyr.io/websocket" + "nhooyr.io/websocket/internal/bpool" ) // Read reads a protobuf message from c into v. @@ -21,7 +24,7 @@ func Read(ctx context.Context, c *websocket.Conn, v proto.Message) error { } func read(ctx context.Context, c *websocket.Conn, v proto.Message) error { - typ, b, err := c.Read(ctx) + typ, r, err := c.Reader(ctx) if err != nil { return err } @@ -31,7 +34,17 @@ func read(ctx context.Context, c *websocket.Conn, v proto.Message) error { return xerrors.Errorf("unexpected frame type for protobuf (expected %v): %v", websocket.MessageBinary, typ) } - err = proto.Unmarshal(b, v) + b := bpool.Get() + defer func() { + bpool.Put(b) + }() + + _, err = b.ReadFrom(r) + if err != nil { + return err + } + + err = proto.Unmarshal(b.Bytes(), v) if err != nil { return xerrors.Errorf("failed to unmarshal protobuf: %w", err) } @@ -49,11 +62,19 @@ func Write(ctx context.Context, c *websocket.Conn, v proto.Message) error { return nil } +var writeBufPool sync.Pool + func write(ctx context.Context, c *websocket.Conn, v proto.Message) error { - b, err := proto.Marshal(v) + b := bpool.Get() + pb := proto.NewBuffer(b.Bytes()) + defer func() { + bpool.Put(bytes.NewBuffer(pb.Bytes())) + }() + + err := pb.Marshal(v) if err != nil { return xerrors.Errorf("failed to marshal protobuf: %w", err) } - return c.Write(ctx, websocket.MessageBinary, b) + return c.Write(ctx, websocket.MessageBinary, pb.Bytes()) } From 877db442a728064466d4cdb59c1a4cb80f1ebfaf Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 2 Jun 2019 11:00:59 -0400 Subject: [PATCH 007/519] Update version in go get in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 4199423c..aff8a3d8 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ websocket is a minimal and idiomatic WebSocket library for Go. ## Install ```bash -go get nhooyr.io/websocket@v1.0.0 +go get nhooyr.io/websocket@v1 ``` ## Features From ff3ef619f24ce72a3b526e8a069d8dbf35006082 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 2 Jun 2019 11:04:11 -0400 Subject: [PATCH 008/519] Use full version in go get on README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aff8a3d8..d41cd159 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ websocket is a minimal and idiomatic WebSocket library for Go. ## Install ```bash -go get nhooyr.io/websocket@v1 +go get nhooyr.io/websocket@v1.1.0 ``` ## Features From e79f3501021508ff5b0f03805d16f5059fcda841 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 2 Jun 2019 11:18:58 -0400 Subject: [PATCH 009/519] Fix ci to include coverage for subpackages --- ci/test/entrypoint.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/test/entrypoint.sh b/ci/test/entrypoint.sh index c9a0e80a..81f504a0 100755 --- a/ci/test/entrypoint.sh +++ b/ci/test/entrypoint.sh @@ -10,7 +10,7 @@ accurate numbers" echo set -x -go test -race -coverprofile=ci/out/coverage.prof --vet=off -bench=. ./... +go test -race -coverprofile=ci/out/coverage.prof --vet=off -bench=. -coverpkg=./... ./... go tool cover -func=ci/out/coverage.prof if [[ $CI ]]; then From 4130a309fa8b3f5de1552360e09413ab9db7783a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 3 Jun 2019 10:00:04 -0400 Subject: [PATCH 010/519] Fix pooling docs in wsjson --- wsjson/wsjson.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/wsjson/wsjson.go b/wsjson/wsjson.go index fdde2e06..19e3e6d7 100644 --- a/wsjson/wsjson.go +++ b/wsjson/wsjson.go @@ -12,8 +12,7 @@ import ( ) // Read reads a json message from c into v. -// If the message is larger than 128 bytes, it will use a buffer -// from a pool instead of performing an allocation. +// It will reuse buffers to avoid allocations. func Read(ctx context.Context, c *websocket.Conn, v interface{}) error { err := read(ctx, c, v) if err != nil { @@ -52,7 +51,7 @@ func read(ctx context.Context, c *websocket.Conn, v interface{}) error { } // Write writes the json message v to c. -// It uses json.Encoder which automatically reuses buffers. +// It will reuse buffers to avoid allocations. func Write(ctx context.Context, c *websocket.Conn, v interface{}) error { err := write(ctx, c, v) if err != nil { From abcbea08411e60bc2553922f3991358ab14f76d0 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 3 Jun 2019 21:02:36 -0400 Subject: [PATCH 011/519] Add WebSocket masking and correctly use Sec-WebSocket-Key in client Closes #88 --- accept.go | 9 ++-- dial.go | 27 ++++++----- dial_test.go | 20 ++++++++- export_test.go | 3 ++ websocket.go | 112 +++++++++++++++++++++++++++++++++++----------- websocket_test.go | 1 - 6 files changed, 132 insertions(+), 40 deletions(-) create mode 100644 export_test.go diff --git a/accept.go b/accept.go index 6e214111..d3ba3258 100644 --- a/accept.go +++ b/accept.go @@ -165,12 +165,15 @@ var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11") func handleSecWebSocketKey(w http.ResponseWriter, r *http.Request) { key := r.Header.Get("Sec-WebSocket-Key") + w.Header().Set("Sec-WebSocket-Accept", secWebSocketAccept(key)) +} + +func secWebSocketAccept(secWebSocketKey string) string { h := sha1.New() - h.Write([]byte(key)) + h.Write([]byte(secWebSocketKey)) h.Write(keyGUID) - responseKey := base64.StdEncoding.EncodeToString(h.Sum(nil)) - w.Header().Set("Sec-WebSocket-Accept", responseKey) + return base64.StdEncoding.EncodeToString(h.Sum(nil)) } func authenticateOrigin(r *http.Request) error { diff --git a/dial.go b/dial.go index 64d2820d..1983f89a 100644 --- a/dial.go +++ b/dial.go @@ -7,6 +7,7 @@ import ( "encoding/base64" "io" "io/ioutil" + "math/rand" "net/http" "net/url" "strings" @@ -30,11 +31,6 @@ type DialOptions struct { Subprotocols []string } -// We use this key for all client requests as the Sec-WebSocket-Key header doesn't do anything. -// See https://stackoverflow.com/a/37074398/4283659. -// We also use the same mask key for every message as it too does not make a difference. -var secWebSocketKey = base64.StdEncoding.EncodeToString(make([]byte, 16)) - // Dial performs a WebSocket handshake on the given url with the given options. // The response is the WebSocket handshake response from the server. // If an error occurs, the returned response may be non nil. However, you can only @@ -82,7 +78,7 @@ func dial(ctx context.Context, u string, opts DialOptions) (_ *Conn, _ *http.Res req.Header.Set("Connection", "Upgrade") req.Header.Set("Upgrade", "websocket") req.Header.Set("Sec-WebSocket-Version", "13") - req.Header.Set("Sec-WebSocket-Key", secWebSocketKey) + req.Header.Set("Sec-WebSocket-Key", makeSecWebSocketKey()) if len(opts.Subprotocols) > 0 { req.Header.Set("Sec-WebSocket-Protocol", strings.Join(opts.Subprotocols, ",")) } @@ -101,7 +97,7 @@ func dial(ctx context.Context, u string, opts DialOptions) (_ *Conn, _ *http.Res } }() - err = verifyServerResponse(resp) + err = verifyServerResponse(req, resp) if err != nil { return nil, resp, err } @@ -118,12 +114,13 @@ func dial(ctx context.Context, u string, opts DialOptions) (_ *Conn, _ *http.Res closer: rwc, client: true, } + c.extractBufioWriterBuf(rwc) c.init() return c, resp, nil } -func verifyServerResponse(resp *http.Response) error { +func verifyServerResponse(r *http.Request, resp *http.Response) error { if resp.StatusCode != http.StatusSwitchingProtocols { return xerrors.Errorf("expected handshake response status code %v but got %v", http.StatusSwitchingProtocols, resp.StatusCode) } @@ -136,8 +133,12 @@ func verifyServerResponse(resp *http.Response) error { return xerrors.Errorf("websocket protocol violation: Upgrade header %q does not contain websocket", resp.Header.Get("Upgrade")) } - // We do not care about Sec-WebSocket-Accept because it does not matter. - // See the secWebSocketKey global variable. + if resp.Header.Get("Sec-WebSocket-Accept") != secWebSocketAccept(r.Header.Get("Sec-WebSocket-Key")) { + return xerrors.Errorf("websocket protocol violation: invalid Sec-WebSocket-Accept %q, key %q", + resp.Header.Get("Sec-WebSocket-Accept"), + r.Header.Get("Sec-WebSocket-Key"), + ) + } return nil } @@ -176,3 +177,9 @@ func getBufioWriter(w io.Writer) *bufio.Writer { func returnBufioWriter(bw *bufio.Writer) { bufioWriterPool.Put(bw) } + +func makeSecWebSocketKey() string { + b := make([]byte, 16) + rand.Read(b) + return base64.StdEncoding.EncodeToString(b) +} diff --git a/dial_test.go b/dial_test.go index 02aaa4fc..6f0deef9 100644 --- a/dial_test.go +++ b/dial_test.go @@ -38,6 +38,16 @@ func Test_verifyServerHandshake(t *testing.T) { }, success: false, }, + { + name: "badSecWebSocketAccept", + response: func(w http.ResponseWriter) { + w.Header().Set("Connection", "Upgrade") + w.Header().Set("Upgrade", "websocket") + w.Header().Set("Sec-WebSocket-Accept", "xd") + w.WriteHeader(http.StatusSwitchingProtocols) + }, + success: false, + }, { name: "success", response: func(w http.ResponseWriter) { @@ -58,7 +68,15 @@ func Test_verifyServerHandshake(t *testing.T) { tc.response(w) resp := w.Result() - err := verifyServerResponse(resp) + r := httptest.NewRequest("GET", "/", nil) + key := makeSecWebSocketKey() + r.Header.Set("Sec-WebSocket-Key", key) + + if resp.Header.Get("Sec-WebSocket-Accept") == "" { + resp.Header.Set("Sec-WebSocket-Accept", secWebSocketAccept(key)) + } + + err := verifyServerResponse(r, resp) if (err == nil) != tc.success { t.Fatalf("unexpected error: %+v", err) } diff --git a/export_test.go b/export_test.go new file mode 100644 index 00000000..22ad76fc --- /dev/null +++ b/export_test.go @@ -0,0 +1,3 @@ +package websocket + +var Compute = handleSecWebSocketKey diff --git a/websocket.go b/websocket.go index 71d505f5..50744326 100644 --- a/websocket.go +++ b/websocket.go @@ -3,6 +3,7 @@ package websocket import ( "bufio" "context" + cryptorand "crypto/rand" "fmt" "io" "io/ioutil" @@ -26,8 +27,11 @@ type Conn struct { subprotocol string br *bufio.Reader bw *bufio.Writer - closer io.Closer - client bool + // writeBuf is used for masking, its the buffer in bufio.Writer. + // Only used by the client. + writeBuf []byte + closer io.Closer + client bool // read limit for a message in bytes. msgReadLimit int64 @@ -581,22 +585,22 @@ func (c *Conn) writer(ctx context.Context, typ MessageType) (io.WriteCloser, err // See the Writer method if you want to stream a message. The docs on Writer // regarding concurrency also apply to this method. func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { - err := c.write(ctx, typ, p) + _, err := c.write(ctx, typ, p) if err != nil { return xerrors.Errorf("failed to write msg: %w", err) } return nil } -func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) error { +func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error) { err := c.acquireLock(ctx, c.writeMsgLock) if err != nil { - return err + return 0, err } defer c.releaseLock(c.writeMsgLock) - err = c.writeFrame(ctx, true, opcode(typ), p) - return err + n, err := c.writeFrame(ctx, true, opcode(typ), p) + return n, err } // messageWriter enables writing to a WebSocket connection. @@ -620,12 +624,12 @@ func (w *messageWriter) write(p []byte) (int, error) { if w.closed { return 0, xerrors.Errorf("cannot use closed writer") } - err := w.c.writeFrame(w.ctx, false, w.opcode, p) + n, err := w.c.writeFrame(w.ctx, false, w.opcode, p) if err != nil { - return 0, xerrors.Errorf("failed to write data frame: %w", err) + return n, xerrors.Errorf("failed to write data frame: %w", err) } w.opcode = opContinuation - return len(p), nil + return n, nil } // Close flushes the frame to the connection. @@ -644,7 +648,7 @@ func (w *messageWriter) close() error { } w.closed = true - err := w.c.writeFrame(w.ctx, true, w.opcode, nil) + _, err := w.c.writeFrame(w.ctx, true, w.opcode, nil) if err != nil { return xerrors.Errorf("failed to write fin frame: %w", err) } @@ -654,7 +658,7 @@ func (w *messageWriter) close() error { } func (c *Conn) writeControl(ctx context.Context, opcode opcode, p []byte) error { - err := c.writeFrame(ctx, true, opcode, p) + _, err := c.writeFrame(ctx, true, opcode, p) if err != nil { return xerrors.Errorf("failed to write control frame: %w", err) } @@ -662,26 +666,32 @@ func (c *Conn) writeControl(ctx context.Context, opcode opcode, p []byte) error } // writeFrame handles all writes to the connection. -// We never mask inside here because our mask key is always 0,0,0,0. -// See comment on secWebSocketKey for why. -func (c *Conn) writeFrame(ctx context.Context, fin bool, opcode opcode, p []byte) error { +func (c *Conn) writeFrame(ctx context.Context, fin bool, opcode opcode, p []byte) (int, error) { h := header{ fin: fin, opcode: opcode, masked: c.client, payloadLength: int64(len(p)), } + + if c.client { + _, err := io.ReadFull(cryptorand.Reader, h.maskKey[:]) + if err != nil { + return 0, xerrors.Errorf("failed to generate masking key: %w", err) + } + } + b2 := marshalHeader(h) err := c.acquireLock(ctx, c.writeFrameLock) if err != nil { - return err + return 0, err } defer c.releaseLock(c.writeFrameLock) select { case <-c.closed: - return c.closeErr + return 0, c.closeErr case c.setWriteTimeout <- ctx: } @@ -705,17 +715,49 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, opcode opcode, p []byte _, err = c.bw.Write(b2) if err != nil { - return writeErr(err) - } - _, err = c.bw.Write(p) - if err != nil { - return writeErr(err) + return 0, writeErr(err) + } + + var n int + if c.client { + var keypos int + for len(p) > 0 { + if c.bw.Available() == 0 { + err = c.bw.Flush() + if err != nil { + return n, writeErr(err) + } + } + + // Start of next write in the buffer. + i := c.bw.Buffered() + + p2 := p + if len(p) > c.bw.Available() { + p2 = p[:c.bw.Available()] + } + + n2, err := c.bw.Write(p2) + if err != nil { + return n, writeErr(err) + } + + keypos = fastXOR(h.maskKey, keypos, c.writeBuf[i:i+n2]) + + p = p[n2:] + n += n2 + } + } else { + n, err = c.bw.Write(p) + if err != nil { + return n, writeErr(err) + } } if fin { err = c.bw.Flush() if err != nil { - return writeErr(err) + return n, writeErr(err) } } @@ -723,11 +765,11 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, opcode opcode, p []byte // the context expires. select { case <-c.closed: - return c.closeErr + return n, c.closeErr case c.setWriteTimeout <- context.Background(): } - return nil + return n, nil } func (c *Conn) writePong(p []byte) error { @@ -842,3 +884,23 @@ func (c *Conn) ping(ctx context.Context) error { return nil } } + +type writerFunc func(p []byte) (int, error) + +func (f writerFunc) Write(p []byte) (int, error) { + return f(p) +} + +// extractBufioWriterBuf grabs the []byte backing a *bufio.Writer +// and stores it in c.writeBuf. +func (c *Conn) extractBufioWriterBuf(w io.Writer) { + c.bw.Reset(writerFunc(func(p2 []byte) (int, error) { + c.writeBuf = p2[:cap(p2)] + return len(p2), nil + })) + + c.bw.WriteByte(0) + c.bw.Flush() + + c.bw.Reset(w) +} diff --git a/websocket_test.go b/websocket_test.go index 00e510c8..9d867b50 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -68,7 +68,6 @@ func TestHandshake(t *testing.T) { checkHeader("Connection", "Upgrade") checkHeader("Upgrade", "websocket") - checkHeader("Sec-WebSocket-Accept", "ICX+Yqv66kxgM0FcWaLWlFLwTAI=") checkHeader("Sec-WebSocket-Protocol", "myproto") c.Close(websocket.StatusNormalClosure, "") From 9eda2249c931582f8aa74d78fb1c35339fe5fcfd Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 3 Jun 2019 21:30:52 -0400 Subject: [PATCH 012/519] Update tag in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d41cd159..732c5f20 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ websocket is a minimal and idiomatic WebSocket library for Go. ## Install ```bash -go get nhooyr.io/websocket@v1.1.0 +go get nhooyr.io/websocket@v1.1.1 ``` ## Features From d0b105ca25bef65beae772d1d0a5f391d3ff19d4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 4 Jun 2019 01:03:00 -0400 Subject: [PATCH 013/519] Mention Ping API in README --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 732c5f20..15d830d2 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,10 @@ your application doesn't always need to read from the connection unless it expec gorilla/websocket requires you to constantly read from the connection to respond to control frames even if you don't expect the peer to send any messages. +The ping API is also much nicer. Unlike gorilla/websocket, you don't need to register a +callback for pongs, there is a single exported Ping method on the Conn you use that sends +a ping and waits for the pong. + In terms of performance, the differences depend on your application code. nhooyr/websocket reuses buffers efficiently out of the box if you use the wsjson and wspb subpackages whereas gorilla/websocket does not. As mentioned above, nhooyr/websocket also supports concurrent From 56f091198f0ef2705d761b5cb93397ba37bd25f8 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 4 Jun 2019 01:04:32 -0400 Subject: [PATCH 014/519] Simplify README language --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 15d830d2..f96664d8 100644 --- a/README.md +++ b/README.md @@ -129,8 +129,8 @@ gorilla/websocket requires you to constantly read from the connection to respond even if you don't expect the peer to send any messages. The ping API is also much nicer. Unlike gorilla/websocket, you don't need to register a -callback for pongs, there is a single exported Ping method on the Conn you use that sends -a ping and waits for the pong. +callback for pongs which makes the control flow awkward. There is Ping method on the Conn +that sends a ping and waits for the pong. In terms of performance, the differences depend on your application code. nhooyr/websocket reuses buffers efficiently out of the box if you use the wsjson and wspb subpackages whereas From 0acea456d32ed2d208d88df0545a0fddb3a20821 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 4 Jun 2019 01:05:45 -0400 Subject: [PATCH 015/519] Better README language again --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f96664d8..9223326e 100644 --- a/README.md +++ b/README.md @@ -129,8 +129,8 @@ gorilla/websocket requires you to constantly read from the connection to respond even if you don't expect the peer to send any messages. The ping API is also much nicer. Unlike gorilla/websocket, you don't need to register a -callback for pongs which makes the control flow awkward. There is Ping method on the Conn -that sends a ping and waits for the pong. +callback for pongs. There is Ping method on the Conn that sends a ping and waits for the pong. +Callbacks tend to encourage awkward control flow. In terms of performance, the differences depend on your application code. nhooyr/websocket reuses buffers efficiently out of the box if you use the wsjson and wspb subpackages whereas From 308f9c2cb4d5056cb322131887470326f81ddfca Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 4 Jun 2019 01:07:33 -0400 Subject: [PATCH 016/519] Simplify ping API desc --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9223326e..cb5eb7a3 100644 --- a/README.md +++ b/README.md @@ -128,9 +128,9 @@ your application doesn't always need to read from the connection unless it expec gorilla/websocket requires you to constantly read from the connection to respond to control frames even if you don't expect the peer to send any messages. -The ping API is also much nicer. Unlike gorilla/websocket, you don't need to register a -callback for pongs. There is Ping method on the Conn that sends a ping and waits for the pong. -Callbacks tend to encourage awkward control flow. +The ping API is also much nicer. gorilla/websocket requires registering a pong handler on the Conn +which results in an awkward control flow. With nhooyr/websocket you use the Ping method on +the Conn that sends a ping and also waits for the pong. In terms of performance, the differences depend on your application code. nhooyr/websocket reuses buffers efficiently out of the box if you use the wsjson and wspb subpackages whereas From bf6f475db47abe7fc4f2bfe4cd13ff075c581a20 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 4 Jun 2019 01:08:15 -0400 Subject: [PATCH 017/519] Improve README grammar --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cb5eb7a3..490d99f5 100644 --- a/README.md +++ b/README.md @@ -129,8 +129,8 @@ gorilla/websocket requires you to constantly read from the connection to respond even if you don't expect the peer to send any messages. The ping API is also much nicer. gorilla/websocket requires registering a pong handler on the Conn -which results in an awkward control flow. With nhooyr/websocket you use the Ping method on -the Conn that sends a ping and also waits for the pong. +which results in awkward control flow. With nhooyr/websocket you use the Ping method on the Conn +that sends a ping and also waits for the pong. In terms of performance, the differences depend on your application code. nhooyr/websocket reuses buffers efficiently out of the box if you use the wsjson and wspb subpackages whereas From e883cde4a0bf47b26252b62e92cf201434b7e252 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 5 Jun 2019 09:35:54 -0400 Subject: [PATCH 018/519] Add companies list Closes #90 --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 490d99f5..47cc3296 100644 --- a/README.md +++ b/README.md @@ -165,3 +165,11 @@ about performant WebSocket servers. If you want a library that gives you absolute control over everything, this is the library, but for most users, the API provided by nhooyr/websocket will fit better as it is nearly just as performant but much easier to use correctly and idiomatic. + +## Users + +This is a list of companies or projects that use this library. + +- [Coder](https://github.com/cdr) + +If your company or project is using this library, please feel free to open a PR to amend the list. From 22638e0bfb78969034c479c62cab5d63bd504b63 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 5 Jun 2019 09:38:45 -0400 Subject: [PATCH 019/519] Document separate idle and read timeout Closes #87 --- websocket.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/websocket.go b/websocket.go index 50744326..37719932 100644 --- a/websocket.go +++ b/websocket.go @@ -342,6 +342,11 @@ func (c *Conn) handleControl(h header) { // be read if you do not read the message from the connection. // // Only one Reader may be open at a time. +// +// If you need a separate timeout on the Reader call and then the message +// Read, use time.AfterFunc to cancel the context passed in early. +// See https://github.com/nhooyr/websocket/issues/87#issue-451703332 +// Most users should not need this. func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) { typ, r, err := c.reader(ctx) if err != nil { From 1e92b02bfcdd0d5353be868c08538d13a7ef7d0f Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 5 Jun 2019 09:39:26 -0400 Subject: [PATCH 020/519] Explicitly state that Accept does not allow cross origin requests Closes #91 --- accept.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/accept.go b/accept.go index d3ba3258..bf2ed3c8 100644 --- a/accept.go +++ b/accept.go @@ -79,7 +79,8 @@ func verifyClientRequest(w http.ResponseWriter, r *http.Request) error { // the connection to a WebSocket. // // Accept will reject the handshake if the Origin domain is not the same as the Host unless -// the InsecureSkipVerify option is set. +// the InsecureSkipVerify option is set. In other words, by default it does not allow +// cross origin requests. // // The returned connection will be bound by r.Context(). Use conn.Context() to change // the bounding context. From f628749790bc308f2c978f72b92666bd7fd5ae58 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 7 Jun 2019 15:41:10 -0400 Subject: [PATCH 021/519] Remove readLoop Closes #93 --- README.md | 14 +-- accept.go | 4 - limitedreader.go | 7 +- websocket.go | 228 +++++++++++++++++----------------------------- websocket_test.go | 54 ++--------- wsjson/wsjson.go | 1 + wspb/wspb.go | 1 + 7 files changed, 103 insertions(+), 206 deletions(-) diff --git a/README.md b/README.md index 47cc3296..38541cfd 100644 --- a/README.md +++ b/README.md @@ -123,24 +123,18 @@ it has to reinvent hooks for TLS and proxies and prevents support of HTTP/2. Some more advantages of nhooyr/websocket are that it supports concurrent writes and makes it very easy to close the connection with a status code and reason. -nhooyr/websocket also responds to pings, pongs and close frames in a separate goroutine so that -your application doesn't always need to read from the connection unless it expects a data message. -gorilla/websocket requires you to constantly read from the connection to respond to control frames -even if you don't expect the peer to send any messages. - The ping API is also much nicer. gorilla/websocket requires registering a pong handler on the Conn which results in awkward control flow. With nhooyr/websocket you use the Ping method on the Conn that sends a ping and also waits for the pong. In terms of performance, the differences depend on your application code. nhooyr/websocket reuses buffers efficiently out of the box if you use the wsjson and wspb subpackages whereas -gorilla/websocket does not. As mentioned above, nhooyr/websocket also supports concurrent +gorilla/websocket does not at all. As mentioned above, nhooyr/websocket also supports concurrent writers out of the box. -The only performance con to nhooyr/websocket is that uses two extra goroutines. One for -reading pings, pongs and close frames async to application code and another to support -context.Context cancellation. This costs 4 KB of memory which is cheap compared -to the benefits. +The only performance con to nhooyr/websocket is that uses one extra goroutine to support +cancellation with context.Context and the net/http client side body upgrade. +This costs 2 KB of memory which is cheap compared to simplicity benefits. ### x/net/websocket diff --git a/accept.go b/accept.go index bf2ed3c8..e0054b2b 100644 --- a/accept.go +++ b/accept.go @@ -81,9 +81,6 @@ func verifyClientRequest(w http.ResponseWriter, r *http.Request) error { // Accept will reject the handshake if the Origin domain is not the same as the Host unless // the InsecureSkipVerify option is set. In other words, by default it does not allow // cross origin requests. -// -// The returned connection will be bound by r.Context(). Use conn.Context() to change -// the bounding context. func Accept(w http.ResponseWriter, r *http.Request, opts AcceptOptions) (*Conn, error) { c, err := accept(w, r, opts) if err != nil { @@ -143,7 +140,6 @@ func accept(w http.ResponseWriter, r *http.Request, opts AcceptOptions) (*Conn, closer: netConn, } c.init() - c.Context(r.Context()) return c, nil } diff --git a/limitedreader.go b/limitedreader.go index 63bf40c4..7957e794 100644 --- a/limitedreader.go +++ b/limitedreader.go @@ -1,7 +1,6 @@ package websocket import ( - "fmt" "io" "golang.org/x/xerrors" @@ -20,9 +19,9 @@ func (lr *limitedReader) Read(p []byte) (int, error) { } if lr.left <= 0 { - msg := fmt.Sprintf("read limited at %v bytes", lr.limit) - lr.c.Close(StatusPolicyViolation, msg) - return 0, xerrors.Errorf(msg) + err := xerrors.Errorf("read limited at %v bytes", lr.limit) + lr.c.Close(StatusMessageTooBig, err.Error()) + return 0, err } if int64(len(p)) > lr.left { diff --git a/websocket.go b/websocket.go index 37719932..3553707a 100644 --- a/websocket.go +++ b/websocket.go @@ -28,7 +28,7 @@ type Conn struct { br *bufio.Reader bw *bufio.Writer // writeBuf is used for masking, its the buffer in bufio.Writer. - // Only used by the client. + // Only used by the client for masking the bytes in the buffer. writeBuf []byte closer io.Closer client bool @@ -51,17 +51,9 @@ type Conn struct { previousReader *messageReader // readFrameLock is acquired to read from bw. readFrameLock chan struct{} - // readMsg is used by messageReader to receive frames from - // readLoop. - readMsg chan header - // readMsgDone is used to tell the readLoop to continue after - // messageReader has read a frame. - readMsgDone chan struct{} setReadTimeout chan context.Context setWriteTimeout chan context.Context - setConnContext chan context.Context - getConnContext chan context.Context activePingsMu sync.Mutex activePings map[string]chan<- struct{} @@ -76,13 +68,9 @@ func (c *Conn) init() { c.writeFrameLock = make(chan struct{}, 1) c.readFrameLock = make(chan struct{}, 1) - c.readMsg = make(chan header) - c.readMsgDone = make(chan struct{}) c.setReadTimeout = make(chan context.Context) c.setWriteTimeout = make(chan context.Context) - c.setConnContext = make(chan context.Context) - c.getConnContext = make(chan context.Context) c.activePings = make(map[string]chan<- struct{}) @@ -91,7 +79,6 @@ func (c *Conn) init() { }) go c.timeoutLoop() - go c.readLoop() } // Subprotocol returns the negotiated subprotocol. @@ -131,56 +118,23 @@ func (c *Conn) close(err error) { func (c *Conn) timeoutLoop() { readCtx := context.Background() writeCtx := context.Background() - parentCtx := context.Background() for { select { case <-c.closed: return + case writeCtx = <-c.setWriteTimeout: case readCtx = <-c.setReadTimeout: + case <-readCtx.Done(): c.close(xerrors.Errorf("data read timed out: %w", readCtx.Err())) case <-writeCtx.Done(): c.close(xerrors.Errorf("data write timed out: %w", writeCtx.Err())) - case <-parentCtx.Done(): - c.close(xerrors.Errorf("parent context cancelled: %w", parentCtx.Err())) - return - case parentCtx = <-c.setConnContext: - ctx, cancelCtx := context.WithCancel(parentCtx) - defer cancelCtx() - - select { - case <-c.closed: - return - case c.getConnContext <- ctx: - } } } } -// Context returns a context derived from parent that will be cancelled -// when the connection is closed or broken. -// If the parent context is cancelled, the connection will be closed. -func (c *Conn) Context(parent context.Context) context.Context { - select { - case <-c.closed: - ctx, cancel := context.WithCancel(parent) - cancel() - return ctx - case c.setConnContext <- parent: - } - - select { - case <-c.closed: - ctx, cancel := context.WithCancel(parent) - cancel() - return ctx - case ctx := <-c.getConnContext: - return ctx - } -} - func (c *Conn) acquireLock(ctx context.Context, lock chan struct{}) error { select { case <-ctx.Done(): @@ -210,30 +164,9 @@ func (c *Conn) releaseLock(lock chan struct{}) { } } -func (c *Conn) readLoop() { +func (c *Conn) readTillMsg(ctx context.Context) (header, error) { for { - h, err := c.readTillMsg() - if err != nil { - return - } - - select { - case <-c.closed: - return - case c.readMsg <- h: - } - - select { - case <-c.closed: - return - case <-c.readMsgDone: - } - } -} - -func (c *Conn) readTillMsg() (header, error) { - for { - h, err := c.readFrameHeader() + h, err := c.readFrameHeader(ctx) if err != nil { return header{}, err } @@ -245,7 +178,10 @@ func (c *Conn) readTillMsg() (header, error) { } if h.opcode.controlOp() { - c.handleControl(h) + err = c.handleControl(ctx, h) + if err != nil { + return header{}, err + } continue } @@ -260,43 +196,64 @@ func (c *Conn) readTillMsg() (header, error) { } } -func (c *Conn) readFrameHeader() (header, error) { +func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { err := c.acquireLock(context.Background(), c.readFrameLock) if err != nil { return header{}, err } defer c.releaseLock(c.readFrameLock) + select { + case <-c.closed: + return header{}, c.closeErr + case c.setReadTimeout <- ctx: + } + h, err := readHeader(c.br) if err != nil { + select { + case <-c.closed: + return header{}, c.closeErr + case <-ctx.Done(): + err = ctx.Err() + default: + } err := xerrors.Errorf("failed to read header: %w", err) c.releaseLock(c.readFrameLock) c.close(err) return header{}, err } + select { + case <-c.closed: + return header{}, c.closeErr + case c.setReadTimeout <- context.Background(): + } + return h, nil } -func (c *Conn) handleControl(h header) { +func (c *Conn) handleControl(ctx context.Context, h header) error { if h.payloadLength > maxControlFramePayload { - c.Close(StatusProtocolError, "control frame too large") - return + err := xerrors.Errorf("control frame too large at %v bytes", h.payloadLength) + c.Close(StatusProtocolError, err.Error()) + return err } if !h.fin { - c.Close(StatusProtocolError, "control frame cannot be fragmented") - return + err := xerrors.Errorf("received fragmented control frame") + c.Close(StatusProtocolError, err.Error()) + return err } - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + ctx, cancel := context.WithTimeout(ctx, time.Second*5) defer cancel() b := make([]byte, h.payloadLength) _, err := c.readFramePayload(ctx, b) if err != nil { - return + return err } if h.masked { @@ -305,7 +262,7 @@ func (c *Conn) handleControl(h header) { switch h.opcode { case opPing: - c.writePong(b) + return c.writePong(b) case opPong: c.activePingsMu.Lock() pong, ok := c.activePings[string(b)] @@ -313,17 +270,20 @@ func (c *Conn) handleControl(h header) { if ok { close(pong) } + return nil case opClose: ce, err := parseClosePayload(b) if err != nil { - c.close(xerrors.Errorf("received invalid close payload: %w", err)) - return + err = xerrors.Errorf("received invalid close payload: %w", err) + c.close(err) + return err } if ce.Code == StatusNoStatusRcvd { c.writeClose(nil, ce) } else { c.Close(ce.Code, ce.Reason) } + return c.closeErr default: panic(fmt.Sprintf("websocket: unexpected control opcode: %#v", h)) } @@ -335,11 +295,10 @@ func (c *Conn) handleControl(h header) { // The passed context will also bound the reader. // Ensure you read to EOF otherwise the connection will hang. // -// Control (ping, pong, close) frames will be handled automatically -// in a separate goroutine so if you do not expect any data messages, -// you do not need to read from the connection. However, if the peer -// sends a data message, further pings, pongs and close frames will not -// be read if you do not read the message from the connection. +// You must read from the connection for close frames to be read. +// If you do not expect any data messages from the peer, just call +// Reader in a separate goroutine and close the connection with StatusPolicyViolation +// when it returns. Example at // TODO // // Only one Reader may be open at a time. // @@ -368,47 +327,39 @@ func (c *Conn) reader(ctx context.Context) (MessageType, io.Reader, error) { return 0, nil, xerrors.Errorf("previous message not read to completion") } - select { - case <-c.closed: - return 0, nil, c.closeErr - case <-ctx.Done(): - return 0, nil, ctx.Err() - case h := <-c.readMsg: - if c.previousReader != nil && !c.previousReader.done { - if h.opcode != opContinuation { - err := xerrors.Errorf("received new data message without finishing the previous message") - c.Close(StatusProtocolError, err.Error()) - return 0, nil, err - } - - if !h.fin || h.payloadLength > 0 { - return 0, nil, xerrors.Errorf("previous message not read to completion") - } - - c.previousReader.done = true - - select { - case <-c.closed: - return 0, nil, c.closeErr - case c.readMsgDone <- struct{}{}: - } + h, err := c.readTillMsg(ctx) + if err != nil { + return 0, nil, err + } - return c.reader(ctx) - } else if h.opcode == opContinuation { - err := xerrors.Errorf("received continuation frame not after data or text frame") + if c.previousReader != nil && !c.previousReader.done { + if h.opcode != opContinuation { + err := xerrors.Errorf("received new data message without finishing the previous message") c.Close(StatusProtocolError, err.Error()) return 0, nil, err } - r := &messageReader{ - ctx: ctx, - c: c, - - h: &h, + if !h.fin || h.payloadLength > 0 { + return 0, nil, xerrors.Errorf("previous message not read to completion") } - c.previousReader = r - return MessageType(h.opcode), r, nil + + c.previousReader.done = true + + return c.reader(ctx) + } else if h.opcode == opContinuation { + err := xerrors.Errorf("received continuation frame not after data or text frame") + c.Close(StatusProtocolError, err.Error()) + return 0, nil, err + } + + r := &messageReader{ + ctx: ctx, + c: c, + + h: &h, } + c.previousReader = r + return MessageType(h.opcode), r, nil } // messageReader enables reading a data frame from the WebSocket connection. @@ -441,20 +392,17 @@ func (r *messageReader) read(p []byte) (int, error) { } if r.h == nil { - select { - case <-r.c.closed: - return 0, r.c.closeErr - case <-r.ctx.Done(): - r.c.close(xerrors.Errorf("failed to read: %w", r.ctx.Err())) - return 0, r.ctx.Err() - case h := <-r.c.readMsg: - if h.opcode != opContinuation { - err := xerrors.Errorf("received new data frame without finishing the previous frame") - r.c.Close(StatusProtocolError, err.Error()) - return 0, err - } - r.h = &h + h, err := r.c.readTillMsg(r.ctx) + if err != nil { + return 0, err } + + if h.opcode != opContinuation { + err := xerrors.Errorf("received new data frame without finishing the previous frame") + r.c.Close(StatusProtocolError, err.Error()) + return 0, err + } + r.h = &h } if int64(len(p)) > r.h.payloadLength { @@ -473,12 +421,6 @@ func (r *messageReader) read(p []byte) (int, error) { } if r.h.payloadLength == 0 { - select { - case <-r.c.closed: - return n, r.c.closeErr - case r.c.readMsgDone <- struct{}{}: - } - fin := r.h.fin // Need to nil this as Reader uses it to check @@ -539,7 +481,7 @@ func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { // // By default, the connection has a message read limit of 32768 bytes. // -// When the limit is hit, the connection will be closed with StatusPolicyViolation. +// When the limit is hit, the connection will be closed with StatusMessageTooBig. func (c *Conn) SetReadLimit(n int64) { c.msgReadLimit = n } diff --git a/websocket_test.go b/websocket_test.go index 9d867b50..8d1e7b1d 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -383,6 +383,8 @@ func TestHandshake(t *testing.T) { } defer c.Close(websocket.StatusInternalError, "") + go c.Reader(r.Context()) + err = c.Ping(r.Context()) if err != nil { return err @@ -403,10 +405,10 @@ func TestHandshake(t *testing.T) { } defer c.Close(websocket.StatusInternalError, "") - err = c.Ping(ctx) - if err != nil { - return err - } + errc := make(chan error, 1) + go func() { + errc <- c.Ping(ctx) + }() _, _, err = c.Read(ctx) if err != nil { @@ -414,7 +416,7 @@ func TestHandshake(t *testing.T) { } c.Close(websocket.StatusNormalClosure, "") - return nil + return <-errc }, }, { @@ -439,6 +441,8 @@ func TestHandshake(t *testing.T) { } defer c.Close(websocket.StatusInternalError, "") + go c.Reader(ctx) + err = c.Write(ctx, websocket.MessageBinary, []byte(strings.Repeat("x", 32769))) if err != nil { return err @@ -454,46 +458,6 @@ func TestHandshake(t *testing.T) { return nil }, }, - { - name: "context", - server: func(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, websocket.AcceptOptions{}) - if err != nil { - return err - } - defer c.Close(websocket.StatusInternalError, "") - - ctx, cancel := context.WithTimeout(r.Context(), time.Second) - defer cancel() - - c.Context(ctx) - - for r.Context().Err() == nil { - err = c.Ping(ctx) - if err != nil { - return nil - } - } - - return xerrors.Errorf("all pings succeeded") - }, - client: func(ctx context.Context, u string) error { - c, _, err := websocket.Dial(ctx, u, websocket.DialOptions{}) - if err != nil { - return err - } - defer c.Close(websocket.StatusInternalError, "") - - cctx := c.Context(ctx) - - select { - case <-ctx.Done(): - return xerrors.Errorf("child context never cancelled") - case <-cctx.Done(): - return nil - } - }, - }, } for _, tc := range testCases { diff --git a/wsjson/wsjson.go b/wsjson/wsjson.go index 19e3e6d7..b72d562f 100644 --- a/wsjson/wsjson.go +++ b/wsjson/wsjson.go @@ -44,6 +44,7 @@ func read(ctx context.Context, c *websocket.Conn, v interface{}) error { err = json.Unmarshal(b.Bytes(), v) if err != nil { + c.Close(websocket.StatusInvalidFramePayloadData, "failed to unmarshal JSON") return xerrors.Errorf("failed to unmarshal json: %w", err) } diff --git a/wspb/wspb.go b/wspb/wspb.go index 49c2ae54..56b14ee8 100644 --- a/wspb/wspb.go +++ b/wspb/wspb.go @@ -46,6 +46,7 @@ func read(ctx context.Context, c *websocket.Conn, v proto.Message) error { err = proto.Unmarshal(b.Bytes(), v) if err != nil { + c.Close(websocket.StatusInvalidFramePayloadData, "failed to unmarshal protobuf") return xerrors.Errorf("failed to unmarshal protobuf: %w", err) } From 5add79dcd311c286a695a022058d21e46bbc534c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 7 Jun 2019 17:21:27 -0400 Subject: [PATCH 022/519] Simplify and improve error messages --- internal/bpool/bpool_test.go | 1 - websocket.go | 36 +++++++++++++++++++----------------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/internal/bpool/bpool_test.go b/internal/bpool/bpool_test.go index 2b302a47..5dfe56e6 100644 --- a/internal/bpool/bpool_test.go +++ b/internal/bpool/bpool_test.go @@ -32,7 +32,6 @@ func BenchmarkSyncPool(b *testing.B) { p := sync.Pool{} - b.ResetTimer() for i := 0; i < b.N; i++ { buf := p.Get() if buf == nil { diff --git a/websocket.go b/websocket.go index 3553707a..91197537 100644 --- a/websocket.go +++ b/websocket.go @@ -180,7 +180,7 @@ func (c *Conn) readTillMsg(ctx context.Context) (header, error) { if h.opcode.controlOp() { err = c.handleControl(ctx, h) if err != nil { - return header{}, err + return header{}, xerrors.Errorf("failed to handle control frame: %w", err) } continue } @@ -274,15 +274,10 @@ func (c *Conn) handleControl(ctx context.Context, h header) error { case opClose: ce, err := parseClosePayload(b) if err != nil { - err = xerrors.Errorf("received invalid close payload: %w", err) - c.close(err) - return err - } - if ce.Code == StatusNoStatusRcvd { - c.writeClose(nil, ce) - } else { - c.Close(ce.Code, ce.Reason) + c.Close(StatusProtocolError, "received invalid close payload") + return xerrors.Errorf("received invalid close payload: %w", err) } + c.writeClose(b, ce, false) return c.closeErr default: panic(fmt.Sprintf("websocket: unexpected control opcode: %#v", h)) @@ -398,7 +393,7 @@ func (r *messageReader) read(p []byte) (int, error) { } if h.opcode != opContinuation { - err := xerrors.Errorf("received new data frame without finishing the previous frame") + err := xerrors.Errorf("received new data message without finishing the previous message") r.c.Close(StatusProtocolError, err.Error()) return 0, err } @@ -461,7 +456,7 @@ func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { err = ctx.Err() default: } - err = xerrors.Errorf("failed to read from connection: %w", err) + err = xerrors.Errorf("failed to read frame payload: %w", err) c.releaseLock(c.readFrameLock) c.close(err) return n, err @@ -651,7 +646,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, opcode opcode, p []byte default: } - err = xerrors.Errorf("failed to write to connection: %w", err) + err = xerrors.Errorf("failed to write frame: %w", err) // We need to release the lock first before closing the connection to ensure // the lock can be acquired inside close to ensure no one can access c.bw. c.releaseLock(c.writeFrameLock) @@ -764,20 +759,27 @@ func (c *Conn) exportedClose(code StatusCode, reason string) error { p, _ = ce.bytes() } - return c.writeClose(p, ce) + return c.writeClose(p, ce, true) } -func (c *Conn) writeClose(p []byte, cerr CloseError) error { +func (c *Conn) writeClose(p []byte, err error, us bool) error { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() - err := c.writeControl(ctx, opClose, p) + // If this fails, the connection had to have died. + err = c.writeControl(ctx, opClose, p) if err != nil { return err } - c.close(cerr) - if !xerrors.Is(c.closeErr, cerr) { + if us { + err = xerrors.Errorf("sent close frame: %w", err) + } else { + err = xerrors.Errorf("received close frame: %w", err) + } + + c.close(err) + if !xerrors.Is(c.closeErr, err) { return c.closeErr } From 5404d35122bc0e869b45a4b90330a6886ebb1d2d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 7 Jun 2019 17:33:41 -0400 Subject: [PATCH 023/519] Make CI pass --- websocket.go | 12 ++++++------ websocket_test.go | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/websocket.go b/websocket.go index 91197537..0e263dd3 100644 --- a/websocket.go +++ b/websocket.go @@ -762,24 +762,24 @@ func (c *Conn) exportedClose(code StatusCode, reason string) error { return c.writeClose(p, ce, true) } -func (c *Conn) writeClose(p []byte, err error, us bool) error { +func (c *Conn) writeClose(p []byte, cerr error, us bool) error { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() // If this fails, the connection had to have died. - err = c.writeControl(ctx, opClose, p) + err := c.writeControl(ctx, opClose, p) if err != nil { return err } if us { - err = xerrors.Errorf("sent close frame: %w", err) + cerr = xerrors.Errorf("sent close frame: %w", cerr) } else { - err = xerrors.Errorf("received close frame: %w", err) + cerr = xerrors.Errorf("received close frame: %w", cerr) } - c.close(err) - if !xerrors.Is(c.closeErr, err) { + c.close(cerr) + if !xerrors.Is(c.closeErr, cerr) { return c.closeErr } diff --git a/websocket_test.go b/websocket_test.go index 8d1e7b1d..dd72afb4 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -451,7 +451,7 @@ func TestHandshake(t *testing.T) { err = c.Ping(ctx) var ce websocket.CloseError - if !xerrors.As(err, &ce) || ce.Code != websocket.StatusPolicyViolation { + if !xerrors.As(err, &ce) || ce.Code != websocket.StatusMessageTooBig { return xerrors.Errorf("unexpected error: %w", err) } From df60edfe1d248784520291315436581829b8d998 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 7 Jun 2019 17:38:28 -0400 Subject: [PATCH 024/519] Reuse header and control payload buffers --- header.go | 13 ++++++++++--- header_test.go | 4 ++-- websocket.go | 11 ++++++++--- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/header.go b/header.go index 62b30b38..b1aa2554 100644 --- a/header.go +++ b/header.go @@ -75,12 +75,19 @@ func marshalHeader(h header) []byte { return b } +func makeHeaderBuf() []byte { + return make([]byte, maxHeaderSize-2) +} + // readHeader reads a header from the reader. // See https://tools.ietf.org/html/rfc6455#section-5.2 -func readHeader(r io.Reader) (header, error) { - // We read the first two bytes directly so that we know +func readHeader(b []byte, r io.Reader) (header, error) { + if b == nil { + b = makeHeaderBuf() + } + // We read the first two bytes first so that we know // exactly how long the header is. - b := make([]byte, 2, maxHeaderSize-2) + b = b[:2] _, err := io.ReadFull(r, b) if err != nil { return header{}, err diff --git a/header_test.go b/header_test.go index b9cf351b..78d61899 100644 --- a/header_test.go +++ b/header_test.go @@ -32,7 +32,7 @@ func TestHeader(t *testing.T) { b[2] |= 1 << 7 r := bytes.NewReader(b) - _, err := readHeader(r) + _, err := readHeader(nil, r) if err == nil { t.Fatalf("unexpected error value: %+v", err) } @@ -92,7 +92,7 @@ func TestHeader(t *testing.T) { func testHeader(t *testing.T, h header) { b := marshalHeader(h) r := bytes.NewReader(b) - h2, err := readHeader(r) + h2, err := readHeader(nil, r) if err != nil { t.Logf("header: %#v", h) t.Logf("bytes: %b", b) diff --git a/websocket.go b/websocket.go index 0e263dd3..ebe12597 100644 --- a/websocket.go +++ b/websocket.go @@ -57,6 +57,9 @@ type Conn struct { activePingsMu sync.Mutex activePings map[string]chan<- struct{} + + headerBuf []byte + controlPayloadBuf []byte } func (c *Conn) init() { @@ -74,6 +77,9 @@ func (c *Conn) init() { c.activePings = make(map[string]chan<- struct{}) + c.headerBuf = makeHeaderBuf() + c.controlPayloadBuf = make([]byte, maxControlFramePayload) + runtime.SetFinalizer(c, func(c *Conn) { c.close(xerrors.New("connection garbage collected")) }) @@ -209,7 +215,7 @@ func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { case c.setReadTimeout <- ctx: } - h, err := readHeader(c.br) + h, err := readHeader(c.headerBuf, c.br) if err != nil { select { case <-c.closed: @@ -249,8 +255,7 @@ func (c *Conn) handleControl(ctx context.Context, h header) error { ctx, cancel := context.WithTimeout(ctx, time.Second*5) defer cancel() - b := make([]byte, h.payloadLength) - + b := c.controlPayloadBuf[:h.payloadLength] _, err := c.readFramePayload(ctx, b) if err != nil { return err From ee1f3c601b22b62b9f82c2a2646c9611c1ea838e Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 7 Jun 2019 18:14:56 -0400 Subject: [PATCH 025/519] Reuse write and read header buffers Next is reusing the header structures. --- header.go | 18 ++++++++++++++---- header_test.go | 4 ++-- websocket.go | 19 ++++++++++--------- 3 files changed, 26 insertions(+), 15 deletions(-) diff --git a/header.go b/header.go index b1aa2554..16ab6474 100644 --- a/header.go +++ b/header.go @@ -31,10 +31,19 @@ type header struct { maskKey [4]byte } +func makeWriteHeaderBuf() []byte { + return make([]byte, maxHeaderSize) +} + // bytes returns the bytes of the header. // See https://tools.ietf.org/html/rfc6455#section-5.2 -func marshalHeader(h header) []byte { - b := make([]byte, 2, maxHeaderSize) +func writeHeader(b []byte, h header) []byte { + if b == nil { + b = makeWriteHeaderBuf() + } + + b = b[:2] + b[0] = 0 if h.fin { b[0] |= 1 << 7 @@ -75,7 +84,7 @@ func marshalHeader(h header) []byte { return b } -func makeHeaderBuf() []byte { +func makeReadHeaderBuf() []byte { return make([]byte, maxHeaderSize-2) } @@ -83,8 +92,9 @@ func makeHeaderBuf() []byte { // See https://tools.ietf.org/html/rfc6455#section-5.2 func readHeader(b []byte, r io.Reader) (header, error) { if b == nil { - b = makeHeaderBuf() + b = makeReadHeaderBuf() } + // We read the first two bytes first so that we know // exactly how long the header is. b = b[:2] diff --git a/header_test.go b/header_test.go index 78d61899..b45854ea 100644 --- a/header_test.go +++ b/header_test.go @@ -24,7 +24,7 @@ func TestHeader(t *testing.T) { t.Run("readNegativeLength", func(t *testing.T) { t.Parallel() - b := marshalHeader(header{ + b := writeHeader(nil, header{ payloadLength: 1<<16 + 1, }) @@ -90,7 +90,7 @@ func TestHeader(t *testing.T) { } func testHeader(t *testing.T, h header) { - b := marshalHeader(h) + b := writeHeader(nil, h) r := bytes.NewReader(b) h2, err := readHeader(nil, r) if err != nil { diff --git a/websocket.go b/websocket.go index ebe12597..375685e7 100644 --- a/websocket.go +++ b/websocket.go @@ -45,21 +45,21 @@ type Conn struct { // writeFrameLock is acquired to write a single frame. // Effectively meaning whoever holds it gets to write to bw. writeFrameLock chan struct{} + writeHeaderBuf []byte // Used to ensure the previous reader is read till EOF before allowing // a new one. previousReader *messageReader // readFrameLock is acquired to read from bw. - readFrameLock chan struct{} + readFrameLock chan struct{} + readHeaderBuf []byte + controlPayloadBuf []byte setReadTimeout chan context.Context setWriteTimeout chan context.Context activePingsMu sync.Mutex activePings map[string]chan<- struct{} - - headerBuf []byte - controlPayloadBuf []byte } func (c *Conn) init() { @@ -77,7 +77,8 @@ func (c *Conn) init() { c.activePings = make(map[string]chan<- struct{}) - c.headerBuf = makeHeaderBuf() + c.writeHeaderBuf = makeWriteHeaderBuf() + c.readHeaderBuf = makeReadHeaderBuf() c.controlPayloadBuf = make([]byte, maxControlFramePayload) runtime.SetFinalizer(c, func(c *Conn) { @@ -215,7 +216,7 @@ func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { case c.setReadTimeout <- ctx: } - h, err := readHeader(c.headerBuf, c.br) + h, err := readHeader(c.readHeaderBuf, c.br) if err != nil { select { case <-c.closed: @@ -628,7 +629,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, opcode opcode, p []byte } } - b2 := marshalHeader(h) + headerBytes := writeHeader(c.writeHeaderBuf, h) err := c.acquireLock(ctx, c.writeFrameLock) if err != nil { @@ -651,7 +652,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, opcode opcode, p []byte default: } - err = xerrors.Errorf("failed to write frame: %w", err) + err = xerrors.Errorf("failed to write %v frame: %w", h.opcode, err) // We need to release the lock first before closing the connection to ensure // the lock can be acquired inside close to ensure no one can access c.bw. c.releaseLock(c.writeFrameLock) @@ -660,7 +661,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, opcode opcode, p []byte return err } - _, err = c.bw.Write(b2) + _, err = c.bw.Write(headerBytes) if err != nil { return 0, writeErr(err) } From 4357cbf9cd4e7f80e18aac7d14d0ae9c77aced75 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 7 Jun 2019 18:25:15 -0400 Subject: [PATCH 026/519] Add WriteOnly example --- example_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ websocket.go | 5 ++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/example_test.go b/example_test.go index 57f0aa5e..bc10209e 100644 --- a/example_test.go +++ b/example_test.go @@ -59,3 +59,45 @@ func ExampleDial() { c.Close(websocket.StatusNormalClosure, "") } + +func ExampleWriteOnly() { + fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c, err := websocket.Accept(w, r, websocket.AcceptOptions{}) + if err != nil { + log.Println(err) + return + } + defer c.Close(websocket.StatusInternalError, "the sky is falling") + + ctx, cancel := context.WithTimeout(r.Context(), time.Minute*10) + defer cancel() + + go func() { + defer cancel() + _, _, err := c.Reader(ctx) + if err == nil { + c.Close(websocket.StatusPolicyViolation, "server doesn't accept data messages") + } + }() + + t := time.NewTicker(time.Second * 30) + defer t.Stop() + + for { + select { + case <-ctx.Done(): + c.Close(websocket.StatusNormalClosure, "") + return + case <-t.C: + err = wsjson.Write(ctx, c, "hi") + if err != nil { + log.Println(err) + return + } + } + } + }) + + err := http.ListenAndServe("localhost:8080", fn) + log.Fatal(err) +} diff --git a/websocket.go b/websocket.go index 375685e7..bd087d51 100644 --- a/websocket.go +++ b/websocket.go @@ -21,6 +21,9 @@ import ( // All methods may be called concurrently except for Reader, Read // and SetReadLimit. // +// You must always read from the connection. Otherwise control +// frames will not be handled. See the docs on Reader. +// // Please be sure to call Close on the connection when you // are finished with it to release the associated resources. type Conn struct { @@ -299,7 +302,7 @@ func (c *Conn) handleControl(ctx context.Context, h header) error { // You must read from the connection for close frames to be read. // If you do not expect any data messages from the peer, just call // Reader in a separate goroutine and close the connection with StatusPolicyViolation -// when it returns. Example at // TODO +// when it returns. See the WriteOnly example. // // Only one Reader may be open at a time. // From 0ed9c744fa64fa1b20caaa0394e592298a36bd80 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 7 Jun 2019 18:25:58 -0400 Subject: [PATCH 027/519] Fix race in writeFrame --- websocket.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/websocket.go b/websocket.go index bd087d51..70f40756 100644 --- a/websocket.go +++ b/websocket.go @@ -632,8 +632,6 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, opcode opcode, p []byte } } - headerBytes := writeHeader(c.writeHeaderBuf, h) - err := c.acquireLock(ctx, c.writeFrameLock) if err != nil { return 0, err @@ -664,6 +662,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, opcode opcode, p []byte return err } + headerBytes := writeHeader(c.writeHeaderBuf, h) _, err = c.bw.Write(headerBytes) if err != nil { return 0, writeErr(err) From d76d893a4a4421d406bff6712c7699658eb59f45 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 10 Jun 2019 00:27:30 -0400 Subject: [PATCH 028/519] Improve write structure --- websocket.go | 92 ++++++++++++++++++++++++++++------------------------ 1 file changed, 50 insertions(+), 42 deletions(-) diff --git a/websocket.go b/websocket.go index 70f40756..0c525b58 100644 --- a/websocket.go +++ b/websocket.go @@ -286,7 +286,7 @@ func (c *Conn) handleControl(ctx context.Context, h header) error { c.Close(StatusProtocolError, "received invalid close payload") return xerrors.Errorf("received invalid close payload: %w", err) } - c.writeClose(b, ce, false) + c.writeClose(b, xerrors.Errorf("received close frame: %w", ce)) return c.closeErr default: panic(fmt.Sprintf("websocket: unexpected control opcode: %#v", h)) @@ -644,38 +644,54 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, opcode opcode, p []byte case c.setWriteTimeout <- ctx: } - writeErr := func(err error) error { - select { - case <-c.closed: - return c.closeErr - case <-ctx.Done(): - err = ctx.Err() - default: - } - - err = xerrors.Errorf("failed to write %v frame: %w", h.opcode, err) - // We need to release the lock first before closing the connection to ensure - // the lock can be acquired inside close to ensure no one can access c.bw. - c.releaseLock(c.writeFrameLock) - c.close(err) + n, err := c.realWriteFrame(ctx, h, p) + if err != nil { + return n, err + } - return err + // We already finished writing, no need to potentially brick the connection if + // the context expires. + select { + case <-c.closed: + return n, c.closeErr + case c.setWriteTimeout <- context.Background(): } + return n, nil +} + +func (c *Conn) realWriteFrame(ctx context.Context, h header, p []byte) (n int, err error){ + defer func() { + if err != nil { + select { + case <-c.closed: + err = c.closeErr + case <-ctx.Done(): + err = ctx.Err() + default: + } + + err = xerrors.Errorf("failed to write %v frame: %w", h.opcode, err) + // We need to release the lock first before closing the connection to ensure + // the lock can be acquired inside close to ensure no one can access c.bw. + c.releaseLock(c.writeFrameLock) + c.close(err) + } + }() + headerBytes := writeHeader(c.writeHeaderBuf, h) _, err = c.bw.Write(headerBytes) if err != nil { - return 0, writeErr(err) + return 0, err } - var n int if c.client { var keypos int for len(p) > 0 { if c.bw.Available() == 0 { err = c.bw.Flush() if err != nil { - return n, writeErr(err) + return n, err } } @@ -689,7 +705,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, opcode opcode, p []byte n2, err := c.bw.Write(p2) if err != nil { - return n, writeErr(err) + return n, err } keypos = fastXOR(h.maskKey, keypos, c.writeBuf[i:i+n2]) @@ -700,25 +716,17 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, opcode opcode, p []byte } else { n, err = c.bw.Write(p) if err != nil { - return n, writeErr(err) + return n, err } } - if fin { + if h.fin { err = c.bw.Flush() if err != nil { - return n, writeErr(err) + return n, err } } - // We already finished writing, no need to potentially brick the connection if - // the context expires. - select { - case <-c.closed: - return n, c.closeErr - case c.setWriteTimeout <- context.Background(): - } - return n, nil } @@ -767,10 +775,19 @@ func (c *Conn) exportedClose(code StatusCode, reason string) error { p, _ = ce.bytes() } - return c.writeClose(p, ce, true) + err = c.writeClose(p, xerrors.Errorf("sent close frame: %w", ce)) + if err != nil { + return err + } + + if !xerrors.Is(c.closeErr, ce) { + return c.closeErr + } + + return nil } -func (c *Conn) writeClose(p []byte, cerr error, us bool) error { +func (c *Conn) writeClose(p []byte, cerr error) error { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() @@ -780,16 +797,7 @@ func (c *Conn) writeClose(p []byte, cerr error, us bool) error { return err } - if us { - cerr = xerrors.Errorf("sent close frame: %w", cerr) - } else { - cerr = xerrors.Errorf("received close frame: %w", cerr) - } - c.close(cerr) - if !xerrors.Is(c.closeErr, cerr) { - return c.closeErr - } return nil } From 4234de22a59c15774a05f73580f34f0d1c71b86b Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 10 Jun 2019 00:27:44 -0400 Subject: [PATCH 029/519] Fix docs --- example_test.go | 4 +++- websocket.go | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/example_test.go b/example_test.go index bc10209e..eef0e98d 100644 --- a/example_test.go +++ b/example_test.go @@ -60,7 +60,9 @@ func ExampleDial() { c.Close(websocket.StatusNormalClosure, "") } -func ExampleWriteOnly() { +// This example shows how to correctly handle a WebSocket connection +// on which you will only write and do not expect to read data messages. +func Example_writeOnly() { fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c, err := websocket.Accept(w, r, websocket.AcceptOptions{}) if err != nil { diff --git a/websocket.go b/websocket.go index 0c525b58..8d6088c8 100644 --- a/websocket.go +++ b/websocket.go @@ -302,7 +302,7 @@ func (c *Conn) handleControl(ctx context.Context, h header) error { // You must read from the connection for close frames to be read. // If you do not expect any data messages from the peer, just call // Reader in a separate goroutine and close the connection with StatusPolicyViolation -// when it returns. See the WriteOnly example. +// when it returns. See the writeOnly example. // // Only one Reader may be open at a time. // From 029e4124defb2c9c845979a53ffe849bbf6ef926 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 10 Jun 2019 00:32:01 -0400 Subject: [PATCH 030/519] Fix CI --- websocket.go | 2 +- websocket_test.go | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/websocket.go b/websocket.go index 8d6088c8..129f82ff 100644 --- a/websocket.go +++ b/websocket.go @@ -660,7 +660,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, opcode opcode, p []byte return n, nil } -func (c *Conn) realWriteFrame(ctx context.Context, h header, p []byte) (n int, err error){ +func (c *Conn) realWriteFrame(ctx context.Context, h header, p []byte) (n int, err error) { defer func() { if err != nil { select { diff --git a/websocket_test.go b/websocket_test.go index dd72afb4..17444642 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -415,8 +415,9 @@ func TestHandshake(t *testing.T) { return err } + err = <-errc c.Close(websocket.StatusNormalClosure, "") - return <-errc + return err }, }, { From 0b8b974d4148b600a9d1817738768e0987ea8fcf Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 10 Jun 2019 03:42:53 -0400 Subject: [PATCH 031/519] Reduce allocation overhea to absolute minimum Can't go any lower than this afaict. 16 bytes per Writer and 24 bytes per Reader. go tool pprof agrees with me on bytes per op but says the allocs per op are 3 instead of 4 and thinks echoLoop is allocating. I don't know. Lots of cleanup can be performed. Closes #95 --- limitedreader.go | 33 ----------- websocket.go | 142 ++++++++++++++++++++++++++-------------------- websocket_test.go | 2 +- xor_test.go | 6 ++ 4 files changed, 86 insertions(+), 97 deletions(-) delete mode 100644 limitedreader.go diff --git a/limitedreader.go b/limitedreader.go deleted file mode 100644 index 7957e794..00000000 --- a/limitedreader.go +++ /dev/null @@ -1,33 +0,0 @@ -package websocket - -import ( - "io" - - "golang.org/x/xerrors" -) - -type limitedReader struct { - c *Conn - r io.Reader - left int64 - limit int64 -} - -func (lr *limitedReader) Read(p []byte) (int, error) { - if lr.limit == 0 { - lr.limit = lr.left - } - - if lr.left <= 0 { - err := xerrors.Errorf("read limited at %v bytes", lr.limit) - lr.c.Close(StatusMessageTooBig, err.Error()) - return 0, err - } - - if int64(len(p)) > lr.left { - p = p[:lr.left] - } - n, err := lr.r.Read(p) - lr.left -= int64(n) - return n, err -} diff --git a/websocket.go b/websocket.go index 129f82ff..2efc485d 100644 --- a/websocket.go +++ b/websocket.go @@ -49,6 +49,11 @@ type Conn struct { // Effectively meaning whoever holds it gets to write to bw. writeFrameLock chan struct{} writeHeaderBuf []byte + writeHeader *header + + // messageWriter state. + writeMsgOpcode opcode + writeMsgCtx context.Context // Used to ensure the previous reader is read till EOF before allowing // a new one. @@ -58,6 +63,12 @@ type Conn struct { readHeaderBuf []byte controlPayloadBuf []byte + // messageReader state + readMsgCtx context.Context + readMsgHeader header + readFrameEOF bool + readMaskPos int + setReadTimeout chan context.Context setWriteTimeout chan context.Context @@ -81,6 +92,7 @@ func (c *Conn) init() { c.activePings = make(map[string]chan<- struct{}) c.writeHeaderBuf = makeWriteHeaderBuf() + c.writeHeader = &header{} c.readHeaderBuf = makeReadHeaderBuf() c.controlPayloadBuf = make([]byte, maxControlFramePayload) @@ -315,15 +327,11 @@ func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) { if err != nil { return 0, nil, xerrors.Errorf("failed to get reader: %w", err) } - return typ, &limitedReader{ - c: c, - r: r, - left: c.msgReadLimit, - }, nil + return typ, r, nil } func (c *Conn) reader(ctx context.Context) (MessageType, io.Reader, error) { - if c.previousReader != nil && c.previousReader.h != nil { + if c.previousReader != nil && !c.readFrameEOF { // The only way we know for sure the previous reader is not yet complete is // if there is an active frame not yet fully read. // Otherwise, a user may have read the last byte but not the EOF if the EOF @@ -336,7 +344,7 @@ func (c *Conn) reader(ctx context.Context) (MessageType, io.Reader, error) { return 0, nil, err } - if c.previousReader != nil && !c.previousReader.done { + if c.previousReader != nil && !c.previousReader.eof { if h.opcode != opContinuation { err := xerrors.Errorf("received new data message without finishing the previous message") c.Close(StatusProtocolError, err.Error()) @@ -347,20 +355,26 @@ func (c *Conn) reader(ctx context.Context) (MessageType, io.Reader, error) { return 0, nil, xerrors.Errorf("previous message not read to completion") } - c.previousReader.done = true + c.previousReader.eof = true - return c.reader(ctx) + h, err = c.readTillMsg(ctx) + if err != nil { + return 0, nil, err + } } else if h.opcode == opContinuation { err := xerrors.Errorf("received continuation frame not after data or text frame") c.Close(StatusProtocolError, err.Error()) return 0, nil, err } - r := &messageReader{ - ctx: ctx, - c: c, + c.readMsgCtx = ctx + c.readMsgHeader = h + c.readFrameEOF = false + c.readMaskPos = 0 - h: &h, + r := &messageReader{ + c: c, + left: c.msgReadLimit, } c.previousReader = r return MessageType(h.opcode), r, nil @@ -368,12 +382,9 @@ func (c *Conn) reader(ctx context.Context) (MessageType, io.Reader, error) { // messageReader enables reading a data frame from the WebSocket connection. type messageReader struct { - ctx context.Context - c *Conn - - h *header - maskPos int - done bool + c *Conn + left int64 + eof bool } // Read reads as many bytes as possible into p. @@ -391,12 +402,22 @@ func (r *messageReader) Read(p []byte) (int, error) { } func (r *messageReader) read(p []byte) (int, error) { - if r.done { + if r.eof { return 0, xerrors.Errorf("cannot use EOFed reader") } - if r.h == nil { - h, err := r.c.readTillMsg(r.ctx) + if r.left <= 0 { + err := xerrors.Errorf("read limited at %v bytes", r.c.msgReadLimit) + r.c.Close(StatusMessageTooBig, err.Error()) + return 0, err + } + + if int64(len(p)) > r.left { + p = p[:r.left] + } + + if r.c.readFrameEOF { + h, err := r.c.readTillMsg(r.c.readMsgCtx) if err != nil { return 0, err } @@ -406,38 +427,37 @@ func (r *messageReader) read(p []byte) (int, error) { r.c.Close(StatusProtocolError, err.Error()) return 0, err } - r.h = &h + + r.c.readMsgHeader = h + r.c.readFrameEOF = false + r.c.readMaskPos = 0 } - if int64(len(p)) > r.h.payloadLength { - p = p[:r.h.payloadLength] + h := r.c.readMsgHeader + if int64(len(p)) > h.payloadLength { + p = p[:h.payloadLength] } - n, err := r.c.readFramePayload(r.ctx, p) + n, err := r.c.readFramePayload(r.c.readMsgCtx, p) - r.h.payloadLength -= int64(n) - if r.h.masked { - r.maskPos = fastXOR(r.h.maskKey, r.maskPos, p) + h.payloadLength -= int64(n) + r.left -= int64(n) + if h.masked { + r.c.readMaskPos = fastXOR(h.maskKey, r.c.readMaskPos, p) } + r.c.readMsgHeader = h if err != nil { return n, err } - if r.h.payloadLength == 0 { - fin := r.h.fin - - // Need to nil this as Reader uses it to check - // whether there is active data on the previous reader and - // now there isn't. - r.h = nil + if h.payloadLength == 0 { + r.c.readFrameEOF = true - if fin { - r.done = true + if h.fin { + r.eof = true return n, io.EOF } - - r.maskPos = 0 } return n, nil @@ -524,10 +544,10 @@ func (c *Conn) writer(ctx context.Context, typ MessageType) (io.WriteCloser, err if err != nil { return nil, err } + c.writeMsgCtx = ctx + c.writeMsgOpcode = opcode(typ) return &messageWriter{ - ctx: ctx, - opcode: opcode(typ), - c: c, + c: c, }, nil } @@ -556,8 +576,6 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error // messageWriter enables writing to a WebSocket connection. type messageWriter struct { - ctx context.Context - opcode opcode c *Conn closed bool } @@ -575,11 +593,11 @@ func (w *messageWriter) write(p []byte) (int, error) { if w.closed { return 0, xerrors.Errorf("cannot use closed writer") } - n, err := w.c.writeFrame(w.ctx, false, w.opcode, p) + n, err := w.c.writeFrame(w.c.writeMsgCtx, false, w.c.writeMsgOpcode, p) if err != nil { return n, xerrors.Errorf("failed to write data frame: %w", err) } - w.opcode = opContinuation + w.c.writeMsgOpcode = opContinuation return n, nil } @@ -599,7 +617,7 @@ func (w *messageWriter) close() error { } w.closed = true - _, err := w.c.writeFrame(w.ctx, true, w.opcode, nil) + _, err := w.c.writeFrame(w.c.writeMsgCtx, true, w.c.writeMsgOpcode, nil) if err != nil { return xerrors.Errorf("failed to write fin frame: %w", err) } @@ -618,20 +636,6 @@ func (c *Conn) writeControl(ctx context.Context, opcode opcode, p []byte) error // writeFrame handles all writes to the connection. func (c *Conn) writeFrame(ctx context.Context, fin bool, opcode opcode, p []byte) (int, error) { - h := header{ - fin: fin, - opcode: opcode, - masked: c.client, - payloadLength: int64(len(p)), - } - - if c.client { - _, err := io.ReadFull(cryptorand.Reader, h.maskKey[:]) - if err != nil { - return 0, xerrors.Errorf("failed to generate masking key: %w", err) - } - } - err := c.acquireLock(ctx, c.writeFrameLock) if err != nil { return 0, err @@ -644,7 +648,19 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, opcode opcode, p []byte case c.setWriteTimeout <- ctx: } - n, err := c.realWriteFrame(ctx, h, p) + c.writeHeader.fin = fin + c.writeHeader.opcode = opcode + c.writeHeader.masked = c.client + c.writeHeader.payloadLength = int64(len(p)) + + if c.client { + _, err := io.ReadFull(cryptorand.Reader, c.writeHeader.maskKey[:]) + if err != nil { + return 0, xerrors.Errorf("failed to generate masking key: %w", err) + } + } + + n, err := c.realWriteFrame(ctx, *c.writeHeader, p) if err != nil { return n, err } diff --git a/websocket_test.go b/websocket_test.go index 17444642..adcc8aeb 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -879,7 +879,7 @@ func BenchmarkConn(b *testing.B) { b.Run("echo", func(b *testing.B) { for _, size := range sizes { b.Run(strconv.Itoa(size), func(b *testing.B) { - benchConn(b, true, true, size) + benchConn(b, true, false, size) }) } }) diff --git a/xor_test.go b/xor_test.go index 634af606..be766227 100644 --- a/xor_test.go +++ b/xor_test.go @@ -4,6 +4,7 @@ import ( "crypto/rand" "strconv" "testing" + "unsafe" "github.com/google/go-cmp/cmp" ) @@ -80,3 +81,8 @@ func BenchmarkXOR(b *testing.B) { }) } } + +func TestFoo(t *testing.T) { + t.Log(unsafe.Sizeof(messageWriter{})) + t.Log(unsafe.Sizeof(messageReader{})) +} From 5eff0e397a3a46c3e3e8cd51cb531b4619f5ea02 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 10 Jun 2019 03:47:46 -0400 Subject: [PATCH 032/519] Update performance comparison --- README.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 38541cfd..1ba912a4 100644 --- a/README.md +++ b/README.md @@ -127,10 +127,9 @@ The ping API is also much nicer. gorilla/websocket requires registering a pong h which results in awkward control flow. With nhooyr/websocket you use the Ping method on the Conn that sends a ping and also waits for the pong. -In terms of performance, the differences depend on your application code. nhooyr/websocket -reuses buffers efficiently out of the box if you use the wsjson and wspb subpackages whereas -gorilla/websocket does not at all. As mentioned above, nhooyr/websocket also supports concurrent -writers out of the box. +In terms of performance, the differences mostly depend on your application code. nhooyr/websocket +reuses message buffers out of the box if you use the wsjson and wspb subpackages. +As mentioned above, nhooyr/websocket also supports concurrent writers. The only performance con to nhooyr/websocket is that uses one extra goroutine to support cancellation with context.Context and the net/http client side body upgrade. From 7b05f53672061cd623e64515a6b211001e964ad1 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 10 Jun 2019 10:27:57 -0400 Subject: [PATCH 033/519] Fix writeOnly example --- example_test.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/example_test.go b/example_test.go index eef0e98d..050af907 100644 --- a/example_test.go +++ b/example_test.go @@ -76,10 +76,8 @@ func Example_writeOnly() { go func() { defer cancel() - _, _, err := c.Reader(ctx) - if err == nil { - c.Close(websocket.StatusPolicyViolation, "server doesn't accept data messages") - } + c.Reader(ctx) + c.Close(websocket.StatusPolicyViolation, "server doesn't accept data messages") }() t := time.NewTicker(time.Second * 30) From 3e007c6d1af687b6460b37635067b6b25ea86f8a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 10 Jun 2019 10:49:05 -0400 Subject: [PATCH 034/519] Remove unneeded Foo test --- websocket_test.go | 6 +++--- xor_test.go | 6 ------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/websocket_test.go b/websocket_test.go index adcc8aeb..5209e2d7 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -809,7 +809,7 @@ func benchConn(b *testing.B, echo, stream bool, size int) { defer c.Close(websocket.StatusInternalError, "") msg := []byte(strings.Repeat("2", size)) - buf := make([]byte, len(msg)) + readBuf := make([]byte, len(msg)) b.SetBytes(int64(len(msg))) b.ReportAllocs() b.ResetTimer() @@ -842,7 +842,7 @@ func benchConn(b *testing.B, echo, stream bool, size int) { b.Fatal(err) } - _, err = io.ReadFull(r, buf) + _, err = io.ReadFull(r, readBuf) if err != nil { b.Fatal(err) } @@ -879,7 +879,7 @@ func BenchmarkConn(b *testing.B) { b.Run("echo", func(b *testing.B) { for _, size := range sizes { b.Run(strconv.Itoa(size), func(b *testing.B) { - benchConn(b, true, false, size) + benchConn(b, false, false, size) }) } }) diff --git a/xor_test.go b/xor_test.go index be766227..634af606 100644 --- a/xor_test.go +++ b/xor_test.go @@ -4,7 +4,6 @@ import ( "crypto/rand" "strconv" "testing" - "unsafe" "github.com/google/go-cmp/cmp" ) @@ -81,8 +80,3 @@ func BenchmarkXOR(b *testing.B) { }) } } - -func TestFoo(t *testing.T) { - t.Log(unsafe.Sizeof(messageWriter{})) - t.Log(unsafe.Sizeof(messageReader{})) -} From 73d39e21ebe2c59ccc70a68831808b4058367251 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 10 Jun 2019 11:08:32 -0400 Subject: [PATCH 035/519] Fix a error resp in Accept --- accept.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accept.go b/accept.go index e0054b2b..ca1eeeaf 100644 --- a/accept.go +++ b/accept.go @@ -106,7 +106,7 @@ func accept(w http.ResponseWriter, r *http.Request, opts AcceptOptions) (*Conn, hj, ok := w.(http.Hijacker) if !ok { err = xerrors.New("passed ResponseWriter does not implement http.Hijacker") - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented) return nil, err } From 1e0955ce5087a7d1d00f8d584871d596af78f4f5 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 11 Jun 2019 17:03:02 -0400 Subject: [PATCH 036/519] Update release version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1ba912a4..00394ac9 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ websocket is a minimal and idiomatic WebSocket library for Go. ## Install ```bash -go get nhooyr.io/websocket@v1.1.1 +go get nhooyr.io/websocket@v1.2.0 ``` ## Features From 0fa48a57a1e1c5d8d82ec7eedc5cc164e6d80a35 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 12 Jun 2019 12:06:01 -0400 Subject: [PATCH 037/519] Reduce Reader allocation by 8 bytes Both Reader and Writer now will only ever allocate 16 bytes for their entire usage :) This is the minimum possible while still preventing misuse of a EOFed Reader or a closed Writer. --- websocket.go | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/websocket.go b/websocket.go index 2efc485d..c5a8f68f 100644 --- a/websocket.go +++ b/websocket.go @@ -36,9 +36,6 @@ type Conn struct { closer io.Closer client bool - // read limit for a message in bytes. - msgReadLimit int64 - closeOnce sync.Once closeErr error closed chan struct{} @@ -50,10 +47,13 @@ type Conn struct { writeFrameLock chan struct{} writeHeaderBuf []byte writeHeader *header + // read limit for a message in bytes. + msgReadLimit int64 // messageWriter state. writeMsgOpcode opcode writeMsgCtx context.Context + readMsgLeft int64 // Used to ensure the previous reader is read till EOF before allowing // a new one. @@ -371,10 +371,10 @@ func (c *Conn) reader(ctx context.Context) (MessageType, io.Reader, error) { c.readMsgHeader = h c.readFrameEOF = false c.readMaskPos = 0 + c.readMsgLeft = c.msgReadLimit r := &messageReader{ - c: c, - left: c.msgReadLimit, + c: c, } c.previousReader = r return MessageType(h.opcode), r, nil @@ -382,9 +382,8 @@ func (c *Conn) reader(ctx context.Context) (MessageType, io.Reader, error) { // messageReader enables reading a data frame from the WebSocket connection. type messageReader struct { - c *Conn - left int64 - eof bool + c *Conn + eof bool } // Read reads as many bytes as possible into p. @@ -406,14 +405,14 @@ func (r *messageReader) read(p []byte) (int, error) { return 0, xerrors.Errorf("cannot use EOFed reader") } - if r.left <= 0 { + if r.c.readMsgLeft <= 0 { err := xerrors.Errorf("read limited at %v bytes", r.c.msgReadLimit) r.c.Close(StatusMessageTooBig, err.Error()) return 0, err } - if int64(len(p)) > r.left { - p = p[:r.left] + if int64(len(p)) > r.c.readMsgLeft { + p = p[:r.c.readMsgLeft] } if r.c.readFrameEOF { @@ -441,7 +440,7 @@ func (r *messageReader) read(p []byte) (int, error) { n, err := r.c.readFramePayload(r.c.readMsgCtx, p) h.payloadLength -= int64(n) - r.left -= int64(n) + r.c.readMsgLeft -= int64(n) if h.masked { r.c.readMaskPos = fastXOR(h.maskKey, r.c.readMaskPos, p) } From 8c04993776769fac245b5ce485debbc328afb050 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 13 Jun 2019 07:19:50 -0400 Subject: [PATCH 038/519] Fix racey ping test --- websocket.go | 4 ++++ websocket_test.go | 35 +++++++++++++++++++++++++++-------- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/websocket.go b/websocket.go index c5a8f68f..290fc308 100644 --- a/websocket.go +++ b/websocket.go @@ -823,6 +823,10 @@ func init() { // Ping sends a ping to the peer and waits for a pong. // Use this to measure latency or ensure the peer is responsive. +// Ping must be called concurrently with Reader as otherwise it does +// not read from the connection and relies on Reader to unblock +// when the pong arrives. +// // TCP Keepalives should suffice for most use cases. func (c *Conn) Ping(ctx context.Context) error { err := c.ping(ctx) diff --git a/websocket_test.go b/websocket_test.go index 5209e2d7..993ff9ab 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -383,7 +383,11 @@ func TestHandshake(t *testing.T) { } defer c.Close(websocket.StatusInternalError, "") - go c.Reader(r.Context()) + errc := make(chan error, 1) + go func() { + _, _, err2 := c.Read(r.Context()) + errc <- err2 + }() err = c.Ping(r.Context()) if err != nil { @@ -395,8 +399,12 @@ func TestHandshake(t *testing.T) { return err } - c.Close(websocket.StatusNormalClosure, "") - return nil + err = <-errc + var ce websocket.CloseError + if xerrors.As(err, &ce) && ce.Code == websocket.StatusNormalClosure { + return nil + } + return xerrors.Errorf("unexpected error: %w", err) }, client: func(ctx context.Context, u string) error { c, _, err := websocket.Dial(ctx, u, websocket.DialOptions{}) @@ -405,19 +413,30 @@ func TestHandshake(t *testing.T) { } defer c.Close(websocket.StatusInternalError, "") - errc := make(chan error, 1) + // We read a message from the connection and then keep reading until + // the Ping completes. + done := make(chan struct{}) go func() { - errc <- c.Ping(ctx) + _, _, err := c.Read(ctx) + if err != nil { + c.Close(websocket.StatusInternalError, err.Error()) + return + } + + close(done) + + c.Read(ctx) }() - _, _, err = c.Read(ctx) + err = c.Ping(ctx) if err != nil { return err } - err = <-errc + <-done + c.Close(websocket.StatusNormalClosure, "") - return err + return nil }, }, { From e43d9adea176121f1b96436b4a30ec91987148ba Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 20 Jun 2019 14:49:04 -0400 Subject: [PATCH 039/519] Improve docs and CI --- ci/fmt/entrypoint.sh | 6 +++++- ci/lib.sh | 4 ---- dial.go | 2 ++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/ci/fmt/entrypoint.sh b/ci/fmt/entrypoint.sh index 14cecc81..f4749ec2 100755 --- a/ci/fmt/entrypoint.sh +++ b/ci/fmt/entrypoint.sh @@ -2,6 +2,10 @@ source ci/lib.sh || exit 1 +unstaged_files() { + git ls-files --other --modified --exclude-standard +} + gen() { # Unfortunately, this is the only way to ensure go.mod and go.sum are correct. # See https://github.com/golang/go/issues/27005 @@ -25,7 +29,7 @@ if [[ $CI && $(unstaged_files) != "" ]]; then echo echo "files either need generation or are formatted incorrectly" echo "please run:" - echo "./test.sh" + echo "./ci/run.sh" echo git status exit 1 diff --git a/ci/lib.sh b/ci/lib.sh index e656feef..0bc093cd 100644 --- a/ci/lib.sh +++ b/ci/lib.sh @@ -12,7 +12,3 @@ export CI=${GITHUB_ACTION-} if [[ $CI ]]; then export GOFLAGS=-mod=readonly fi - -unstaged_files() { - git ls-files --other --modified --exclude-standard -} diff --git a/dial.go b/dial.go index 1983f89a..ac632c11 100644 --- a/dial.go +++ b/dial.go @@ -36,6 +36,8 @@ type DialOptions struct { // If an error occurs, the returned response may be non nil. However, you can only // read the first 1024 bytes of its body. // +// You never need to close the resp.Body yourself. +// // This function requires at least Go 1.12 to succeed as it uses a new feature // in net/http to perform WebSocket handshakes and get a writable body // from the transport. See https://github.com/golang/go/issues/26937#issuecomment-415855861 From def4f37e9546132f94424862105541b7da73a448 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 20 Jun 2019 15:04:37 -0400 Subject: [PATCH 040/519] Clarify docs on Read methods --- websocket.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/websocket.go b/websocket.go index 290fc308..bc90415d 100644 --- a/websocket.go +++ b/websocket.go @@ -26,6 +26,10 @@ import ( // // Please be sure to call Close on the connection when you // are finished with it to release the associated resources. +// +// Every error from Read or Reader will cause the connection +// to be closed so you do not need to write your own error message. +// This applies to the Read methods in the wsjson/wspb subpackages as well. type Conn struct { subprotocol string br *bufio.Reader @@ -311,6 +315,10 @@ func (c *Conn) handleControl(ctx context.Context, h header) error { // The passed context will also bound the reader. // Ensure you read to EOF otherwise the connection will hang. // +// All returned errors will cause the connection +// to be closed so you do not need to write your own error message. +// This applies to the Read methods in the wsjson/wspb subpackages as well. +// // You must read from the connection for close frames to be read. // If you do not expect any data messages from the peer, just call // Reader in a separate goroutine and close the connection with StatusPolicyViolation From 31492255de53f0499a7255b5c4f45b345cb3ec7b Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 20 Jun 2019 15:08:08 -0400 Subject: [PATCH 041/519] Clarify docs on Accept --- accept.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/accept.go b/accept.go index ca1eeeaf..7b727d16 100644 --- a/accept.go +++ b/accept.go @@ -81,6 +81,9 @@ func verifyClientRequest(w http.ResponseWriter, r *http.Request) error { // Accept will reject the handshake if the Origin domain is not the same as the Host unless // the InsecureSkipVerify option is set. In other words, by default it does not allow // cross origin requests. +// +// If an error occurs, Accept will always write an appropriate response so you do not +// have to. func Accept(w http.ResponseWriter, r *http.Request, opts AcceptOptions) (*Conn, error) { c, err := accept(w, r, opts) if err != nil { From 956dbd89dc16b27f6c43d7dc74173deb4ed8bb0f Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 22 Jun 2019 23:44:23 -0400 Subject: [PATCH 042/519] Add closeError test --- websocket_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/websocket_test.go b/websocket_test.go index 993ff9ab..2d7db271 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -74,6 +74,50 @@ func TestHandshake(t *testing.T) { return nil }, }, + { + name: "closeError", + server: func(w http.ResponseWriter, r *http.Request) error { + c, err := websocket.Accept(w, r, websocket.AcceptOptions{}) + if err != nil { + return err + } + defer c.Close(websocket.StatusInternalError, "") + + err = wsjson.Write(r.Context(), c, "hello") + if err != nil { + return err + } + + return nil + }, + client: func(ctx context.Context, u string) error { + c, _, err := websocket.Dial(ctx, u, websocket.DialOptions{ + Subprotocols: []string{"meow"}, + }) + if err != nil { + return err + } + defer c.Close(websocket.StatusInternalError, "") + + var m string + err = wsjson.Read(ctx, c, &m) + if err != nil { + return err + } + + if m != "hello" { + return xerrors.Errorf("recieved unexpected msg but expected hello: %+v", m) + } + + _, _, err = c.Reader(ctx) + var cerr websocket.CloseError + if !xerrors.As(err, &cerr) || cerr.Code != websocket.StatusInternalError { + return xerrors.Errorf("unexpected error: %+v", err) + } + + return nil + }, + }, { name: "defaultSubprotocol", server: func(w http.ResponseWriter, r *http.Request) error { From 559c169714ca4aad8002440b4cce38fe1d2affdd Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 22 Jun 2019 23:44:29 -0400 Subject: [PATCH 043/519] Add CloseRead Closes #98 --- websocket.go | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/websocket.go b/websocket.go index bc90415d..a2ff415d 100644 --- a/websocket.go +++ b/websocket.go @@ -388,6 +388,16 @@ func (c *Conn) reader(ctx context.Context) (MessageType, io.Reader, error) { return MessageType(h.opcode), r, nil } +func (c *Conn) CloseRead(ctx context.Context) context.Context { + ctx, cancel := context.WithCancel(ctx) + go func() { + defer cancel() + c.Reader(ctx) + c.Close(StatusPolicyViolation, "unexpected data message") + }() + return ctx +} + // messageReader enables reading a data frame from the WebSocket connection. type messageReader struct { c *Conn From 6eda9c565cffe89232e23e9e0bdb8179dd0f8794 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 22 Jun 2019 23:48:58 -0400 Subject: [PATCH 044/519] Docs for CloseRead --- example_test.go | 6 +----- websocket.go | 13 ++++++++----- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/example_test.go b/example_test.go index 050af907..0b59e6a0 100644 --- a/example_test.go +++ b/example_test.go @@ -74,11 +74,7 @@ func Example_writeOnly() { ctx, cancel := context.WithTimeout(r.Context(), time.Minute*10) defer cancel() - go func() { - defer cancel() - c.Reader(ctx) - c.Close(websocket.StatusPolicyViolation, "server doesn't accept data messages") - }() + ctx = c.CloseRead(ctx) t := time.NewTicker(time.Second * 30) defer t.Stop() diff --git a/websocket.go b/websocket.go index a2ff415d..91a6808f 100644 --- a/websocket.go +++ b/websocket.go @@ -22,7 +22,7 @@ import ( // and SetReadLimit. // // You must always read from the connection. Otherwise control -// frames will not be handled. See the docs on Reader. +// frames will not be handled. See the docs on Reader and CloseRead. // // Please be sure to call Close on the connection when you // are finished with it to release the associated resources. @@ -319,10 +319,8 @@ func (c *Conn) handleControl(ctx context.Context, h header) error { // to be closed so you do not need to write your own error message. // This applies to the Read methods in the wsjson/wspb subpackages as well. // -// You must read from the connection for close frames to be read. -// If you do not expect any data messages from the peer, just call -// Reader in a separate goroutine and close the connection with StatusPolicyViolation -// when it returns. See the writeOnly example. +// You must read from the connection for control frames to be handled. +// If you do not expect any data messages from the peer, call CloseRead. // // Only one Reader may be open at a time. // @@ -388,6 +386,11 @@ func (c *Conn) reader(ctx context.Context) (MessageType, io.Reader, error) { return MessageType(h.opcode), r, nil } +// CloseRead will close the connection if any data message is received from the peer. +// Call this when you are done reading data messages from the connection but will still write +// to it. Since CloseRead is still reading from the connection, it will respond to ping, pong +// and close frames automatically. It will only close the connection on a data frame. The returned +// context will be cancelled when the connection is closed. func (c *Conn) CloseRead(ctx context.Context) context.Context { ctx, cancel := context.WithCancel(ctx) go func() { From cc4687b56ced49451b9ab15bcc7296e220d4d432 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 23 Jun 2019 00:21:40 -0400 Subject: [PATCH 045/519] Bump README version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 00394ac9..419e892f 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ websocket is a minimal and idiomatic WebSocket library for Go. ## Install ```bash -go get nhooyr.io/websocket@v1.2.0 +go get nhooyr.io/websocket@v1.2.1 ``` ## Features From 80ac84757d5708f4eeccb1b3fb46dd0402b54e2c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 23 Jun 2019 12:54:14 -0400 Subject: [PATCH 046/519] Clarify CloseRead docs --- websocket.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/websocket.go b/websocket.go index 91a6808f..e7fb0dfa 100644 --- a/websocket.go +++ b/websocket.go @@ -386,11 +386,14 @@ func (c *Conn) reader(ctx context.Context) (MessageType, io.Reader, error) { return MessageType(h.opcode), r, nil } -// CloseRead will close the connection if any data message is received from the peer. -// Call this when you are done reading data messages from the connection but will still write -// to it. Since CloseRead is still reading from the connection, it will respond to ping, pong -// and close frames automatically. It will only close the connection on a data frame. The returned -// context will be cancelled when the connection is closed. +// CloseRead will start a goroutine to read from the connection until it is closed or a data message +// is received. If a data message is received, the connection will be closed with StatusPolicyViolation. +// Since CloseRead reads from the connection, it will respond to ping, pong and close frames. +// After calling this method, you cannot read any data messages from the connection. +// The returned context will be cancelled when the connection is closed. +// +// Use this when you do not want to read data messages from the connection anymore but will +// want to write messages to it. func (c *Conn) CloseRead(ctx context.Context) context.Context { ctx, cancel := context.WithCancel(ctx) go func() { From 1c4fdf2b2717a8c6995829516da6be223d864936 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 27 Jun 2019 01:20:22 -0400 Subject: [PATCH 047/519] Clarify gorilla/websocket comparison --- README.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 419e892f..ad9fdd23 100644 --- a/README.md +++ b/README.md @@ -113,8 +113,6 @@ which makes it easy to use correctly. Not only is the API simpler, the implement only 1700 lines whereas gorilla/websocket is at 3500 lines. That's more code to maintain, more code to test, more code to document and more surface area for bugs. -The future of gorilla/websocket is also uncertain. See [gorilla/websocket#370](https://github.com/gorilla/websocket/issues/370). - Moreover, nhooyr/websocket has support for newer Go idioms such as context.Context and also uses net/http's Client and ResponseWriter directly for WebSocket handshakes. gorilla/websocket writes its handshakes to the underlying net.Conn which means @@ -123,7 +121,7 @@ it has to reinvent hooks for TLS and proxies and prevents support of HTTP/2. Some more advantages of nhooyr/websocket are that it supports concurrent writes and makes it very easy to close the connection with a status code and reason. -The ping API is also much nicer. gorilla/websocket requires registering a pong handler on the Conn +The ping API is also nicer. gorilla/websocket requires registering a pong handler on the Conn which results in awkward control flow. With nhooyr/websocket you use the Ping method on the Conn that sends a ping and also waits for the pong. @@ -132,8 +130,8 @@ reuses message buffers out of the box if you use the wsjson and wspb subpackages As mentioned above, nhooyr/websocket also supports concurrent writers. The only performance con to nhooyr/websocket is that uses one extra goroutine to support -cancellation with context.Context and the net/http client side body upgrade. -This costs 2 KB of memory which is cheap compared to simplicity benefits. +cancellation with context.Context. This costs 2 KB of memory which is cheap compared to +simplicity benefits. ### x/net/websocket From a2a2d31cb3d23134087d033f88b340bf3b25b686 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 1 Jul 2019 10:29:59 -0400 Subject: [PATCH 048/519] Add NetConn adapter Closes #100 --- netconn.go | 116 ++++++++++++++++++++++++++++++++++++++++++++++ websocket_test.go | 48 +++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 netconn.go diff --git a/netconn.go b/netconn.go new file mode 100644 index 00000000..0de2f1cb --- /dev/null +++ b/netconn.go @@ -0,0 +1,116 @@ +package websocket + +import ( + "context" + "golang.org/x/xerrors" + "io" + "math" + "net" + "time" +) + +// NetConn converts a *websocket.Conn into a net.Conn. +// Every Write to the net.Conn will correspond to a binary message +// write on *webscoket.Conn. +// Close will close the *websocket.Conn with StatusNormalClosure. +// When a deadline is hit, the connection will be closed. This is +// different from most net.Conn implementations where only the +// reading/writing goroutines are interrupted but the connection is kept alive. +// The Addr methods will return zero value net.TCPAddr. +func NetConn(c *Conn) net.Conn { + nc := &netConn{ + c: c, + } + + var cancel context.CancelFunc + nc.writeContext, cancel = context.WithCancel(context.Background()) + nc.writeTimer = time.AfterFunc(math.MaxInt64, cancel) + nc.writeTimer.Stop() + + nc.readContext, cancel = context.WithCancel(context.Background()) + nc.readTimer = time.AfterFunc(math.MaxInt64, cancel) + nc.readTimer.Stop() + + return nc +} + +type netConn struct { + c *Conn + + writeTimer *time.Timer + writeContext context.Context + + readTimer *time.Timer + readContext context.Context + + reader io.Reader +} + +var _ net.Conn = &netConn{} + +func (c *netConn) Close() error { + return c.c.Close(StatusNormalClosure, "") +} + +func (c *netConn) Write(p []byte) (int, error) { + err := c.c.Write(c.writeContext, MessageBinary, p) + if err != nil { + return 0, err + } + return len(p), nil +} + +func (c *netConn) Read(p []byte) (int, error) { + if c.reader == nil { + typ, r, err := c.c.Reader(c.readContext) + if err != nil { + return 0, err + } + if typ != MessageBinary { + c.c.Close(StatusUnsupportedData, "can only accept binary messages") + return 0, xerrors.Errorf("unexpected frame type read for net conn adapter (expected %v): %v", MessageBinary, typ) + } + c.reader = r + } + + n, err := c.reader.Read(p) + if err == io.EOF { + c.reader = nil + } + return n, err +} + +type unknownAddr struct { +} + +func (a unknownAddr) Network() string { + return "unknown" +} + +func (a unknownAddr) String() string { + return "unknown" +} + +func (c *netConn) RemoteAddr() net.Addr { + return unknownAddr{} +} + +func (c *netConn) LocalAddr() net.Addr { + return unknownAddr{} +} + +func (c *netConn) SetDeadline(t time.Time) error { + c.SetWriteDeadline(t) + c.SetReadDeadline(t) + return nil +} + +func (c *netConn) SetWriteDeadline(t time.Time) error { + c.writeTimer.Reset(t.Sub(time.Now())) + return nil +} + +func (c *netConn) SetReadDeadline(t time.Time) error { + c.readTimer.Reset(t.Sub(time.Now())) + return nil +} diff --git a/websocket_test.go b/websocket_test.go index 2d7db271..2112ff7e 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -118,6 +118,54 @@ func TestHandshake(t *testing.T) { return nil }, }, + { + name: "netConn", + server: func(w http.ResponseWriter, r *http.Request) error { + c, err := websocket.Accept(w, r, websocket.AcceptOptions{}) + if err != nil { + return err + } + defer c.Close(websocket.StatusInternalError, "") + + nc := websocket.NetConn(c) + defer nc.Close() + + nc.SetWriteDeadline(time.Now().Add(time.Second * 10)) + + _, err = nc.Write([]byte("hello")) + if err != nil { + return err + } + + return nil + }, + client: func(ctx context.Context, u string) error { + c, _, err := websocket.Dial(ctx, u, websocket.DialOptions{ + Subprotocols: []string{"meow"}, + }) + if err != nil { + return err + } + defer c.Close(websocket.StatusInternalError, "") + + nc := websocket.NetConn(c) + defer nc.Close() + + nc.SetReadDeadline(time.Now().Add(time.Second * 10)) + + p := make([]byte, len("hello")) + _, err = io.ReadFull(nc, p) + if err != nil { + return err + } + + if string(p) != "hello" { + return xerrors.Errorf("unexpected payload %q received", string(p)) + } + + return nil + }, + }, { name: "defaultSubprotocol", server: func(w http.ResponseWriter, r *http.Request) error { From 2e4b1105932814e737c4fa3b5048bc9d72d7dea3 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 1 Jul 2019 10:36:09 -0400 Subject: [PATCH 049/519] Protect against Reader after CloseRead Closes #101 --- netconn.go | 3 ++- websocket.go | 11 ++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/netconn.go b/netconn.go index 0de2f1cb..184d5d6c 100644 --- a/netconn.go +++ b/netconn.go @@ -2,11 +2,12 @@ package websocket import ( "context" - "golang.org/x/xerrors" "io" "math" "net" "time" + + "golang.org/x/xerrors" ) // NetConn converts a *websocket.Conn into a net.Conn. diff --git a/websocket.go b/websocket.go index e7fb0dfa..f875a142 100644 --- a/websocket.go +++ b/websocket.go @@ -12,6 +12,7 @@ import ( "runtime" "strconv" "sync" + "sync/atomic" "time" "golang.org/x/xerrors" @@ -64,6 +65,7 @@ type Conn struct { previousReader *messageReader // readFrameLock is acquired to read from bw. readFrameLock chan struct{} + readClosed int64 readHeaderBuf []byte controlPayloadBuf []byte @@ -329,6 +331,10 @@ func (c *Conn) handleControl(ctx context.Context, h header) error { // See https://github.com/nhooyr/websocket/issues/87#issue-451703332 // Most users should not need this. func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) { + if atomic.LoadInt64(&c.readClosed) == 1 { + return 0, nil, xerrors.Errorf("websocket connection read closed") + } + typ, r, err := c.reader(ctx) if err != nil { return 0, nil, xerrors.Errorf("failed to get reader: %w", err) @@ -395,10 +401,13 @@ func (c *Conn) reader(ctx context.Context) (MessageType, io.Reader, error) { // Use this when you do not want to read data messages from the connection anymore but will // want to write messages to it. func (c *Conn) CloseRead(ctx context.Context) context.Context { + atomic.StoreInt64(&c.readClosed, 1) + ctx, cancel := context.WithCancel(ctx) go func() { defer cancel() - c.Reader(ctx) + // We use the unexported reader so that we don't get the read closed error. + c.reader(ctx) c.Close(StatusPolicyViolation, "unexpected data message") }() return ctx From 9d31b8d2a78704c6a994508c29116342335b1a5c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 1 Jul 2019 10:56:23 -0400 Subject: [PATCH 050/519] Fix docs on NetConn --- netconn.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netconn.go b/netconn.go index 184d5d6c..3e43d905 100644 --- a/netconn.go +++ b/netconn.go @@ -17,7 +17,7 @@ import ( // When a deadline is hit, the connection will be closed. This is // different from most net.Conn implementations where only the // reading/writing goroutines are interrupted but the connection is kept alive. -// The Addr methods will return zero value net.TCPAddr. +// The Addr methods will return a mock net.Addr. func NetConn(c *Conn) net.Conn { nc := &netConn{ c: c, From 97f63d09c312f87af01afd47678a6fd5ee6643aa Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 1 Jul 2019 11:03:26 -0400 Subject: [PATCH 051/519] Improve docs formatting on NetConn --- netconn.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/netconn.go b/netconn.go index 3e43d905..e397d7a2 100644 --- a/netconn.go +++ b/netconn.go @@ -11,12 +11,16 @@ import ( ) // NetConn converts a *websocket.Conn into a net.Conn. +// // Every Write to the net.Conn will correspond to a binary message // write on *webscoket.Conn. +// // Close will close the *websocket.Conn with StatusNormalClosure. +// // When a deadline is hit, the connection will be closed. This is // different from most net.Conn implementations where only the // reading/writing goroutines are interrupted but the connection is kept alive. +// // The Addr methods will return a mock net.Addr. func NetConn(c *Conn) net.Conn { nc := &netConn{ From 5024792059fe58b2fadff9bcd3ba25f5f34e759e Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 3 Jul 2019 13:54:54 -0400 Subject: [PATCH 052/519] Fix NetConn read bug See https://github.com/nhooyr/websocket/issues/100#issuecomment-508148024 --- netconn.go | 13 +++++++++++++ websocket_test.go | 45 +++++++++++++++++++++++++++++++++++---------- 2 files changed, 48 insertions(+), 10 deletions(-) diff --git a/netconn.go b/netconn.go index e397d7a2..c43d3a29 100644 --- a/netconn.go +++ b/netconn.go @@ -22,6 +22,8 @@ import ( // reading/writing goroutines are interrupted but the connection is kept alive. // // The Addr methods will return a mock net.Addr. +// +// A received StatusNormalClosure close frame will be translated to EOF when reading. func NetConn(c *Conn) net.Conn { nc := &netConn{ c: c, @@ -47,6 +49,7 @@ type netConn struct { readTimer *time.Timer readContext context.Context + eofed bool reader io.Reader } @@ -66,9 +69,18 @@ func (c *netConn) Write(p []byte) (int, error) { } func (c *netConn) Read(p []byte) (int, error) { + if c.eofed { + return 0, io.EOF + } + if c.reader == nil { typ, r, err := c.c.Reader(c.readContext) if err != nil { + var ce CloseError + if xerrors.As(err, &ce) && (ce.Code == StatusNormalClosure) { + c.eofed = true + return 0, io.EOF + } return 0, err } if typ != MessageBinary { @@ -81,6 +93,7 @@ func (c *netConn) Read(p []byte) (int, error) { n, err := c.reader.Read(p) if err == io.EOF { c.reader = nil + err = nil } return n, err } diff --git a/websocket_test.go b/websocket_test.go index 2112ff7e..1dc5283b 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -130,11 +130,13 @@ func TestHandshake(t *testing.T) { nc := websocket.NetConn(c) defer nc.Close() - nc.SetWriteDeadline(time.Now().Add(time.Second * 10)) + nc.SetWriteDeadline(time.Now().Add(time.Second * 15)) - _, err = nc.Write([]byte("hello")) - if err != nil { - return err + for i := 0; i < 3; i++ { + _, err = nc.Write([]byte("hello")) + if err != nil { + return err + } } return nil @@ -151,16 +153,39 @@ func TestHandshake(t *testing.T) { nc := websocket.NetConn(c) defer nc.Close() - nc.SetReadDeadline(time.Now().Add(time.Second * 10)) + nc.SetReadDeadline(time.Now().Add(time.Second * 15)) - p := make([]byte, len("hello")) - _, err = io.ReadFull(nc, p) - if err != nil { + read := func() error { + p := make([]byte, len("hello")) + // We do not use io.ReadFull here as it masks EOFs. + // See https://github.com/nhooyr/websocket/issues/100#issuecomment-508148024 + _, err = nc.Read(p) + if err != nil { + return err + } + + if string(p) != "hello" { + return xerrors.Errorf("unexpected payload %q received", string(p)) + } + return nil + } + + for i := 0; i < 3; i++ { + err = read() + if err != nil { + return err + } + } + + // Ensure the close frame is converted to an EOF and multiple read's after all return EOF. + err = read() + if err != io.EOF { return err } - if string(p) != "hello" { - return xerrors.Errorf("unexpected payload %q received", string(p)) + err = read() + if err != io.EOF { + return err } return nil From d772f32c109da363a3f09fa871d76d2f89180028 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 3 Jul 2019 14:10:17 -0400 Subject: [PATCH 053/519] Bump README.md version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ad9fdd23..71490061 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ websocket is a minimal and idiomatic WebSocket library for Go. ## Install ```bash -go get nhooyr.io/websocket@v1.2.1 +go get nhooyr.io/websocket@v1.3.0 ``` ## Features From f5b6f08a09a9049d99353b64a7ee4920fb436c2a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 12 Jul 2019 11:45:46 -0400 Subject: [PATCH 054/519] Update Conn.Close docs Closes #103 --- statuscode.go | 3 ++- websocket.go | 7 ++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/statuscode.go b/statuscode.go index 661c6693..42ae40c0 100644 --- a/statuscode.go +++ b/statuscode.go @@ -41,7 +41,8 @@ const ( ) // CloseError represents a WebSocket close frame. -// It is returned by Conn's methods when the Connection is closed with a WebSocket close frame. +// It is returned by Conn's methods when a WebSocket close frame is received from +// the peer. // You will need to use https://golang.org/x/xerrors to check for this error. type CloseError struct { Code StatusCode diff --git a/websocket.go b/websocket.go index f875a142..47db5f18 100644 --- a/websocket.go +++ b/websocket.go @@ -792,6 +792,9 @@ func (c *Conn) writePong(p []byte) error { // The connection can only be closed once. Additional calls to Close // are no-ops. // +// This does not perform a WebSocket close handshake. +// See https://github.com/nhooyr/websocket/issues/103 for details on why. +// // The maximum length of reason must be 125 bytes otherwise an internal // error will be sent to the peer. For this reason, you should avoid // sending a dynamic reason. @@ -823,7 +826,9 @@ func (c *Conn) exportedClose(code StatusCode, reason string) error { p, _ = ce.bytes() } - err = c.writeClose(p, xerrors.Errorf("sent close frame: %w", ce)) + // CloseErrors sent are made opaque to prevent applications from thinking + // they received a given status. + err = c.writeClose(p, xerrors.Errorf("sent close frame: %v", ce)) if err != nil { return err } From 4d72b518f0be0f09b81dd3cef2dac7fdf0251a8b Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 12 Jul 2019 23:25:19 -0400 Subject: [PATCH 055/519] Bump version in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 71490061..419c4630 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ websocket is a minimal and idiomatic WebSocket library for Go. ## Install ```bash -go get nhooyr.io/websocket@v1.3.0 +go get nhooyr.io/websocket@v1.3.1 ``` ## Features From e2d44e32fa387782fa84bab2529f1f74e9a6e148 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 19 Jul 2019 00:50:57 +0000 Subject: [PATCH 056/519] Document why NetConn is included in the library. --- netconn.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/netconn.go b/netconn.go index c43d3a29..2db1f11e 100644 --- a/netconn.go +++ b/netconn.go @@ -12,6 +12,11 @@ import ( // NetConn converts a *websocket.Conn into a net.Conn. // +// It's for tunneling arbitrary protocols over WebSockets. +// Few users of the library will need this but it's tricky to implement +// correctly and so provided in the library. +// See https://github.com/nhooyr/websocket/issues/100. +// // Every Write to the net.Conn will correspond to a binary message // write on *webscoket.Conn. // From 1de4bc28736bf04b9388277f2dfa1b5ff1ab6d6d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 20 Jul 2019 14:00:32 +0000 Subject: [PATCH 057/519] Improve docs and implementation of NetConn --- netconn.go | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/netconn.go b/netconn.go index 2db1f11e..9c70b6fe 100644 --- a/netconn.go +++ b/netconn.go @@ -26,7 +26,8 @@ import ( // different from most net.Conn implementations where only the // reading/writing goroutines are interrupted but the connection is kept alive. // -// The Addr methods will return a mock net.Addr. +// The Addr methods will return a mock net.Addr that returns "websocket" for Network +// and "websocket/unknown-addr" for String. // // A received StatusNormalClosure close frame will be translated to EOF when reading. func NetConn(c *Conn) net.Conn { @@ -37,11 +38,15 @@ func NetConn(c *Conn) net.Conn { var cancel context.CancelFunc nc.writeContext, cancel = context.WithCancel(context.Background()) nc.writeTimer = time.AfterFunc(math.MaxInt64, cancel) - nc.writeTimer.Stop() + if !nc.writeTimer.Stop() { + <-nc.writeTimer.C + } nc.readContext, cancel = context.WithCancel(context.Background()) nc.readTimer = time.AfterFunc(math.MaxInt64, cancel) - nc.readTimer.Stop() + if !nc.readTimer.Stop() { + <-nc.readTimer.C + } return nc } @@ -103,23 +108,23 @@ func (c *netConn) Read(p []byte) (int, error) { return n, err } -type unknownAddr struct { +type websocketAddr struct { } -func (a unknownAddr) Network() string { - return "unknown" +func (a websocketAddr) Network() string { + return "websocket" } -func (a unknownAddr) String() string { - return "unknown" +func (a websocketAddr) String() string { + return "websocket/unknown-addr" } func (c *netConn) RemoteAddr() net.Addr { - return unknownAddr{} + return websocketAddr{} } func (c *netConn) LocalAddr() net.Addr { - return unknownAddr{} + return websocketAddr{} } func (c *netConn) SetDeadline(t time.Time) error { From 4d5abcb1cf48b64dfc61cba68ddb9669bc71098e Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 20 Jul 2019 14:00:40 +0000 Subject: [PATCH 058/519] Remove unused dependency in test docker image --- ci/test/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/test/Dockerfile b/ci/test/Dockerfile index 424c56c3..04bd9939 100644 --- a/ci/test/Dockerfile +++ b/ci/test/Dockerfile @@ -6,7 +6,7 @@ LABEL "com.github.actions.icon"="code" LABEL "com.github.actions.color"="green" RUN apt update && \ - apt install -y shellcheck python-pip && \ + apt install -y python-pip && \ pip install autobahntestsuite COPY entrypoint.sh /entrypoint.sh From 01602c919a2449585ada4ace5d037dec7376e7f5 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 20 Jul 2019 14:10:55 +0000 Subject: [PATCH 059/519] Remove usages of apt for apt-get --- ci/lint/Dockerfile | 2 +- ci/test/Dockerfile | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/ci/lint/Dockerfile b/ci/lint/Dockerfile index 5fb46936..3fe06f08 100644 --- a/ci/lint/Dockerfile +++ b/ci/lint/Dockerfile @@ -5,7 +5,7 @@ LABEL "com.github.actions.description"="" LABEL "com.github.actions.icon"="code" LABEL "com.github.actions.color"="purple" -RUN apt update && apt install -y shellcheck +RUN apt-get update && apt-get install -y shellcheck COPY entrypoint.sh /entrypoint.sh diff --git a/ci/test/Dockerfile b/ci/test/Dockerfile index 04bd9939..a2fe695c 100644 --- a/ci/test/Dockerfile +++ b/ci/test/Dockerfile @@ -5,8 +5,8 @@ LABEL "com.github.actions.description"="" LABEL "com.github.actions.icon"="code" LABEL "com.github.actions.color"="green" -RUN apt update && \ - apt install -y python-pip && \ +RUN apt-get update && \ + apt-get install -y python-pip && \ pip install autobahntestsuite COPY entrypoint.sh /entrypoint.sh From 002abf1ecbb0ba78037c104fe9dda81beb7bf0c5 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 23 Jul 2019 20:26:42 -0400 Subject: [PATCH 060/519] Switch to CircleCI Closes #107 --- .circleci/config.yml | 64 ++++++++++++++++++++++++++++++++++++++++++ .github/main.workflow | 21 -------------- ci/.codecov.yml | 1 + ci/bench.sh | 16 +++++++++++ ci/bench/Dockerfile | 10 ------- ci/bench/entrypoint.sh | 15 ---------- ci/fmt.sh | 37 ++++++++++++++++++++++++ ci/fmt/Dockerfile | 10 ------- ci/fmt/entrypoint.sh | 36 ------------------------ ci/lib.sh | 16 +++++------ ci/lint.sh | 15 ++++++++++ ci/lint/Dockerfile | 12 -------- ci/lint/entrypoint.sh | 11 -------- ci/run.sh | 64 +++++------------------------------------- ci/test.sh | 30 ++++++++++++++++++++ ci/test/Dockerfile | 14 --------- ci/test/entrypoint.sh | 24 ---------------- docs/CONTRIBUTING.md | 9 +++++- 18 files changed, 185 insertions(+), 220 deletions(-) create mode 100644 .circleci/config.yml delete mode 100644 .github/main.workflow create mode 100755 ci/bench.sh delete mode 100644 ci/bench/Dockerfile delete mode 100755 ci/bench/entrypoint.sh create mode 100755 ci/fmt.sh delete mode 100644 ci/fmt/Dockerfile delete mode 100755 ci/fmt/entrypoint.sh create mode 100755 ci/lint.sh delete mode 100644 ci/lint/Dockerfile delete mode 100755 ci/lint/entrypoint.sh create mode 100755 ci/test.sh delete mode 100644 ci/test/Dockerfile delete mode 100755 ci/test/entrypoint.sh diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000..b34f651f --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,64 @@ +version: 2 +jobs: + fmt: + docker: + - image: golang:1 + steps: + - checkout + - restore_cache: + keys: + - go-{{ checksum "go.sum" }} + # Fallback to using the latest cache if no exact match is found. + - go- + - run: ./ci/fmt.sh + - save_cache: + paths: + - /go + - /root/.cache/go-build + key: go-{{ checksum "go.sum" }} + + lint: + docker: + - image: golang:1 + steps: + - checkout + - restore_cache: + keys: + - go-{{ checksum "go.sum" }} + # Fallback to using the latest cache if no exact match is found. + - go- + - run: ./ci/lint.sh + - save_cache: + paths: + - /go + - /root/.cache/go-build + key: go-{{ checksum "go.sum" }} + + test: + docker: + - image: golang:1 + steps: + - checkout + - restore_cache: + keys: + - go-{{ checksum "go.sum" }} + # Fallback to using the latest cache if no exact match is found. + - go- + - run: ./ci/test.sh + - save_cache: + paths: + - /go + - /root/.cache/go-build + key: go-{{ checksum "go.sum" }} + +workflows: + version: 2 + fmt: + jobs: + - fmt + lint: + jobs: + - lint + test: + jobs: + - test diff --git a/.github/main.workflow b/.github/main.workflow deleted file mode 100644 index d56d42ef..00000000 --- a/.github/main.workflow +++ /dev/null @@ -1,21 +0,0 @@ -workflow "main" { - on = "push" - resolves = ["fmt", "lint", "test", "bench"] -} - -action "lint" { - uses = "./ci/lint" -} - -action "fmt" { - uses = "./ci/fmt" -} - -action "test" { - uses = "./ci/test" - secrets = ["CODECOV_TOKEN"] -} - -action "bench" { - uses = "./ci/bench" -} diff --git a/ci/.codecov.yml b/ci/.codecov.yml index 7d614ef3..f7eec9f8 100644 --- a/ci/.codecov.yml +++ b/ci/.codecov.yml @@ -1,3 +1,4 @@ +comment: off coverage: status: # Prevent small changes in coverage from failing CI. diff --git a/ci/bench.sh b/ci/bench.sh new file mode 100755 index 00000000..a3d81b26 --- /dev/null +++ b/ci/bench.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash + +set -euo pipefail +cd "$(dirname "${0}")" +source ./lib.sh + +go test --vet=off --run=^$ -bench=. -o=ci/out/websocket.test \ + -cpuprofile=ci/out/cpu.prof \ + -memprofile=ci/out/mem.prof \ + -blockprofile=ci/out/block.prof \ + -mutexprofile=ci/out/mutex.prof \ + . + +echo +echo "Profiles are in ./ci/out/*.prof +Keep in mind that every profiler Go provides is enabled so that may skew the benchmarks." diff --git a/ci/bench/Dockerfile b/ci/bench/Dockerfile deleted file mode 100644 index a2b6c73d..00000000 --- a/ci/bench/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM golang:1.12 - -LABEL "com.github.actions.name"="bench" -LABEL "com.github.actions.description"="" -LABEL "com.github.actions.icon"="code" -LABEL "com.github.actions.color"="red" - -COPY entrypoint.sh /entrypoint.sh - -CMD ["/entrypoint.sh"] diff --git a/ci/bench/entrypoint.sh b/ci/bench/entrypoint.sh deleted file mode 100755 index a8350c9d..00000000 --- a/ci/bench/entrypoint.sh +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env bash - -source ci/lib.sh || exit 1 - -go test --vet=off --run=^$ -bench=. -o=ci/out/websocket.test \ - -cpuprofile=ci/out/cpu.prof \ - -memprofile=ci/out/mem.prof \ - -blockprofile=ci/out/block.prof \ - -mutexprofile=ci/out/mutex.prof \ - . - -set +x -echo -echo "profiles are in ./ci/out/*.prof -keep in mind that every profiler Go provides is enabled so that may skew the benchmarks" diff --git a/ci/fmt.sh b/ci/fmt.sh new file mode 100755 index 00000000..52ef3fd1 --- /dev/null +++ b/ci/fmt.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +set -euo pipefail +cd "$(dirname "${0}")" +source ./lib.sh + +unstaged_files() { + git ls-files --other --modified --exclude-standard +} + +gen() { + # Unfortunately, this is the only way to ensure go.mod and go.sum are correct. + # See https://github.com/golang/go/issues/27005 + go list ./... > /dev/null + go mod tidy + + go generate ./... +} + +fmt() { + gofmt -w -s . + go run go.coder.com/go-tools/cmd/goimports -w "-local=$(go list -m)" . + go run mvdan.cc/sh/cmd/shfmt -i 2 -w -s -sr . +} + +gen +fmt + +if [[ $CI && $(unstaged_files) != "" ]]; then + echo + echo "Files either need generation or are formatted incorrectly." + echo "Please run:" + echo "./ci/fmt.sh" + echo + git status + exit 1 +fi diff --git a/ci/fmt/Dockerfile b/ci/fmt/Dockerfile deleted file mode 100644 index f829f514..00000000 --- a/ci/fmt/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM golang:1.12 - -LABEL "com.github.actions.name"="fmt" -LABEL "com.github.actions.description"="" -LABEL "com.github.actions.icon"="code" -LABEL "com.github.actions.color"="blue" - -COPY entrypoint.sh /entrypoint.sh - -CMD ["/entrypoint.sh"] diff --git a/ci/fmt/entrypoint.sh b/ci/fmt/entrypoint.sh deleted file mode 100755 index f4749ec2..00000000 --- a/ci/fmt/entrypoint.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/usr/bin/env bash - -source ci/lib.sh || exit 1 - -unstaged_files() { - git ls-files --other --modified --exclude-standard -} - -gen() { - # Unfortunately, this is the only way to ensure go.mod and go.sum are correct. - # See https://github.com/golang/go/issues/27005 - go list ./... > /dev/null - go mod tidy - - go generate ./... -} - -fmt() { - gofmt -w -s . - go run go.coder.com/go-tools/cmd/goimports -w "-local=$(go list -m)" . - go run mvdan.cc/sh/cmd/shfmt -w -s -sr . -} - -gen -fmt - -if [[ $CI && $(unstaged_files) != "" ]]; then - set +x - echo - echo "files either need generation or are formatted incorrectly" - echo "please run:" - echo "./ci/run.sh" - echo - git status - exit 1 -fi diff --git a/ci/lib.sh b/ci/lib.sh index 0bc093cd..590e7908 100644 --- a/ci/lib.sh +++ b/ci/lib.sh @@ -1,14 +1,12 @@ #!/usr/bin/env bash -set -euxo pipefail || exit 1 - -export GO111MODULE=on -export PAGER=cat - -# shellcheck disable=SC2034 -# CI is used by the scripts that source this file. -export CI=${GITHUB_ACTION-} +set -euo pipefail +# Ensures $CI can be used if it's set or not. +export CI=${CI:-} if [[ $CI ]]; then - export GOFLAGS=-mod=readonly + export GOFLAGS=-mod=readonly + export DEBIAN_FRONTEND=noninteractive fi + +cd "$(git rev-parse --show-toplevel)" diff --git a/ci/lint.sh b/ci/lint.sh new file mode 100755 index 00000000..65655451 --- /dev/null +++ b/ci/lint.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash + +set -euo pipefail +cd "$(dirname "${0}")" +source ./lib.sh + +if [[ $CI ]]; then + apt-get update -qq + apt-get install -qq shellcheck > /dev/null +fi + +# shellcheck disable=SC2046 +shellcheck -e SC1091 -x $(git ls-files "*.sh") +go vet ./... +go run golang.org/x/lint/golint -set_exit_status ./... diff --git a/ci/lint/Dockerfile b/ci/lint/Dockerfile deleted file mode 100644 index 3fe06f08..00000000 --- a/ci/lint/Dockerfile +++ /dev/null @@ -1,12 +0,0 @@ -FROM golang:1.12 - -LABEL "com.github.actions.name"="lint" -LABEL "com.github.actions.description"="" -LABEL "com.github.actions.icon"="code" -LABEL "com.github.actions.color"="purple" - -RUN apt-get update && apt-get install -y shellcheck - -COPY entrypoint.sh /entrypoint.sh - -CMD ["/entrypoint.sh"] diff --git a/ci/lint/entrypoint.sh b/ci/lint/entrypoint.sh deleted file mode 100755 index 62f74022..00000000 --- a/ci/lint/entrypoint.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash - -source ci/lib.sh || exit 1 - -( - shopt -s globstar nullglob dotglob - shellcheck ./**/*.sh -) - -go vet ./... -go run golang.org/x/lint/golint -set_exit_status ./... diff --git a/ci/run.sh b/ci/run.sh index 9112c83a..53f04278 100755 --- a/ci/run.sh +++ b/ci/run.sh @@ -1,61 +1,11 @@ #!/usr/bin/env bash -# This script is for local testing. See .github for CI. +# This script is for local testing. See .circleci for CI. -cd "$(dirname "${0}")/.." || exit 1 -source ci/lib.sh || exit 1 +set -euo pipefail +cd "$(dirname "${0}")" +source ./lib.sh -function docker_run() { - local DIR="$1" - local IMAGE - IMAGE="$(docker build -q "$DIR")" - docker run \ - -it \ - -v "${PWD}:/repo" \ - -v "$(go env GOPATH):/go" \ - -v "$(go env GOCACHE):/root/.cache/go-build" \ - -w /repo \ - "${IMAGE}" -} - -function help() { - set +x - echo - echo "$0 [-h] " - cat << EOF - -If you do not pass in an explicit step, all steps will be ran in order. -Pass "analyze" as the step to be put into an interactive container to analyze -profiles. -EOF - exit 1 -} - -# Use this to analyze benchmark profiles. -if [[ ${1-} == "analyze" ]]; then - docker run \ - -it \ - -v "${PWD}:/repo" \ - -v "$(go env GOPATH):/go" \ - -v "$(go env GOCACHE):/root/.cache/go-build" \ - -w /repo \ - golang:1.12 -fi - -if [[ ${1-} == "-h" || ${1-} == "--help" || ${1-} == "help" ]]; then - help -fi - -if [[ $# -gt 0 ]]; then - if [[ ! -d "ci/$*" ]]; then - help - fi - - docker_run "ci/$*" - exit 0 -fi - -docker_run ci/fmt -docker_run ci/lint -docker_run ci/test -docker_run ci/bench +./fmt.sh +./lint.sh +./test.sh diff --git a/ci/test.sh b/ci/test.sh new file mode 100755 index 00000000..9cf982a5 --- /dev/null +++ b/ci/test.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -euo pipefail +cd "$(dirname "${0}")" +source ./lib.sh + +echo "This step includes benchmarks for race detection and coverage purposes +but the numbers will be misleading. please see the bench step or ./bench.sh for +more accurate numbers." +echo + +if [[ $CI ]]; then + apt-get update -qq + apt-get install -qq python-pip > /dev/null + # Need to add pip install directory to $PATH. + export PATH="/home/circleci/.local/bin:$PATH" + pip install -qqq autobahntestsuite +fi + +go test -race -coverprofile=ci/out/coverage.prof --vet=off -bench=. -coverpkg=./... ./... +go tool cover -func=ci/out/coverage.prof + +if [[ $CI ]]; then + bash <(curl -s https://codecov.io/bash) -f ci/out/coverage.prof +else + go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html + + echo + echo "Please open ci/out/coverage.html to see detailed test coverage stats." +fi diff --git a/ci/test/Dockerfile b/ci/test/Dockerfile deleted file mode 100644 index a2fe695c..00000000 --- a/ci/test/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM golang:1.12 - -LABEL "com.github.actions.name"="test" -LABEL "com.github.actions.description"="" -LABEL "com.github.actions.icon"="code" -LABEL "com.github.actions.color"="green" - -RUN apt-get update && \ - apt-get install -y python-pip && \ - pip install autobahntestsuite - -COPY entrypoint.sh /entrypoint.sh - -CMD ["/entrypoint.sh"] diff --git a/ci/test/entrypoint.sh b/ci/test/entrypoint.sh deleted file mode 100755 index 81f504a0..00000000 --- a/ci/test/entrypoint.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash - -source ci/lib.sh || exit 1 - -set +x -echo -echo "this step includes benchmarks for race detection and coverage purposes -but the numbers will be misleading. please see the bench step for more -accurate numbers" -echo -set -x - -go test -race -coverprofile=ci/out/coverage.prof --vet=off -bench=. -coverpkg=./... ./... -go tool cover -func=ci/out/coverage.prof - -if [[ $CI ]]; then - bash <(curl -s https://codecov.io/bash) -f ci/out/coverage.prof -else - go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html - - set +x - echo - echo "please open ci/out/coverage.html to see detailed test coverage stats" -fi diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 72149158..ade2013f 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -18,4 +18,11 @@ Be sure to link to an existing issue if one exists. In general, try creating an before making a PR to get some discussion going and to make sure you do not spend time on a PR that may be rejected. -Run `ci/run.sh` to test your changes. You only need docker and bash to run the tests. +Run `ci/run.sh` to test your changes. You'll need [shellcheck](https://github.com/koalaman/shellcheck#installing), the [Autobahn Test suite pip package](https://github.com/crossbario/autobahn-testsuite) and Go. + +See [../ci/lint.sh](../ci/lint.sh) and [../ci/lint.sh](../ci/test.sh) for the +installation of shellcheck and the Autobahn test suite on Debian or Ubuntu. + +For Go, please refer to the [offical docs](https://golang.org/doc/install). + +You can benchmark the library with `./ci/benchmark.sh`. You only need Go to run that script. From c5ea06dfd83d320e8221dc7d8106cf6250cb6d37 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 24 Jul 2019 18:16:49 -0400 Subject: [PATCH 061/519] Ignore write error when echoing close frame Closes #109 --- ci/test.sh | 2 +- websocket.go | 21 ++++++++++++++++----- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/ci/test.sh b/ci/test.sh index 9cf982a5..f08c2ddf 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -17,7 +17,7 @@ if [[ $CI ]]; then pip install -qqq autobahntestsuite fi -go test -race -coverprofile=ci/out/coverage.prof --vet=off -bench=. -coverpkg=./... ./... +go test -race -coverprofile=ci/out/coverage.prof --vet=off -bench=. -coverpkg=./... "$@" ./... go tool cover -func=ci/out/coverage.prof if [[ $CI ]]; then diff --git a/websocket.go b/websocket.go index 47db5f18..80d5511a 100644 --- a/websocket.go +++ b/websocket.go @@ -41,9 +41,10 @@ type Conn struct { closer io.Closer client bool - closeOnce sync.Once - closeErr error - closed chan struct{} + closeOnce sync.Once + closeErrOnce sync.Once + closeErr error + closed chan struct{} // writeMsgLock is acquired to write a data message. writeMsgLock chan struct{} @@ -115,11 +116,17 @@ func (c *Conn) Subprotocol() string { return c.subprotocol } +func (c *Conn) setCloseErr(err error) { + c.closeErrOnce.Do(func() { + c.closeErr = xerrors.Errorf("websocket closed: %w", err) + }) +} + func (c *Conn) close(err error) { c.closeOnce.Do(func() { runtime.SetFinalizer(c, nil) - c.closeErr = xerrors.Errorf("websocket closed: %w", err) + c.setCloseErr(err) close(c.closed) // Have to close after c.closed is closed to ensure any goroutine that wakes up @@ -304,7 +311,11 @@ func (c *Conn) handleControl(ctx context.Context, h header) error { c.Close(StatusProtocolError, "received invalid close payload") return xerrors.Errorf("received invalid close payload: %w", err) } - c.writeClose(b, xerrors.Errorf("received close frame: %w", ce)) + // This ensures the closeErr of the Conn is always the received CloseError + // in case the echo close frame write fails. + // See https://github.com/nhooyr/websocket/issues/109 + c.setCloseErr(xerrors.Errorf("received close frame: %w", ce)) + c.writeClose(b, nil) return c.closeErr default: panic(fmt.Sprintf("websocket: unexpected control opcode: %#v", h)) From 3e63f8260ac9a2f9d59e848168e81df2c3c18dd1 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 24 Jul 2019 18:57:23 -0400 Subject: [PATCH 062/519] Bump README.md version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 419c4630..29ff4520 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ websocket is a minimal and idiomatic WebSocket library for Go. ## Install ```bash -go get nhooyr.io/websocket@v1.3.1 +go get nhooyr.io/websocket@v1.3.2 ``` ## Features From fb30712af122f689784e91a2723e2cd511ccd2d3 Mon Sep 17 00:00:00 2001 From: Toyam Cox Date: Tue, 6 Aug 2019 03:35:49 -0400 Subject: [PATCH 063/519] netconn: Fix SetDeadline methods Closes #111 Closes #112 --- netconn.go | 12 ++++++++++-- websocket_test.go | 4 ++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/netconn.go b/netconn.go index 9c70b6fe..06cbc2f5 100644 --- a/netconn.go +++ b/netconn.go @@ -134,11 +134,19 @@ func (c *netConn) SetDeadline(t time.Time) error { } func (c *netConn) SetWriteDeadline(t time.Time) error { - c.writeTimer.Reset(t.Sub(time.Now())) + if t.IsZero() { + c.writeTimer.Stop() + } else { + c.writeTimer.Reset(t.Sub(time.Now())) + } return nil } func (c *netConn) SetReadDeadline(t time.Time) error { - c.readTimer.Reset(t.Sub(time.Now())) + if t.IsZero() { + c.readTimer.Stop() + } else { + c.readTimer.Reset(t.Sub(time.Now())) + } return nil } diff --git a/websocket_test.go b/websocket_test.go index 1dc5283b..46f9c833 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -130,6 +130,8 @@ func TestHandshake(t *testing.T) { nc := websocket.NetConn(c) defer nc.Close() + nc.SetWriteDeadline(time.Time{}) + time.Sleep(1) nc.SetWriteDeadline(time.Now().Add(time.Second * 15)) for i := 0; i < 3; i++ { @@ -153,6 +155,8 @@ func TestHandshake(t *testing.T) { nc := websocket.NetConn(c) defer nc.Close() + nc.SetReadDeadline(time.Time{}) + time.Sleep(1) nc.SetReadDeadline(time.Now().Add(time.Second * 15)) read := func() error { From 3a7c15d0ee677ce992db1d6df3a7c7c0ed4a5021 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 6 Aug 2019 22:13:14 -0600 Subject: [PATCH 064/519] Bump version in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 29ff4520..5d07c1ba 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ websocket is a minimal and idiomatic WebSocket library for Go. ## Install ```bash -go get nhooyr.io/websocket@v1.3.2 +go get nhooyr.io/websocket@v1.3.3 ``` ## Features From 5028f225d6ace0e770e99b69beca74666d3641ca Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 15 Aug 2019 16:46:32 -0700 Subject: [PATCH 065/519] Clarify Ping API in README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 5d07c1ba..7cbc68db 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,8 @@ makes it very easy to close the connection with a status code and reason. The ping API is also nicer. gorilla/websocket requires registering a pong handler on the Conn which results in awkward control flow. With nhooyr/websocket you use the Ping method on the Conn -that sends a ping and also waits for the pong. +that sends a ping and also waits for the pong, though you must be reading from the connection +for the pong to be read. In terms of performance, the differences mostly depend on your application code. nhooyr/websocket reuses message buffers out of the box if you use the wsjson and wspb subpackages. From 711cce472d00c014c451069bb09a1b5e8c911154 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 15 Aug 2019 16:51:07 -0700 Subject: [PATCH 066/519] Add msgType parameter to NetConn adapter Closes #113 --- netconn.go | 24 +++++++++++++++--------- websocket_test.go | 4 ++-- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/netconn.go b/netconn.go index 06cbc2f5..25787703 100644 --- a/netconn.go +++ b/netconn.go @@ -2,6 +2,7 @@ package websocket import ( "context" + "fmt" "io" "math" "net" @@ -17,8 +18,11 @@ import ( // correctly and so provided in the library. // See https://github.com/nhooyr/websocket/issues/100. // -// Every Write to the net.Conn will correspond to a binary message -// write on *webscoket.Conn. +// Every Write to the net.Conn will correspond to a message write of +// the given type on *websocket.Conn. +// +// If a message is read that is not of the correct type, an error +// will be thrown. // // Close will close the *websocket.Conn with StatusNormalClosure. // @@ -30,9 +34,10 @@ import ( // and "websocket/unknown-addr" for String. // // A received StatusNormalClosure close frame will be translated to EOF when reading. -func NetConn(c *Conn) net.Conn { +func NetConn(c *Conn, msgType MessageType) net.Conn { nc := &netConn{ - c: c, + c: c, + msgType: msgType, } var cancel context.CancelFunc @@ -52,7 +57,8 @@ func NetConn(c *Conn) net.Conn { } type netConn struct { - c *Conn + c *Conn + msgType MessageType writeTimer *time.Timer writeContext context.Context @@ -71,7 +77,7 @@ func (c *netConn) Close() error { } func (c *netConn) Write(p []byte) (int, error) { - err := c.c.Write(c.writeContext, MessageBinary, p) + err := c.c.Write(c.writeContext, c.msgType, p) if err != nil { return 0, err } @@ -93,9 +99,9 @@ func (c *netConn) Read(p []byte) (int, error) { } return 0, err } - if typ != MessageBinary { - c.c.Close(StatusUnsupportedData, "can only accept binary messages") - return 0, xerrors.Errorf("unexpected frame type read for net conn adapter (expected %v): %v", MessageBinary, typ) + if typ != c.msgType { + c.c.Close(StatusUnsupportedData, fmt.Sprintf("can only accept %v messages", c.msgType)) + return 0, xerrors.Errorf("unexpected frame type read for net conn adapter (expected %v): %v", c.msgType, typ) } c.reader = r } diff --git a/websocket_test.go b/websocket_test.go index 46f9c833..06e0fc6d 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -127,7 +127,7 @@ func TestHandshake(t *testing.T) { } defer c.Close(websocket.StatusInternalError, "") - nc := websocket.NetConn(c) + nc := websocket.NetConn(c, websocket.MessageBinary) defer nc.Close() nc.SetWriteDeadline(time.Time{}) @@ -152,7 +152,7 @@ func TestHandshake(t *testing.T) { } defer c.Close(websocket.StatusInternalError, "") - nc := websocket.NetConn(c) + nc := websocket.NetConn(c, websocket.MessageBinary) defer nc.Close() nc.SetReadDeadline(time.Time{}) From 922891235cc2b0823eb2e57e352b16de7d1001ba Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 15 Aug 2019 16:56:13 -0700 Subject: [PATCH 067/519] Clarify Reader semantics --- websocket.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/websocket.go b/websocket.go index 80d5511a..35ddb024 100644 --- a/websocket.go +++ b/websocket.go @@ -333,6 +333,10 @@ func (c *Conn) handleControl(ctx context.Context, h header) error { // This applies to the Read methods in the wsjson/wspb subpackages as well. // // You must read from the connection for control frames to be handled. +// Thus if you expect messages to take a long time to be responded to, +// you should handle such messages async to reading from the connection +// to ensure control frames are promptly handled. +// // If you do not expect any data messages from the peer, call CloseRead. // // Only one Reader may be open at a time. From dd38de53c291cc0b4b5848d7b74d774a6a5a9637 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 16 Aug 2019 14:09:47 -0700 Subject: [PATCH 068/519] Bump README.md version --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7cbc68db..d30ee5f5 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ websocket is a minimal and idiomatic WebSocket library for Go. ## Install ```bash -go get nhooyr.io/websocket@v1.3.3 +go get nhooyr.io/websocket@v1.4.0 ``` ## Features From 80ddbb42f535c230df2be82bf701b5660e63e114 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 18 Aug 2019 21:58:24 -0400 Subject: [PATCH 069/519] Simplify some docs --- websocket.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/websocket.go b/websocket.go index 35ddb024..ee61f543 100644 --- a/websocket.go +++ b/websocket.go @@ -600,8 +600,7 @@ func (c *Conn) writer(ctx context.Context, typ MessageType) (io.WriteCloser, err // Write is a convenience method to write a message to the connection. // -// See the Writer method if you want to stream a message. The docs on Writer -// regarding concurrency also apply to this method. +// See the Writer method if you want to stream a message. func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { _, err := c.write(ctx, typ, p) if err != nil { @@ -876,9 +875,9 @@ func init() { // Ping sends a ping to the peer and waits for a pong. // Use this to measure latency or ensure the peer is responsive. -// Ping must be called concurrently with Reader as otherwise it does -// not read from the connection and relies on Reader to unblock -// when the pong arrives. +// Ping must be called concurrently with Reader as it does +// not read from the connection but instead waits for a Reader call +// to read the pong. // // TCP Keepalives should suffice for most use cases. func (c *Conn) Ping(ctx context.Context) error { From 63f27e246b5bad3c2d0cc674be91fa03bae40d36 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 20 Aug 2019 17:46:00 -0400 Subject: [PATCH 070/519] Reduce Reader/Writer allocations Closes #116 --- websocket.go | 43 +++++++++++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 16 deletions(-) diff --git a/websocket.go b/websocket.go index ee61f543..5942b68c 100644 --- a/websocket.go +++ b/websocket.go @@ -56,6 +56,8 @@ type Conn struct { // read limit for a message in bytes. msgReadLimit int64 + // Used to ensure a previous writer is not used after being closed. + activeWriter *messageWriter // messageWriter state. writeMsgOpcode opcode writeMsgCtx context.Context @@ -63,7 +65,7 @@ type Conn struct { // Used to ensure the previous reader is read till EOF before allowing // a new one. - previousReader *messageReader + activeReader *messageReader // readFrameLock is acquired to read from bw. readFrameLock chan struct{} readClosed int64 @@ -358,7 +360,7 @@ func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) { } func (c *Conn) reader(ctx context.Context) (MessageType, io.Reader, error) { - if c.previousReader != nil && !c.readFrameEOF { + if c.activeReader != nil && !c.readFrameEOF { // The only way we know for sure the previous reader is not yet complete is // if there is an active frame not yet fully read. // Otherwise, a user may have read the last byte but not the EOF if the EOF @@ -371,7 +373,7 @@ func (c *Conn) reader(ctx context.Context) (MessageType, io.Reader, error) { return 0, nil, err } - if c.previousReader != nil && !c.previousReader.eof { + if c.activeReader != nil && !c.activeReader.eof() { if h.opcode != opContinuation { err := xerrors.Errorf("received new data message without finishing the previous message") c.Close(StatusProtocolError, err.Error()) @@ -382,7 +384,7 @@ func (c *Conn) reader(ctx context.Context) (MessageType, io.Reader, error) { return 0, nil, xerrors.Errorf("previous message not read to completion") } - c.previousReader.eof = true + c.activeReader = nil h, err = c.readTillMsg(ctx) if err != nil { @@ -403,7 +405,7 @@ func (c *Conn) reader(ctx context.Context) (MessageType, io.Reader, error) { r := &messageReader{ c: c, } - c.previousReader = r + c.activeReader = r return MessageType(h.opcode), r, nil } @@ -430,8 +432,11 @@ func (c *Conn) CloseRead(ctx context.Context) context.Context { // messageReader enables reading a data frame from the WebSocket connection. type messageReader struct { - c *Conn - eof bool + c *Conn +} + +func (r *messageReader) eof() bool { + return r.c.activeReader != r } // Read reads as many bytes as possible into p. @@ -449,7 +454,7 @@ func (r *messageReader) Read(p []byte) (int, error) { } func (r *messageReader) read(p []byte) (int, error) { - if r.eof { + if r.eof() { return 0, xerrors.Errorf("cannot use EOFed reader") } @@ -502,7 +507,7 @@ func (r *messageReader) read(p []byte) (int, error) { r.c.readFrameEOF = true if h.fin { - r.eof = true + r.c.activeReader = nil return n, io.EOF } } @@ -593,9 +598,11 @@ func (c *Conn) writer(ctx context.Context, typ MessageType) (io.WriteCloser, err } c.writeMsgCtx = ctx c.writeMsgOpcode = opcode(typ) - return &messageWriter{ + w := &messageWriter{ c: c, - }, nil + } + c.activeWriter = w + return w, nil } // Write is a convenience method to write a message to the connection. @@ -622,8 +629,11 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error // messageWriter enables writing to a WebSocket connection. type messageWriter struct { - c *Conn - closed bool + c *Conn +} + +func (w *messageWriter) closed() bool { + return w != w.c.activeWriter } // Write writes the given bytes to the WebSocket connection. @@ -636,7 +646,7 @@ func (w *messageWriter) Write(p []byte) (int, error) { } func (w *messageWriter) write(p []byte) (int, error) { - if w.closed { + if w.closed() { return 0, xerrors.Errorf("cannot use closed writer") } n, err := w.c.writeFrame(w.c.writeMsgCtx, false, w.c.writeMsgOpcode, p) @@ -658,16 +668,17 @@ func (w *messageWriter) Close() error { } func (w *messageWriter) close() error { - if w.closed { + if w.closed() { return xerrors.Errorf("cannot use closed writer") } - w.closed = true + w.closed() _, err := w.c.writeFrame(w.c.writeMsgCtx, true, w.c.writeMsgOpcode, nil) if err != nil { return xerrors.Errorf("failed to write fin frame: %w", err) } + w.c.activeWriter = nil w.c.releaseLock(w.c.writeMsgLock) return nil } From 01721c99284c8189c308d699e006d2d2a0cfd553 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 20 Aug 2019 17:46:59 -0400 Subject: [PATCH 071/519] Improve reliability of wstest in tests Ensures each invocation gets a unique address to listen on. --- websocket_test.go | 40 +++++++++++++++++++++++++++++----------- 1 file changed, 29 insertions(+), 11 deletions(-) diff --git a/websocket_test.go b/websocket_test.go index 06e0fc6d..be592d91 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "io/ioutil" + "net" "net/http" "net/http/cookiejar" "net/http/httptest" @@ -781,12 +782,28 @@ func discardLoop(ctx context.Context, c *websocket.Conn) { } } +func unusedListenAddr() (string, error) { + l, err := net.Listen("tcp", "localhost:0") + if err != nil { + return "", err + } + l.Close() + return l.Addr().String(), nil +} + // https://github.com/crossbario/autobahn-python/blob/master/wstest/testee_client_aio.py func TestAutobahnClient(t *testing.T) { t.Parallel() + serverAddr, err := unusedListenAddr() + if err != nil { + t.Fatalf("failed to get unused listen addr for wstest: %v", err) + } + + wsServerURL := "ws://" + serverAddr + spec := map[string]interface{}{ - "url": "ws://localhost:9001", + "url": wsServerURL, "outdir": "ci/out/wstestClientReports", "cases": []string{"*"}, // See TestAutobahnServer for the reasons why we exclude these. @@ -814,9 +831,10 @@ func TestAutobahnClient(t *testing.T) { ctx, cancel := context.WithTimeout(ctx, time.Minute*10) defer cancel() - args := []string{"--mode", "fuzzingserver", "--spec", specFile.Name()} - if os.Getenv("CI") == "" { - args = append([]string{"--debug"}, args...) + args := []string{"--mode", "fuzzingserver", "--spec", specFile.Name(), + // Disables some server that runs as part of fuzzingserver mode. + // See https://github.com/crossbario/autobahn-testsuite/blob/058db3a36b7c3a1edf68c282307c6b899ca4857f/autobahntestsuite/autobahntestsuite/wstest.py#L124 + "--webport=0", } wstest := exec.CommandContext(ctx, "wstest", args...) err = wstest.Start() @@ -835,9 +853,9 @@ func TestAutobahnClient(t *testing.T) { var cases int func() { - c, _, err := websocket.Dial(ctx, "ws://localhost:9001/getCaseCount", websocket.DialOptions{}) + c, _, err := websocket.Dial(ctx, wsServerURL+"/getCaseCount", websocket.DialOptions{}) if err != nil { - t.Fatalf("failed to dial: %v", err) + t.Fatal(err) } defer c.Close(websocket.StatusInternalError, "") @@ -862,17 +880,17 @@ func TestAutobahnClient(t *testing.T) { ctx, cancel := context.WithTimeout(ctx, time.Second*45) defer cancel() - c, _, err := websocket.Dial(ctx, fmt.Sprintf("ws://localhost:9001/runCase?case=%v&agent=main", i), websocket.DialOptions{}) + c, _, err := websocket.Dial(ctx, fmt.Sprintf(wsServerURL+"/runCase?case=%v&agent=main", i), websocket.DialOptions{}) if err != nil { - t.Fatalf("failed to dial: %v", err) + t.Fatal(err) } echoLoop(ctx, c) }() } - c, _, err := websocket.Dial(ctx, fmt.Sprintf("ws://localhost:9001/updateReports?agent=main"), websocket.DialOptions{}) + c, _, err := websocket.Dial(ctx, fmt.Sprintf(wsServerURL+"/updateReports?agent=main"), websocket.DialOptions{}) if err != nil { - t.Fatalf("failed to dial: %v", err) + t.Fatal(err) } c.Close(websocket.StatusNormalClosure, "") @@ -944,7 +962,7 @@ func benchConn(b *testing.B, echo, stream bool, size int) { c, _, err := websocket.Dial(ctx, wsURL, websocket.DialOptions{}) if err != nil { - b.Fatalf("failed to dial: %v", err) + b.Fatal(err) } defer c.Close(websocket.StatusInternalError, "") From 7a64dc4dbc9bab2d40ffcde4a9a498cafa5f6158 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 20 Aug 2019 17:47:10 -0400 Subject: [PATCH 072/519] Improve CI and add prettier --- .circleci/config.yml | 2 ++ README.md | 12 ++++++------ ci/.gitignore | 1 + ci/fmt.sh | 37 +++++++++++++++++++------------------ ci/lib.sh | 4 +--- ci/lint.sh | 2 +- ci/out/.gitignore | 1 - ci/run.sh | 6 +++--- ci/test.sh | 17 +++++------------ docs/CONTRIBUTING.md | 20 ++++++++++++++++---- websocket_test.go | 4 +--- 11 files changed, 55 insertions(+), 51 deletions(-) create mode 100644 ci/.gitignore delete mode 100644 ci/out/.gitignore diff --git a/.circleci/config.yml b/.circleci/config.yml index b34f651f..9daf2b9b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -45,6 +45,8 @@ jobs: # Fallback to using the latest cache if no exact match is found. - go- - run: ./ci/test.sh + - store_artifacts: + path: out - save_cache: paths: - /go diff --git a/README.md b/README.md index d30ee5f5..f3bd6a84 100644 --- a/README.md +++ b/README.md @@ -42,15 +42,15 @@ http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), time.Second*10) defer cancel() - + var v interface{} err = wsjson.Read(ctx, c, &v) if err != nil { // ... } - + log.Printf("received: %v", v) - + c.Close(websocket.StatusNormalClosure, "") }) ``` @@ -93,7 +93,7 @@ c.Close(websocket.StatusNormalClosure, "") ## Comparison Before the comparison, I want to point out that both gorilla/websocket and gobwas/ws were -extremely useful in implementing the WebSocket protocol correctly so *big thanks* to the +extremely useful in implementing the WebSocket protocol correctly so _big thanks_ to the authors of both. In particular, I made sure to go through the issue tracker of gorilla/websocket to ensure I implemented details correctly and understood how people were using WebSockets in production. @@ -111,7 +111,7 @@ Just compare the godoc of The API for nhooyr/websocket has been designed such that there is only one way to do things which makes it easy to use correctly. Not only is the API simpler, the implementation is only 1700 lines whereas gorilla/websocket is at 3500 lines. That's more code to maintain, - more code to test, more code to document and more surface area for bugs. +more code to test, more code to document and more surface area for bugs. Moreover, nhooyr/websocket has support for newer Go idioms such as context.Context and also uses net/http's Client and ResponseWriter directly for WebSocket handshakes. @@ -151,7 +151,7 @@ and clarity. This library is fantastic in terms of performance. The author put in significant effort to ensure its speed and I have applied as many of its optimizations as -I could into nhooyr/websocket. Definitely check out his fantastic [blog post](https://medium.freecodecamp.org/million-websockets-and-go-cc58418460bb) +I could into nhooyr/websocket. Definitely check out his fantastic [blog post](https://medium.freecodecamp.org/million-websockets-and-go-cc58418460bb) about performant WebSocket servers. If you want a library that gives you absolute control over everything, this is the library, diff --git a/ci/.gitignore b/ci/.gitignore new file mode 100644 index 00000000..1fcb1529 --- /dev/null +++ b/ci/.gitignore @@ -0,0 +1 @@ +out diff --git a/ci/fmt.sh b/ci/fmt.sh index 52ef3fd1..2d3ef4f8 100755 --- a/ci/fmt.sh +++ b/ci/fmt.sh @@ -4,29 +4,30 @@ set -euo pipefail cd "$(dirname "${0}")" source ./lib.sh -unstaged_files() { - git ls-files --other --modified --exclude-standard -} +# Unfortunately, this is the only way to ensure go.mod and go.sum are correct. +# See https://github.com/golang/go/issues/27005 +go list ./... > /dev/null +go mod tidy -gen() { - # Unfortunately, this is the only way to ensure go.mod and go.sum are correct. - # See https://github.com/golang/go/issues/27005 - go list ./... > /dev/null - go mod tidy +go generate ./... - go generate ./... -} +gofmt -w -s . +go run go.coder.com/go-tools/cmd/goimports -w "-local=$(go list -m)" . +go run mvdan.cc/sh/cmd/shfmt -i 2 -w -s -sr . +# shellcheck disable=SC2046 +npx prettier \ + --write \ + --print-width 120 \ + --no-semi \ + --trailing-comma all \ + --loglevel silent \ + $(git ls-files "*.yaml" "*.yml" "*.md") -fmt() { - gofmt -w -s . - go run go.coder.com/go-tools/cmd/goimports -w "-local=$(go list -m)" . - go run mvdan.cc/sh/cmd/shfmt -i 2 -w -s -sr . +unstaged_files() { + git ls-files --other --modified --exclude-standard } -gen -fmt - -if [[ $CI && $(unstaged_files) != "" ]]; then +if [[ ${CI:-} && $(unstaged_files) != "" ]]; then echo echo "Files either need generation or are formatted incorrectly." echo "Please run:" diff --git a/ci/lib.sh b/ci/lib.sh index 590e7908..080ac002 100644 --- a/ci/lib.sh +++ b/ci/lib.sh @@ -2,9 +2,7 @@ set -euo pipefail -# Ensures $CI can be used if it's set or not. -export CI=${CI:-} -if [[ $CI ]]; then +if [[ ${CI:-} ]]; then export GOFLAGS=-mod=readonly export DEBIAN_FRONTEND=noninteractive fi diff --git a/ci/lint.sh b/ci/lint.sh index 65655451..ae5642ef 100755 --- a/ci/lint.sh +++ b/ci/lint.sh @@ -4,7 +4,7 @@ set -euo pipefail cd "$(dirname "${0}")" source ./lib.sh -if [[ $CI ]]; then +if [[ ${CI:-} ]]; then apt-get update -qq apt-get install -qq shellcheck > /dev/null fi diff --git a/ci/out/.gitignore b/ci/out/.gitignore deleted file mode 100644 index 72e8ffc0..00000000 --- a/ci/out/.gitignore +++ /dev/null @@ -1 +0,0 @@ -* diff --git a/ci/run.sh b/ci/run.sh index 53f04278..8bf19058 100755 --- a/ci/run.sh +++ b/ci/run.sh @@ -6,6 +6,6 @@ set -euo pipefail cd "$(dirname "${0}")" source ./lib.sh -./fmt.sh -./lint.sh -./test.sh +./ci/fmt.sh +./ci/lint.sh +./ci/test.sh diff --git a/ci/test.sh b/ci/test.sh index f08c2ddf..d0057352 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -4,12 +4,7 @@ set -euo pipefail cd "$(dirname "${0}")" source ./lib.sh -echo "This step includes benchmarks for race detection and coverage purposes -but the numbers will be misleading. please see the bench step or ./bench.sh for -more accurate numbers." -echo - -if [[ $CI ]]; then +if [[ ${CI:-} ]]; then apt-get update -qq apt-get install -qq python-pip > /dev/null # Need to add pip install directory to $PATH. @@ -17,14 +12,12 @@ if [[ $CI ]]; then pip install -qqq autobahntestsuite fi -go test -race -coverprofile=ci/out/coverage.prof --vet=off -bench=. -coverpkg=./... "$@" ./... -go tool cover -func=ci/out/coverage.prof +# If you'd like to modify the args to go test, just run go test directly, this script is meant +# for running tests at the end to get coverage and test under the race detector. +go test -race -vet=off -coverprofile=ci/out/coverage.prof -coverpkg=./... ./... -if [[ $CI ]]; then +if [[ ${CI:-} ]]; then bash <(curl -s https://codecov.io/bash) -f ci/out/coverage.prof else go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html - - echo - echo "Please open ci/out/coverage.html to see detailed test coverage stats." fi diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index ade2013f..951a3267 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -12,17 +12,29 @@ Please capitalize the first word in the commit message title. The commit message title should use the verb tense + phrase that completes the blank in -> This change modifies websocket to ___________ +> This change modifies websocket to \_\_\_\_\_\_\_\_\_ Be sure to link to an existing issue if one exists. In general, try creating an issue before making a PR to get some discussion going and to make sure you do not spend time on a PR that may be rejected. -Run `ci/run.sh` to test your changes. You'll need [shellcheck](https://github.com/koalaman/shellcheck#installing), the [Autobahn Test suite pip package](https://github.com/crossbario/autobahn-testsuite) and Go. +You can run tests normally with `go test`. +You'll need the [Autobahn Test suite pip package](https://github.com/crossbario/autobahn-testsuite). -See [../ci/lint.sh](../ci/lint.sh) and [../ci/lint.sh](../ci/test.sh) for the +On submission please check if CI has passed and if not, please correct your code such that it does. +If necessary, you may run CI locally with the `ci/run.sh` script. +You'll only need [shellcheck](https://github.com/koalaman/shellcheck#installing). + +For coverage details locally, please see `ci/out/coverage.html` after running `ci/run.sh` or `ci/test.sh`. +For remote coverage, you can use either [codecov](https://codecov.io/gh/nhooyr/websocket) or download the +`coverage.html` generated by the go tooling as an artifact on CI. + +You can also run any of the steps individually. All of them are scripts in the `ci` directory. + +See [../ci/lint.sh](../ci/lint.sh) and [../ci/test.sh](../ci/test.sh) for the installation of shellcheck and the Autobahn test suite on Debian or Ubuntu. For Go, please refer to the [offical docs](https://golang.org/doc/install). -You can benchmark the library with `./ci/benchmark.sh`. You only need Go to run that script. +You can benchmark the library with `./ci/benchmark.sh`. You only need Go to run that script. Benchmark +profiles generated by that script are also available for every CI job as artifacts. diff --git a/websocket_test.go b/websocket_test.go index be592d91..23977095 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -933,9 +933,7 @@ func checkWSTestIndex(t *testing.T, path string) { if failed { path = strings.Replace(path, ".json", ".html", 1) if os.Getenv("CI") == "" { - t.Errorf("wstest found failure, please see %q", path) - } else { - t.Errorf("wstest found failure, please run test.sh locally to see %q", path) + t.Errorf("wstest found failure, please see %q (output as an artifact in CI)", path) } } } From 29412064484c0d331096628fc00f9441b3e147eb Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 20 Aug 2019 19:37:35 -0400 Subject: [PATCH 073/519] Update CONTRIBUTING.md --- docs/CONTRIBUTING.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 951a3267..adc1bd0d 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -20,21 +20,24 @@ on a PR that may be rejected. You can run tests normally with `go test`. You'll need the [Autobahn Test suite pip package](https://github.com/crossbario/autobahn-testsuite). +In the future this dependency will be removed. See [#117](https://github.com/nhooyr/websocket/issues/117). On submission please check if CI has passed and if not, please correct your code such that it does. -If necessary, you may run CI locally with the `ci/run.sh` script. -You'll only need [shellcheck](https://github.com/koalaman/shellcheck#installing). +If necessary, you may run CI locally with the `ci/run.sh` script which will fmt, lint and test your code +with coverage. +You'll need [shellcheck](https://github.com/koalaman/shellcheck#installing), node and the +Autobahn Test suite linked above. For coverage details locally, please see `ci/out/coverage.html` after running `ci/run.sh` or `ci/test.sh`. For remote coverage, you can use either [codecov](https://codecov.io/gh/nhooyr/websocket) or download the -`coverage.html` generated by the go tooling as an artifact on CI. +`coverage.html` artifact on the test step in CI. -You can also run any of the steps individually. All of them are scripts in the `ci` directory. +You can also run any of the CI steps individually. All of them are scripts in the `ci` directory. See [../ci/lint.sh](../ci/lint.sh) and [../ci/test.sh](../ci/test.sh) for the -installation of shellcheck and the Autobahn test suite on Debian or Ubuntu. +installation of shellcheck, node and the Autobahn test suite on Debian or Ubuntu. For Go, please refer to the [offical docs](https://golang.org/doc/install). You can benchmark the library with `./ci/benchmark.sh`. You only need Go to run that script. Benchmark -profiles generated by that script are also available for every CI job as artifacts. +profiles generated by that script are also available as artifacts on the bench step. \ No newline at end of file From 2d4daf33eba365f39b6f0c859f26867dbd760d25 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 20 Aug 2019 19:38:04 -0400 Subject: [PATCH 074/519] Improve CI and have it use alpine instead Personal preference and nice documentation on how to setup deps. --- .circleci/config.yml | 27 +++++++++++++++++--- ci/Dockerfile | 16 ++++++++++++ ci/bench.sh | 2 +- ci/fmt.sh | 60 ++++++++++++++++++++++++++------------------ ci/lib.sh | 5 ---- ci/lint.sh | 5 ---- ci/test.sh | 8 ------ 7 files changed, 75 insertions(+), 48 deletions(-) create mode 100644 ci/Dockerfile diff --git a/.circleci/config.yml b/.circleci/config.yml index 9daf2b9b..11495450 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2 jobs: fmt: docker: - - image: golang:1 + - image: nhooyr/websocket-ci steps: - checkout - restore_cache: @@ -19,7 +19,7 @@ jobs: lint: docker: - - image: golang:1 + - image: nhooyr/websocket-ci steps: - checkout - restore_cache: @@ -36,7 +36,7 @@ jobs: test: docker: - - image: golang:1 + - image: nhooyr/websocket-ci steps: - checkout - restore_cache: @@ -46,7 +46,26 @@ jobs: - go- - run: ./ci/test.sh - store_artifacts: - path: out + path: ci/out + - save_cache: + paths: + - /go + - /root/.cache/go-build + key: go-{{ checksum "go.sum" }} + + bench: + docker: + - image: nhooyr/websocket-ci + steps: + - checkout + - restore_cache: + keys: + - go-{{ checksum "go.sum" }} + # Fallback to using the latest cache if no exact match is found. + - go- + - run: ./ci/bench.sh + - store_artifacts: + path: ci/out - save_cache: paths: - /go diff --git a/ci/Dockerfile b/ci/Dockerfile new file mode 100644 index 00000000..f8064198 --- /dev/null +++ b/ci/Dockerfile @@ -0,0 +1,16 @@ +FROM golang:1-alpine + +RUN apk add --update bash git + +# Autobahn Test suite. +RUN apk add py2-pip python2-dev gcc libffi-dev libc-dev openssl-dev && \ + pip install autobahntestsuite + +# prettier. +RUN apk add npm && npm install -g prettier + +# shellcheck +RUN apk add xz curl && \ + curl -o /tmp/shellcheck.tar.xz -L https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.x86_64.tar.xz && \ + tar -xf /tmp/shellcheck.tar.xz && \ + ln -s "${PWD}/shellcheck-stable/shellcheck" /usr/local/bin/shellcheck \ No newline at end of file diff --git a/ci/bench.sh b/ci/bench.sh index a3d81b26..d9249e6d 100755 --- a/ci/bench.sh +++ b/ci/bench.sh @@ -4,7 +4,7 @@ set -euo pipefail cd "$(dirname "${0}")" source ./lib.sh -go test --vet=off --run=^$ -bench=. -o=ci/out/websocket.test \ +go test -vet=off -run=^$ -bench=. -o=ci/out/websocket.test \ -cpuprofile=ci/out/cpu.prof \ -memprofile=ci/out/mem.prof \ -blockprofile=ci/out/block.prof \ diff --git a/ci/fmt.sh b/ci/fmt.sh index 2d3ef4f8..ce277afc 100755 --- a/ci/fmt.sh +++ b/ci/fmt.sh @@ -4,35 +4,45 @@ set -euo pipefail cd "$(dirname "${0}")" source ./lib.sh -# Unfortunately, this is the only way to ensure go.mod and go.sum are correct. -# See https://github.com/golang/go/issues/27005 -go list ./... > /dev/null -go mod tidy +gen() { + # Unfortunately, this is the only way to ensure go.mod and go.sum are correct. + # See https://github.com/golang/go/issues/27005 + go list ./... > /dev/null + go mod tidy -go generate ./... + go generate ./... +} -gofmt -w -s . -go run go.coder.com/go-tools/cmd/goimports -w "-local=$(go list -m)" . -go run mvdan.cc/sh/cmd/shfmt -i 2 -w -s -sr . -# shellcheck disable=SC2046 -npx prettier \ - --write \ - --print-width 120 \ - --no-semi \ - --trailing-comma all \ - --loglevel silent \ - $(git ls-files "*.yaml" "*.yml" "*.md") +fmt() { + gofmt -w -s . + go run go.coder.com/go-tools/cmd/goimports -w "-local=$(go list -m)" . + go run mvdan.cc/sh/cmd/shfmt -i 2 -w -s -sr . + # shellcheck disable=SC2046 + npx prettier \ + --write \ + --print-width 120 \ + --no-semi \ + --trailing-comma all \ + --loglevel silent \ + $(git ls-files "*.yaml" "*.yml" "*.md") +} unstaged_files() { git ls-files --other --modified --exclude-standard } -if [[ ${CI:-} && $(unstaged_files) != "" ]]; then - echo - echo "Files either need generation or are formatted incorrectly." - echo "Please run:" - echo "./ci/fmt.sh" - echo - git status - exit 1 -fi +check() { + if [[ ${CI:-} && $(unstaged_files) != "" ]]; then + echo + echo "Files either need generation or are formatted incorrectly." + echo "Please run:" + echo "./ci/fmt.sh" + echo + git status + exit 1 + fi +} + +gen +fmt +check diff --git a/ci/lib.sh b/ci/lib.sh index 080ac002..4cfe1ff9 100644 --- a/ci/lib.sh +++ b/ci/lib.sh @@ -2,9 +2,4 @@ set -euo pipefail -if [[ ${CI:-} ]]; then - export GOFLAGS=-mod=readonly - export DEBIAN_FRONTEND=noninteractive -fi - cd "$(git rev-parse --show-toplevel)" diff --git a/ci/lint.sh b/ci/lint.sh index ae5642ef..00dcbd38 100755 --- a/ci/lint.sh +++ b/ci/lint.sh @@ -4,11 +4,6 @@ set -euo pipefail cd "$(dirname "${0}")" source ./lib.sh -if [[ ${CI:-} ]]; then - apt-get update -qq - apt-get install -qq shellcheck > /dev/null -fi - # shellcheck disable=SC2046 shellcheck -e SC1091 -x $(git ls-files "*.sh") go vet ./... diff --git a/ci/test.sh b/ci/test.sh index d0057352..1079d543 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -4,14 +4,6 @@ set -euo pipefail cd "$(dirname "${0}")" source ./lib.sh -if [[ ${CI:-} ]]; then - apt-get update -qq - apt-get install -qq python-pip > /dev/null - # Need to add pip install directory to $PATH. - export PATH="/home/circleci/.local/bin:$PATH" - pip install -qqq autobahntestsuite -fi - # If you'd like to modify the args to go test, just run go test directly, this script is meant # for running tests at the end to get coverage and test under the race detector. go test -race -vet=off -coverprofile=ci/out/coverage.prof -coverpkg=./... ./... From 1e606557d0a5a958d806a38bf314f94bc51266bd Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 20 Aug 2019 19:49:15 -0400 Subject: [PATCH 075/519] Remove useless ci/lib.sh and add script to push docker image --- .circleci/config.yml | 3 +++ ci/bench.sh | 3 ++- ci/fmt.sh | 2 +- ci/{ => image}/Dockerfile | 0 ci/image/push.sh | 7 +++++++ ci/lib.sh | 5 ----- ci/lint.sh | 2 +- ci/run.sh | 2 +- ci/test.sh | 3 ++- docs/CONTRIBUTING.md | 2 +- 10 files changed, 18 insertions(+), 11 deletions(-) rename ci/{ => image}/Dockerfile (100%) create mode 100755 ci/image/push.sh delete mode 100644 ci/lib.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index 11495450..59350e09 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -83,3 +83,6 @@ workflows: test: jobs: - test + bench: + jobs: + - bench \ No newline at end of file diff --git a/ci/bench.sh b/ci/bench.sh index d9249e6d..98a74af7 100755 --- a/ci/bench.sh +++ b/ci/bench.sh @@ -2,8 +2,9 @@ set -euo pipefail cd "$(dirname "${0}")" -source ./lib.sh +cd "$(git rev-parse --show-toplevel)" +mkdir -p ci/out go test -vet=off -run=^$ -bench=. -o=ci/out/websocket.test \ -cpuprofile=ci/out/cpu.prof \ -memprofile=ci/out/mem.prof \ diff --git a/ci/fmt.sh b/ci/fmt.sh index ce277afc..cd1e7b15 100755 --- a/ci/fmt.sh +++ b/ci/fmt.sh @@ -2,7 +2,7 @@ set -euo pipefail cd "$(dirname "${0}")" -source ./lib.sh +cd "$(git rev-parse --show-toplevel)" gen() { # Unfortunately, this is the only way to ensure go.mod and go.sum are correct. diff --git a/ci/Dockerfile b/ci/image/Dockerfile similarity index 100% rename from ci/Dockerfile rename to ci/image/Dockerfile diff --git a/ci/image/push.sh b/ci/image/push.sh new file mode 100755 index 00000000..fdc321d4 --- /dev/null +++ b/ci/image/push.sh @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +set -euo pipefail +cd "$(dirname "${0}")" + +docker build -t nhooyr/websocket-ci . +docker push nhooyr/websocket-ci diff --git a/ci/lib.sh b/ci/lib.sh deleted file mode 100644 index 4cfe1ff9..00000000 --- a/ci/lib.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -cd "$(git rev-parse --show-toplevel)" diff --git a/ci/lint.sh b/ci/lint.sh index 00dcbd38..90772869 100755 --- a/ci/lint.sh +++ b/ci/lint.sh @@ -2,7 +2,7 @@ set -euo pipefail cd "$(dirname "${0}")" -source ./lib.sh +cd "$(git rev-parse --show-toplevel)" # shellcheck disable=SC2046 shellcheck -e SC1091 -x $(git ls-files "*.sh") diff --git a/ci/run.sh b/ci/run.sh index 8bf19058..56da2d93 100755 --- a/ci/run.sh +++ b/ci/run.sh @@ -4,7 +4,7 @@ set -euo pipefail cd "$(dirname "${0}")" -source ./lib.sh +cd "$(git rev-parse --show-toplevel)" ./ci/fmt.sh ./ci/lint.sh diff --git a/ci/test.sh b/ci/test.sh index 1079d543..57089b69 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -2,8 +2,9 @@ set -euo pipefail cd "$(dirname "${0}")" -source ./lib.sh +cd "$(git rev-parse --show-toplevel)" +mkdir -p ci/out # If you'd like to modify the args to go test, just run go test directly, this script is meant # for running tests at the end to get coverage and test under the race detector. go test -race -vet=off -coverprofile=ci/out/coverage.prof -coverpkg=./... ./... diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index adc1bd0d..6a77d1b0 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -40,4 +40,4 @@ installation of shellcheck, node and the Autobahn test suite on Debian or Ubuntu For Go, please refer to the [offical docs](https://golang.org/doc/install). You can benchmark the library with `./ci/benchmark.sh`. You only need Go to run that script. Benchmark -profiles generated by that script are also available as artifacts on the bench step. \ No newline at end of file +profiles generated by that script are also available as artifacts on the bench step. From a2abcd49025d9117f67c5702e2f8873dbad4efea Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 20 Aug 2019 20:26:14 -0400 Subject: [PATCH 076/519] Switch to arch linux for CI Race detector doesn't work on alpine. See https://github.com/golang/go/issues/14481 --- .circleci/config.yml | 38 +++++++++++++++++++++----------------- ci/bench.sh | 23 ++++++++++++++++++----- ci/image/Dockerfile | 21 +++++++++------------ ci/lint.sh | 2 +- ci/test.sh | 14 +++++++++----- docs/CONTRIBUTING.md | 10 ++++------ go.mod | 1 + go.sum | 2 ++ tools.go | 1 + 9 files changed, 66 insertions(+), 46 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 59350e09..026ba472 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,15 +7,15 @@ jobs: - checkout - restore_cache: keys: - - go-{{ checksum "go.sum" }} + - go-v1-{{ checksum "go.sum" }} # Fallback to using the latest cache if no exact match is found. - - go- + - go-v1- - run: ./ci/fmt.sh - save_cache: paths: - - /go + - /root/go - /root/.cache/go-build - key: go-{{ checksum "go.sum" }} + key: go-v1-{{ checksum "go.sum" }} lint: docker: @@ -24,15 +24,15 @@ jobs: - checkout - restore_cache: keys: - - go-{{ checksum "go.sum" }} + - go-v1-{{ checksum "go.sum" }} # Fallback to using the latest cache if no exact match is found. - - go- + - go-v1- - run: ./ci/lint.sh - save_cache: paths: - - /go + - /root/go - /root/.cache/go-build - key: go-{{ checksum "go.sum" }} + key: go-v1-{{ checksum "go.sum" }} test: docker: @@ -41,17 +41,20 @@ jobs: - checkout - restore_cache: keys: - - go-{{ checksum "go.sum" }} + - go-v1-{{ checksum "go.sum" }} # Fallback to using the latest cache if no exact match is found. - - go- + - go-v1- - run: ./ci/test.sh - store_artifacts: path: ci/out + destination: out - save_cache: paths: - - /go + - /root/go - /root/.cache/go-build - key: go-{{ checksum "go.sum" }} + key: go-v1-{{ checksum "go.sum" }} + - store_test_results: + path: ci/out bench: docker: @@ -60,17 +63,18 @@ jobs: - checkout - restore_cache: keys: - - go-{{ checksum "go.sum" }} + - go-v1-{{ checksum "go.sum" }} # Fallback to using the latest cache if no exact match is found. - - go- + - go-v1- - run: ./ci/bench.sh - store_artifacts: path: ci/out + destination: out - save_cache: paths: - - /go + - /root/go - /root/.cache/go-build - key: go-{{ checksum "go.sum" }} + key: go-v1-{{ checksum "go.sum" }} workflows: version: 2 @@ -85,4 +89,4 @@ workflows: - test bench: jobs: - - bench \ No newline at end of file + - bench diff --git a/ci/bench.sh b/ci/bench.sh index 98a74af7..b4a0d06e 100755 --- a/ci/bench.sh +++ b/ci/bench.sh @@ -5,12 +5,25 @@ cd "$(dirname "${0}")" cd "$(git rev-parse --show-toplevel)" mkdir -p ci/out -go test -vet=off -run=^$ -bench=. -o=ci/out/websocket.test \ - -cpuprofile=ci/out/cpu.prof \ - -memprofile=ci/out/mem.prof \ - -blockprofile=ci/out/block.prof \ - -mutexprofile=ci/out/mutex.prof \ +benchArgs=( + "-vet=off" + "-run=^$" + "-bench=." + "-o=ci/out/websocket.test" + "-cpuprofile=ci/out/cpu.prof" + "-memprofile=ci/out/mem.prof" + "-blockprofile=ci/out/block.prof" + "-mutexprofile=ci/out/mutex.prof" . +) + +if [[ ${CI-} ]]; then + # https://circleci.com/docs/2.0/collect-test-data/ + go test "${benchArgs[@]}" | tee /dev/stderr | + go run github.com/jstemmer/go-junit-report > ci/out/testReport.xml +else + go test "${benchArgs[@]}" +fi echo echo "Profiles are in ./ci/out/*.prof diff --git a/ci/image/Dockerfile b/ci/image/Dockerfile index f8064198..bedc571f 100644 --- a/ci/image/Dockerfile +++ b/ci/image/Dockerfile @@ -1,16 +1,13 @@ -FROM golang:1-alpine +FROM archlinux/base -RUN apk add --update bash git +RUN pacman -Syu --noconfirm -# Autobahn Test suite. -RUN apk add py2-pip python2-dev gcc libffi-dev libc-dev openssl-dev && \ - pip install autobahntestsuite +RUN pacman -S --noconfirm go git shellcheck tar openssh gzip ca-certificates -# prettier. -RUN apk add npm && npm install -g prettier +RUN pacman -S --noconfirm gcc python2-pip && \ + pip2 install autobahntestsuite -# shellcheck -RUN apk add xz curl && \ - curl -o /tmp/shellcheck.tar.xz -L https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.x86_64.tar.xz && \ - tar -xf /tmp/shellcheck.tar.xz && \ - ln -s "${PWD}/shellcheck-stable/shellcheck" /usr/local/bin/shellcheck \ No newline at end of file +RUN pacman -S --noconfirm npm && \ + npm install -g prettier + +ENV GOFLAGS -mod=readonly \ No newline at end of file diff --git a/ci/lint.sh b/ci/lint.sh index 90772869..b7268c55 100755 --- a/ci/lint.sh +++ b/ci/lint.sh @@ -5,6 +5,6 @@ cd "$(dirname "${0}")" cd "$(git rev-parse --show-toplevel)" # shellcheck disable=SC2046 -shellcheck -e SC1091 -x $(git ls-files "*.sh") +shellcheck -x $(git ls-files "*.sh") go vet ./... go run golang.org/x/lint/golint -set_exit_status ./... diff --git a/ci/test.sh b/ci/test.sh index 57089b69..93b9a4f3 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -5,12 +5,16 @@ cd "$(dirname "${0}")" cd "$(git rev-parse --show-toplevel)" mkdir -p ci/out -# If you'd like to modify the args to go test, just run go test directly, this script is meant -# for running tests at the end to get coverage and test under the race detector. -go test -race -vet=off -coverprofile=ci/out/coverage.prof -coverpkg=./... ./... +testFlags=(-race "-vet=off" "-coverprofile=ci/out/coverage.prof" "-coverpkg=./...") +if [[ ${CI-} ]]; then + # https://circleci.com/docs/2.0/collect-test-data/ + go test "${testFlags[@]}" -v ./... 2>&1 | tee /dev/stderr | + go run github.com/jstemmer/go-junit-report > ci/out/testReport.xml +else + go test "${testFlags[@]}" ./... +fi +go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html if [[ ${CI:-} ]]; then bash <(curl -s https://codecov.io/bash) -f ci/out/coverage.prof -else - go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html fi diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 6a77d1b0..e5541efb 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -34,10 +34,8 @@ For remote coverage, you can use either [codecov](https://codecov.io/gh/nhooyr/w You can also run any of the CI steps individually. All of them are scripts in the `ci` directory. -See [../ci/lint.sh](../ci/lint.sh) and [../ci/test.sh](../ci/test.sh) for the -installation of shellcheck, node and the Autobahn test suite on Debian or Ubuntu. +See [../ci/image/Dockerfile](../ci/image/Dockerfile) for the +installation of the CI dependencies on arch linux. -For Go, please refer to the [offical docs](https://golang.org/doc/install). - -You can benchmark the library with `./ci/benchmark.sh`. You only need Go to run that script. Benchmark -profiles generated by that script are also available as artifacts on the bench step. +You can benchmark the library with `./ci/benchmark.sh`. You only need Go to run that script. +Benchmark profiles generated by that script are also available as artifacts on the bench step. diff --git a/go.mod b/go.mod index cc9a865d..0d558c58 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.12 require ( github.com/golang/protobuf v1.3.1 github.com/google/go-cmp v0.2.0 + github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024 github.com/kr/pretty v0.1.0 // indirect go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16 golang.org/x/lint v0.0.0-20190409202823-959b441ac422 diff --git a/go.sum b/go.sum index 7965958d..5684b205 100644 --- a/go.sum +++ b/go.sum @@ -2,6 +2,8 @@ github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024 h1:rBMNdlhTLzJjJSDIjNEXX1Pz3Hmwmz91v+zycvx9PJc= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= diff --git a/tools.go b/tools.go index c78042b7..e3501492 100644 --- a/tools.go +++ b/tools.go @@ -4,6 +4,7 @@ package tools // See https://github.com/go-modules-by-example/index/blob/master/010_tools/README.md import ( + _ "github.com/jstemmer/go-junit-report" _ "go.coder.com/go-tools/cmd/goimports" _ "golang.org/x/lint/golint" _ "golang.org/x/tools/cmd/stringer" From d8abc3a8927ad73f6d3f07d94b71b4348bfe7871 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 20 Aug 2019 22:21:35 -0400 Subject: [PATCH 077/519] Switch CI to an optimized ubuntu docker image --- .circleci/config.yml | 34 ++++++++++++++++++---------------- ci/bench.sh | 4 ++-- ci/image/Dockerfile | 16 +++++++--------- ci/image/push.sh | 3 ++- ci/test.sh | 6 +++--- 5 files changed, 32 insertions(+), 31 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 026ba472..b4187ba4 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,15 +7,15 @@ jobs: - checkout - restore_cache: keys: - - go-v1-{{ checksum "go.sum" }} + - go-v3-{{ checksum "go.sum" }} # Fallback to using the latest cache if no exact match is found. - - go-v1- + - go-v3- - run: ./ci/fmt.sh - save_cache: paths: - - /root/go + - /root/gopath - /root/.cache/go-build - key: go-v1-{{ checksum "go.sum" }} + key: go-v3-{{ checksum "go.sum" }} lint: docker: @@ -24,15 +24,15 @@ jobs: - checkout - restore_cache: keys: - - go-v1-{{ checksum "go.sum" }} + - go-v3-{{ checksum "go.sum" }} # Fallback to using the latest cache if no exact match is found. - - go-v1- + - go-v3- - run: ./ci/lint.sh - save_cache: paths: - - /root/go + - /root/gopath - /root/.cache/go-build - key: go-v1-{{ checksum "go.sum" }} + key: go-v3-{{ checksum "go.sum" }} test: docker: @@ -41,18 +41,18 @@ jobs: - checkout - restore_cache: keys: - - go-v1-{{ checksum "go.sum" }} + - go-v3-{{ checksum "go.sum" }} # Fallback to using the latest cache if no exact match is found. - - go-v1- + - go-v3- - run: ./ci/test.sh - store_artifacts: path: ci/out destination: out - save_cache: paths: - - /root/go + - /root/gopath - /root/.cache/go-build - key: go-v1-{{ checksum "go.sum" }} + key: go-v3-{{ checksum "go.sum" }} - store_test_results: path: ci/out @@ -63,18 +63,20 @@ jobs: - checkout - restore_cache: keys: - - go-v1-{{ checksum "go.sum" }} + - go-v3-{{ checksum "go.sum" }} # Fallback to using the latest cache if no exact match is found. - - go-v1- + - go-v3- - run: ./ci/bench.sh - store_artifacts: path: ci/out destination: out - save_cache: paths: - - /root/go + - /root/gopath - /root/.cache/go-build - key: go-v1-{{ checksum "go.sum" }} + key: go-v3-{{ checksum "go.sum" }} + - store_test_results: + path: ci/out workflows: version: 2 diff --git a/ci/bench.sh b/ci/bench.sh index b4a0d06e..fa11e325 100755 --- a/ci/bench.sh +++ b/ci/bench.sh @@ -4,7 +4,7 @@ set -euo pipefail cd "$(dirname "${0}")" cd "$(git rev-parse --show-toplevel)" -mkdir -p ci/out +mkdir -p ci/out/gobench benchArgs=( "-vet=off" "-run=^$" @@ -20,7 +20,7 @@ benchArgs=( if [[ ${CI-} ]]; then # https://circleci.com/docs/2.0/collect-test-data/ go test "${benchArgs[@]}" | tee /dev/stderr | - go run github.com/jstemmer/go-junit-report > ci/out/testReport.xml + go run github.com/jstemmer/go-junit-report > ci/out/gobench/report.xml else go test "${benchArgs[@]}" fi diff --git a/ci/image/Dockerfile b/ci/image/Dockerfile index bedc571f..e87f0e2b 100644 --- a/ci/image/Dockerfile +++ b/ci/image/Dockerfile @@ -1,13 +1,11 @@ -FROM archlinux/base +FROM golang:1 -RUN pacman -Syu --noconfirm +ENV DEBIAN_FRONTEND=noninteractive +ENV GOPATH /root/gopath +ENV GOFLAGS -mod=readonly -RUN pacman -S --noconfirm go git shellcheck tar openssh gzip ca-certificates - -RUN pacman -S --noconfirm gcc python2-pip && \ - pip2 install autobahntestsuite - -RUN pacman -S --noconfirm npm && \ +RUN apt-get update && \ + apt-get install -y shellcheck python-pip npm && \ + pip2 install autobahntestsuite && \ npm install -g prettier -ENV GOFLAGS -mod=readonly \ No newline at end of file diff --git a/ci/image/push.sh b/ci/image/push.sh index fdc321d4..1cbae979 100755 --- a/ci/image/push.sh +++ b/ci/image/push.sh @@ -2,6 +2,7 @@ set -euo pipefail cd "$(dirname "${0}")" +cd "$(git rev-parse --show-toplevel)" -docker build -t nhooyr/websocket-ci . +docker build -f ./ci/image/Dockerfile -t nhooyr/websocket-ci . docker push nhooyr/websocket-ci diff --git a/ci/test.sh b/ci/test.sh index 93b9a4f3..988add51 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -4,17 +4,17 @@ set -euo pipefail cd "$(dirname "${0}")" cd "$(git rev-parse --show-toplevel)" -mkdir -p ci/out +mkdir -p ci/out/gotest testFlags=(-race "-vet=off" "-coverprofile=ci/out/coverage.prof" "-coverpkg=./...") if [[ ${CI-} ]]; then # https://circleci.com/docs/2.0/collect-test-data/ go test "${testFlags[@]}" -v ./... 2>&1 | tee /dev/stderr | - go run github.com/jstemmer/go-junit-report > ci/out/testReport.xml + go run github.com/jstemmer/go-junit-report > ci/out/gotest/report.xml else go test "${testFlags[@]}" ./... fi -go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html +go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html if [[ ${CI:-} ]]; then bash <(curl -s https://codecov.io/bash) -f ci/out/coverage.prof fi From 20ccbc4c6814c86aecde3f1b43bdcc2981f0f50e Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 20 Aug 2019 22:49:56 -0400 Subject: [PATCH 078/519] Improve CONTRIBUTING.md --- .circleci/config.yml | 25 ------------------------- ci/bench.sh | 30 ------------------------------ ci/run.sh | 2 +- ci/test.sh | 6 +++--- docs/CONTRIBUTING.md | 26 +++++++++++++------------- websocket_test.go | 2 +- 6 files changed, 18 insertions(+), 73 deletions(-) delete mode 100755 ci/bench.sh diff --git a/.circleci/config.yml b/.circleci/config.yml index b4187ba4..65b17aa0 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -56,28 +56,6 @@ jobs: - store_test_results: path: ci/out - bench: - docker: - - image: nhooyr/websocket-ci - steps: - - checkout - - restore_cache: - keys: - - go-v3-{{ checksum "go.sum" }} - # Fallback to using the latest cache if no exact match is found. - - go-v3- - - run: ./ci/bench.sh - - store_artifacts: - path: ci/out - destination: out - - save_cache: - paths: - - /root/gopath - - /root/.cache/go-build - key: go-v3-{{ checksum "go.sum" }} - - store_test_results: - path: ci/out - workflows: version: 2 fmt: @@ -89,6 +67,3 @@ workflows: test: jobs: - test - bench: - jobs: - - bench diff --git a/ci/bench.sh b/ci/bench.sh deleted file mode 100755 index fa11e325..00000000 --- a/ci/bench.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail -cd "$(dirname "${0}")" -cd "$(git rev-parse --show-toplevel)" - -mkdir -p ci/out/gobench -benchArgs=( - "-vet=off" - "-run=^$" - "-bench=." - "-o=ci/out/websocket.test" - "-cpuprofile=ci/out/cpu.prof" - "-memprofile=ci/out/mem.prof" - "-blockprofile=ci/out/block.prof" - "-mutexprofile=ci/out/mutex.prof" - . -) - -if [[ ${CI-} ]]; then - # https://circleci.com/docs/2.0/collect-test-data/ - go test "${benchArgs[@]}" | tee /dev/stderr | - go run github.com/jstemmer/go-junit-report > ci/out/gobench/report.xml -else - go test "${benchArgs[@]}" -fi - -echo -echo "Profiles are in ./ci/out/*.prof -Keep in mind that every profiler Go provides is enabled so that may skew the benchmarks." diff --git a/ci/run.sh b/ci/run.sh index 56da2d93..904a7598 100755 --- a/ci/run.sh +++ b/ci/run.sh @@ -8,4 +8,4 @@ cd "$(git rev-parse --show-toplevel)" ./ci/fmt.sh ./ci/lint.sh -./ci/test.sh +./ci/test.sh \ No newline at end of file diff --git a/ci/test.sh b/ci/test.sh index 988add51..a746ab34 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -4,12 +4,12 @@ set -euo pipefail cd "$(dirname "${0}")" cd "$(git rev-parse --show-toplevel)" -mkdir -p ci/out/gotest -testFlags=(-race "-vet=off" "-coverprofile=ci/out/coverage.prof" "-coverpkg=./...") +mkdir -p ci/out/websocket +testFlags=(-race "-vet=off" "-bench=." "-coverprofile=ci/out/coverage.prof" "-coverpkg=./...") if [[ ${CI-} ]]; then # https://circleci.com/docs/2.0/collect-test-data/ go test "${testFlags[@]}" -v ./... 2>&1 | tee /dev/stderr | - go run github.com/jstemmer/go-junit-report > ci/out/gotest/report.xml + go run github.com/jstemmer/go-junit-report > ci/out/websocket/testReport.xml else go test "${testFlags[@]}" ./... fi diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index e5541efb..a1838934 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -22,20 +22,20 @@ You can run tests normally with `go test`. You'll need the [Autobahn Test suite pip package](https://github.com/crossbario/autobahn-testsuite). In the future this dependency will be removed. See [#117](https://github.com/nhooyr/websocket/issues/117). -On submission please check if CI has passed and if not, please correct your code such that it does. -If necessary, you may run CI locally with the `ci/run.sh` script which will fmt, lint and test your code -with coverage. -You'll need [shellcheck](https://github.com/koalaman/shellcheck#installing), node and the -Autobahn Test suite linked above. +Please ensure CI passes for your changes. If necessary, you may run CI locally. The various steps are located +in `ci/*.sh`. -For coverage details locally, please see `ci/out/coverage.html` after running `ci/run.sh` or `ci/test.sh`. -For remote coverage, you can use either [codecov](https://codecov.io/gh/nhooyr/websocket) or download the -`coverage.html` artifact on the test step in CI. +`ci/fmt.sh` requires node (specifically prettier). +`ci/lint.sh` requires [shellcheck](https://github.com/koalaman/shellcheck#installing). +`ci/test.sh` requires the [Autobahn Test suite pip package](https://github.com/crossbario/autobahn-testsuite). +`ci/bench.sh` requires only Go. +`ci/run.sh` runs everything in the above order and requires all of their dependencies. -You can also run any of the CI steps individually. All of them are scripts in the `ci` directory. +See [../ci/image/Dockerfile](../ci/image/Dockerfile) for the installation of the CI dependencies on ubuntu. -See [../ci/image/Dockerfile](../ci/image/Dockerfile) for the -installation of the CI dependencies on arch linux. +For CI coverage, you can use either [codecov](https://codecov.io/gh/nhooyr/websocket) or click the +`coverage.html` artifact on the test step in CI. +For coverage details locally, please see `ci/out/coverage.html` after running `ci/run.sh` or `ci/test.sh`. -You can benchmark the library with `./ci/benchmark.sh`. You only need Go to run that script. -Benchmark profiles generated by that script are also available as artifacts on the bench step. +Benchmark profiles generated by `bench.sh` are available as artifacts on the bench step so that they +may be analyzed. diff --git a/websocket_test.go b/websocket_test.go index 23977095..cd6bdaf5 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -1035,7 +1035,7 @@ func BenchmarkConn(b *testing.B) { b.Run("echo", func(b *testing.B) { for _, size := range sizes { b.Run(strconv.Itoa(size), func(b *testing.B) { - benchConn(b, false, false, size) + benchConn(b, true, true, size) }) } }) From 26dd4ac8932572db3f865783d74dd7cecc1a454e Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 21 Aug 2019 00:26:29 -0400 Subject: [PATCH 079/519] Consider StatusGoingAway a io.EOF in NetConn --- netconn.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/netconn.go b/netconn.go index 25787703..d28eeb84 100644 --- a/netconn.go +++ b/netconn.go @@ -33,7 +33,8 @@ import ( // The Addr methods will return a mock net.Addr that returns "websocket" for Network // and "websocket/unknown-addr" for String. // -// A received StatusNormalClosure close frame will be translated to EOF when reading. +// A received StatusNormalClosure or StatusGoingAway close frame will be translated to +// io.EOF when reading. func NetConn(c *Conn, msgType MessageType) net.Conn { nc := &netConn{ c: c, @@ -93,7 +94,7 @@ func (c *netConn) Read(p []byte) (int, error) { typ, r, err := c.c.Reader(c.readContext) if err != nil { var ce CloseError - if xerrors.As(err, &ce) && (ce.Code == StatusNormalClosure) { + if xerrors.As(err, &ce) && (ce.Code == StatusNormalClosure) || (ce.Code == StatusGoingAway) { c.eofed = true return 0, io.EOF } From cb7c7888466ffe390b30e1c0947802e0378c5b5a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 21 Aug 2019 00:26:49 -0400 Subject: [PATCH 080/519] Improve field names in conn --- websocket.go | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/websocket.go b/websocket.go index 5942b68c..962fb924 100644 --- a/websocket.go +++ b/websocket.go @@ -46,6 +46,7 @@ type Conn struct { closeErr error closed chan struct{} + // messageWriter state. // writeMsgLock is acquired to write a data message. writeMsgLock chan struct{} // writeFrameLock is acquired to write a single frame. @@ -72,11 +73,11 @@ type Conn struct { readHeaderBuf []byte controlPayloadBuf []byte - // messageReader state - readMsgCtx context.Context - readMsgHeader header - readFrameEOF bool - readMaskPos int + // messageReader state. + readerMsgCtx context.Context + readerMsgHeader header + readerFrameEOF bool + readerMaskPos int setReadTimeout chan context.Context setWriteTimeout chan context.Context @@ -360,7 +361,7 @@ func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) { } func (c *Conn) reader(ctx context.Context) (MessageType, io.Reader, error) { - if c.activeReader != nil && !c.readFrameEOF { + if c.activeReader != nil && !c.readerFrameEOF { // The only way we know for sure the previous reader is not yet complete is // if there is an active frame not yet fully read. // Otherwise, a user may have read the last byte but not the EOF if the EOF @@ -396,10 +397,10 @@ func (c *Conn) reader(ctx context.Context) (MessageType, io.Reader, error) { return 0, nil, err } - c.readMsgCtx = ctx - c.readMsgHeader = h - c.readFrameEOF = false - c.readMaskPos = 0 + c.readerMsgCtx = ctx + c.readerMsgHeader = h + c.readerFrameEOF = false + c.readerMaskPos = 0 c.readMsgLeft = c.msgReadLimit r := &messageReader{ @@ -468,8 +469,8 @@ func (r *messageReader) read(p []byte) (int, error) { p = p[:r.c.readMsgLeft] } - if r.c.readFrameEOF { - h, err := r.c.readTillMsg(r.c.readMsgCtx) + if r.c.readerFrameEOF { + h, err := r.c.readTillMsg(r.c.readerMsgCtx) if err != nil { return 0, err } @@ -480,31 +481,31 @@ func (r *messageReader) read(p []byte) (int, error) { return 0, err } - r.c.readMsgHeader = h - r.c.readFrameEOF = false - r.c.readMaskPos = 0 + r.c.readerMsgHeader = h + r.c.readerFrameEOF = false + r.c.readerMaskPos = 0 } - h := r.c.readMsgHeader + h := r.c.readerMsgHeader if int64(len(p)) > h.payloadLength { p = p[:h.payloadLength] } - n, err := r.c.readFramePayload(r.c.readMsgCtx, p) + n, err := r.c.readFramePayload(r.c.readerMsgCtx, p) h.payloadLength -= int64(n) r.c.readMsgLeft -= int64(n) if h.masked { - r.c.readMaskPos = fastXOR(h.maskKey, r.c.readMaskPos, p) + r.c.readerMaskPos = fastXOR(h.maskKey, r.c.readerMaskPos, p) } - r.c.readMsgHeader = h + r.c.readerMsgHeader = h if err != nil { return n, err } if h.payloadLength == 0 { - r.c.readFrameEOF = true + r.c.readerFrameEOF = true if h.fin { r.c.activeReader = nil From 451ecab365cb37717d8cf995f95a33102e0aa3da Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 21 Aug 2019 12:24:44 -0400 Subject: [PATCH 081/519] Improve ci/fmt.sh and ci/test.sh --- ci/fmt.sh | 3 ++- ci/image/Dockerfile | 6 +++-- ci/run.sh | 2 +- ci/test.sh | 20 +++++++++------- docs/CONTRIBUTING.md | 10 +++----- go.mod | 4 +++- go.sum | 57 ++++++++++++++++++++++++++++++++++++++++++-- tools.go | 2 +- websocket.go | 3 +-- 9 files changed, 82 insertions(+), 25 deletions(-) diff --git a/ci/fmt.sh b/ci/fmt.sh index cd1e7b15..a4e7ff02 100755 --- a/ci/fmt.sh +++ b/ci/fmt.sh @@ -34,11 +34,12 @@ unstaged_files() { check() { if [[ ${CI:-} && $(unstaged_files) != "" ]]; then echo - echo "Files either need generation or are formatted incorrectly." + echo "Files need generation or are formatted incorrectly." echo "Please run:" echo "./ci/fmt.sh" echo git status + git diff exit 1 fi } diff --git a/ci/image/Dockerfile b/ci/image/Dockerfile index e87f0e2b..d435e949 100644 --- a/ci/image/Dockerfile +++ b/ci/image/Dockerfile @@ -1,11 +1,13 @@ FROM golang:1 ENV DEBIAN_FRONTEND=noninteractive -ENV GOPATH /root/gopath -ENV GOFLAGS -mod=readonly +ENV GOPATH=/root/gopath +ENV GOFLAGS="-mod=readonly" +ENV PAGER=cat RUN apt-get update && \ apt-get install -y shellcheck python-pip npm && \ pip2 install autobahntestsuite && \ npm install -g prettier +RUN git config --global color.ui always \ No newline at end of file diff --git a/ci/run.sh b/ci/run.sh index 904a7598..56da2d93 100755 --- a/ci/run.sh +++ b/ci/run.sh @@ -8,4 +8,4 @@ cd "$(git rev-parse --show-toplevel)" ./ci/fmt.sh ./ci/lint.sh -./ci/test.sh \ No newline at end of file +./ci/test.sh diff --git a/ci/test.sh b/ci/test.sh index a746ab34..ab101e91 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -5,14 +5,18 @@ cd "$(dirname "${0}")" cd "$(git rev-parse --show-toplevel)" mkdir -p ci/out/websocket -testFlags=(-race "-vet=off" "-bench=." "-coverprofile=ci/out/coverage.prof" "-coverpkg=./...") -if [[ ${CI-} ]]; then - # https://circleci.com/docs/2.0/collect-test-data/ - go test "${testFlags[@]}" -v ./... 2>&1 | tee /dev/stderr | - go run github.com/jstemmer/go-junit-report > ci/out/websocket/testReport.xml -else - go test "${testFlags[@]}" ./... -fi +testFlags=( + -race + "-vet=off" + "-bench=." + "-coverprofile=ci/out/coverage.prof" + "-coverpkg=./..." +) +# https://circleci.com/docs/2.0/collect-test-data/ +go run gotest.tools/gotestsum \ + --junitfile ci/out/websocket/testReport.xml \ + --format=short-verbose \ + -- "${testFlags[@]}" go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html if [[ ${CI:-} ]]; then diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index a1838934..2d9104d1 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -22,20 +22,16 @@ You can run tests normally with `go test`. You'll need the [Autobahn Test suite pip package](https://github.com/crossbario/autobahn-testsuite). In the future this dependency will be removed. See [#117](https://github.com/nhooyr/websocket/issues/117). -Please ensure CI passes for your changes. If necessary, you may run CI locally. The various steps are located -in `ci/*.sh`. +Please ensure CI passes for your changes. If necessary, you may run CI locally. +The various steps are located in `ci/*.sh`. `ci/fmt.sh` requires node (specifically prettier). `ci/lint.sh` requires [shellcheck](https://github.com/koalaman/shellcheck#installing). `ci/test.sh` requires the [Autobahn Test suite pip package](https://github.com/crossbario/autobahn-testsuite). -`ci/bench.sh` requires only Go. `ci/run.sh` runs everything in the above order and requires all of their dependencies. -See [../ci/image/Dockerfile](../ci/image/Dockerfile) for the installation of the CI dependencies on ubuntu. +See [../ci/image/Dockerfile](../ci/image/Dockerfile) for the installation of the CI dependencies on Ubuntu. For CI coverage, you can use either [codecov](https://codecov.io/gh/nhooyr/websocket) or click the `coverage.html` artifact on the test step in CI. For coverage details locally, please see `ci/out/coverage.html` after running `ci/run.sh` or `ci/test.sh`. - -Benchmark profiles generated by `bench.sh` are available as artifacts on the bench step so that they -may be analyzed. diff --git a/go.mod b/go.mod index 0d558c58..58d79bf1 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.12 require ( github.com/golang/protobuf v1.3.1 github.com/google/go-cmp v0.2.0 - github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024 github.com/kr/pretty v0.1.0 // indirect go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16 golang.org/x/lint v0.0.0-20190409202823-959b441ac422 @@ -14,5 +13,8 @@ require ( golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 golang.org/x/tools v0.0.0-20190429184909-35c670923e21 golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 + gotest.tools/gotestsum v0.3.5 mvdan.cc/sh v2.6.4+incompatible ) + +replace gotest.tools/gotestsum => github.com/nhooyr/gotestsum v0.3.6-0.20190821172136-aaabbb33254b diff --git a/go.sum b/go.sum index 5684b205..98b766bf 100644 --- a/go.sum +++ b/go.sum @@ -1,24 +1,63 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fatih/color v1.6.0 h1:66qjqZk8kalYAvDRtM1AdAJQI0tj4Wrue3Eq3B3pmFU= +github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024 h1:rBMNdlhTLzJjJSDIjNEXX1Pz3Hmwmz91v+zycvx9PJc= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= +github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/nhooyr/gotestsum v0.3.6-0.20190821172136-aaabbb33254b h1:t6DbmxEtGMM72Uhs638nBOyK7tjsrDwoMfYO1EfQdFE= +github.com/nhooyr/gotestsum v0.3.6-0.20190821172136-aaabbb33254b/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sirupsen/logrus v1.0.5 h1:8c8b5uO0zS4X6RPl/sd1ENwSkIc0/H2PaHxE3udaE8I= +github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16 h1:3gGa1bM0nG7Ruhu5b7wKnoOOwAD/fJ8iyyAcpOzDG3A= go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw= +golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/lint v0.0.0-20190409202823-959b441ac422 h1:QzoH/1pFpZguR8NrRHLcO6jKqfv2zpuSqZLgdm7ZmjI= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6 h1:FP8hkuE6yUEaJnK7O2eTuejKWwW+Rhfj80dQ2JcKxCU= golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -32,5 +71,19 @@ golang.org/x/tools v0.0.0-20190429184909-35c670923e21 h1:Kjcw+D2LTzLmxOHrMK9uvYP golang.org/x/tools v0.0.0-20190429184909-35c670923e21/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4= golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= +gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0= +gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gotest.tools v2.1.0+incompatible h1:5USw7CrJBYKqjg9R7QlA6jzqZKEAtvW82aNmsxxGPxw= +gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= mvdan.cc/sh v2.6.4+incompatible h1:eD6tDeh0pw+/TOTI1BBEryZ02rD2nMcFsgcvde7jffM= mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8= diff --git a/tools.go b/tools.go index e3501492..a6f0268e 100644 --- a/tools.go +++ b/tools.go @@ -4,9 +4,9 @@ package tools // See https://github.com/go-modules-by-example/index/blob/master/010_tools/README.md import ( - _ "github.com/jstemmer/go-junit-report" _ "go.coder.com/go-tools/cmd/goimports" _ "golang.org/x/lint/golint" _ "golang.org/x/tools/cmd/stringer" + _ "gotest.tools/gotestsum" _ "mvdan.cc/sh/cmd/shfmt" ) diff --git a/websocket.go b/websocket.go index 962fb924..393ea547 100644 --- a/websocket.go +++ b/websocket.go @@ -672,14 +672,13 @@ func (w *messageWriter) close() error { if w.closed() { return xerrors.Errorf("cannot use closed writer") } - w.closed() + w.c.activeWriter = nil _, err := w.c.writeFrame(w.c.writeMsgCtx, true, w.c.writeMsgOpcode, nil) if err != nil { return xerrors.Errorf("failed to write fin frame: %w", err) } - w.c.activeWriter = nil w.c.releaseLock(w.c.writeMsgLock) return nil } From 853d3f01a54ef18e5d1d1240b7a956795d1f3d0b Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 22 Aug 2019 01:39:53 -0400 Subject: [PATCH 082/519] Make CONTRIBUTING.md more visible Closes #119 --- README.md | 4 ++++ docs/CONTRIBUTING.md | 21 +++++++++++++-------- docs/ISSUE_TEMPLATE.md | 2 ++ docs/PULL_REQUEST_TEMPLATE.md | 2 ++ 4 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 docs/ISSUE_TEMPLATE.md create mode 100644 docs/PULL_REQUEST_TEMPLATE.md diff --git a/README.md b/README.md index f3bd6a84..cf20b877 100644 --- a/README.md +++ b/README.md @@ -158,6 +158,10 @@ If you want a library that gives you absolute control over everything, this is t but for most users, the API provided by nhooyr/websocket will fit better as it is nearly just as performant but much easier to use correctly and idiomatic. +## Contributing + +Please see [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md). + ## Users This is a list of companies or projects that use this library. diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 2d9104d1..57197ba1 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -4,18 +4,23 @@ Please be as descriptive as possible with your description. +Reproducible examples are key to fixing bugs and strongly encouraged. + ## Pull requests -Please split up changes into several small descriptive commits. +Good issues for first time contributors are marked as such. Please feel free to +reach out for clarification on what needs to be done. + +Split up large changes into several small descriptive commits. -Please capitalize the first word in the commit message title. +Capitalize the first word in the commit message title. The commit message title should use the verb tense + phrase that completes the blank in > This change modifies websocket to \_\_\_\_\_\_\_\_\_ -Be sure to link to an existing issue if one exists. In general, try creating an issue -before making a PR to get some discussion going and to make sure you do not spend time +Be sure to link to an existing issue if one exists. In general, create an issue +before a PR to get some discussion going and to make sure you do not spend time on a PR that may be rejected. You can run tests normally with `go test`. @@ -25,10 +30,10 @@ In the future this dependency will be removed. See [#117](https://github.com/nho Please ensure CI passes for your changes. If necessary, you may run CI locally. The various steps are located in `ci/*.sh`. -`ci/fmt.sh` requires node (specifically prettier). -`ci/lint.sh` requires [shellcheck](https://github.com/koalaman/shellcheck#installing). -`ci/test.sh` requires the [Autobahn Test suite pip package](https://github.com/crossbario/autobahn-testsuite). -`ci/run.sh` runs everything in the above order and requires all of their dependencies. +- `ci/fmt.sh` requires node (specifically prettier). +- `ci/lint.sh` requires [shellcheck](https://github.com/koalaman/shellcheck#installing). +- `ci/test.sh` requires the [Autobahn Test suite pip package](https://github.com/crossbario/autobahn-testsuite). +- `ci/run.sh` runs everything in the above order and requires all of their dependencies. See [../ci/image/Dockerfile](../ci/image/Dockerfile) for the installation of the CI dependencies on Ubuntu. diff --git a/docs/ISSUE_TEMPLATE.md b/docs/ISSUE_TEMPLATE.md new file mode 100644 index 00000000..5f828acb --- /dev/null +++ b/docs/ISSUE_TEMPLATE.md @@ -0,0 +1,2 @@ + + diff --git a/docs/PULL_REQUEST_TEMPLATE.md b/docs/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..c58db085 --- /dev/null +++ b/docs/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,2 @@ + + From fdba8eb4cd247eccd42487e4552f716891325338 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 22 Aug 2019 01:42:34 -0400 Subject: [PATCH 083/519] Fix unfortunate docs typo --- docs/ISSUE_TEMPLATE.md | 2 +- docs/PULL_REQUEST_TEMPLATE.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/ISSUE_TEMPLATE.md b/docs/ISSUE_TEMPLATE.md index 5f828acb..c58db085 100644 --- a/docs/ISSUE_TEMPLATE.md +++ b/docs/ISSUE_TEMPLATE.md @@ -1,2 +1,2 @@ - + diff --git a/docs/PULL_REQUEST_TEMPLATE.md b/docs/PULL_REQUEST_TEMPLATE.md index c58db085..5f828acb 100644 --- a/docs/PULL_REQUEST_TEMPLATE.md +++ b/docs/PULL_REQUEST_TEMPLATE.md @@ -1,2 +1,2 @@ - + From 695d679893438c6f4d36babd87fa80ded53cde05 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 22 Aug 2019 02:03:32 -0400 Subject: [PATCH 084/519] Improve contributing docs --- docs/CONTRIBUTING.md | 43 ++++++++++++++++++++++++------------------ docs/ISSUE_TEMPLATE.md | 4 ++-- 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 57197ba1..a0c97261 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -2,9 +2,9 @@ ## Issues -Please be as descriptive as possible with your description. +Please be as descriptive as possible. -Reproducible examples are key to fixing bugs and strongly encouraged. +Reproducible examples are key to finding and fixing bugs. ## Pull requests @@ -19,24 +19,31 @@ The commit message title should use the verb tense + phrase that completes the b > This change modifies websocket to \_\_\_\_\_\_\_\_\_ -Be sure to link to an existing issue if one exists. In general, create an issue -before a PR to get some discussion going and to make sure you do not spend time -on a PR that may be rejected. +Be sure to [correctly link](https://help.github.com/en/articles/closing-issues-using-keywords) +to an existing issue if one exists. In general, create an issue before a PR to get some +discussion going and to make sure you do not spend time on a PR that may be rejected. -You can run tests normally with `go test`. -You'll need the [Autobahn Test suite pip package](https://github.com/crossbario/autobahn-testsuite). -In the future this dependency will be removed. See [#117](https://github.com/nhooyr/websocket/issues/117). +CI must pass on your changes for them to be merged. -Please ensure CI passes for your changes. If necessary, you may run CI locally. -The various steps are located in `ci/*.sh`. +### CI -- `ci/fmt.sh` requires node (specifically prettier). -- `ci/lint.sh` requires [shellcheck](https://github.com/koalaman/shellcheck#installing). -- `ci/test.sh` requires the [Autobahn Test suite pip package](https://github.com/crossbario/autobahn-testsuite). -- `ci/run.sh` runs everything in the above order and requires all of their dependencies. +CI will ensure your code is formatted correctly, passes linting and tests. +It will collect coverage and report it to [codecov](https://codecov.io/gh/nhooyr/websocket) +and also upload a `out/coverage.html` artifact that you can click on to interactively +browse coverage. -See [../ci/image/Dockerfile](../ci/image/Dockerfile) for the installation of the CI dependencies on Ubuntu. +You can run CI locally. The various steps are located in `ci/*.sh`. -For CI coverage, you can use either [codecov](https://codecov.io/gh/nhooyr/websocket) or click the -`coverage.html` artifact on the test step in CI. -For coverage details locally, please see `ci/out/coverage.html` after running `ci/run.sh` or `ci/test.sh`. +1. `ci/fmt.sh` requires node (specifically prettier). +1. `ci/lint.sh` requires [shellcheck](https://github.com/koalaman/shellcheck#installing). +1. `ci/test.sh` requires the [Autobahn Test suite pip package](https://github.com/crossbario/autobahn-testsuite). +1. `ci/run.sh` runs the above scripts in order. + +For coverage details locally, please see `ci/out/coverage.html` after running `ci/test.sh`. + +See [ci/image/Dockerfile](ci/image/Dockerfile) for the installation of the CI dependencies on Ubuntu. + +You can also run tests normally with `go test` once you have the +[Autobahn Test suite pip package](https://github.com/crossbario/autobahn-testsuite) +installed. `ci/test.sh` just passes a default set of flags to `go test` to collect coverage, +enable the race detector, run benchmarks and also prettifies the output. diff --git a/docs/ISSUE_TEMPLATE.md b/docs/ISSUE_TEMPLATE.md index c58db085..939d3200 100644 --- a/docs/ISSUE_TEMPLATE.md +++ b/docs/ISSUE_TEMPLATE.md @@ -1,2 +1,2 @@ - - + + From a3a891bf62a60365ea2084d19c029fdf8802a55e Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 28 Aug 2019 13:07:01 -0400 Subject: [PATCH 085/519] Improve coverage in dial.go and header.go --- ci/test.sh | 2 +- dial_test.go | 42 ++++++++++++++++++++++++++++++++++++++++++ go.mod | 4 +--- go.sum | 4 ++-- header_test.go | 15 +++++++++++++++ websocket_test.go | 2 ++ 6 files changed, 63 insertions(+), 6 deletions(-) diff --git a/ci/test.sh b/ci/test.sh index ab101e91..875216f1 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -8,7 +8,7 @@ mkdir -p ci/out/websocket testFlags=( -race "-vet=off" - "-bench=." + # "-bench=." "-coverprofile=ci/out/coverage.prof" "-coverpkg=./..." ) diff --git a/dial_test.go b/dial_test.go index 6f0deef9..6400c223 100644 --- a/dial_test.go +++ b/dial_test.go @@ -1,11 +1,53 @@ package websocket import ( + "context" "net/http" "net/http/httptest" "testing" + "time" ) +func TestBadDials(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + url string + opts DialOptions + }{ + { + name: "badURL", + url: "://noscheme", + }, + { + name: "badURLScheme", + url: "ftp://nhooyr.io", + }, + { + name: "badHTTPClient", + url: "ws://nhooyr.io", + opts: DialOptions{ + HTTPClient: &http.Client{ + Timeout: time.Minute, + }, + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + _, _, err := Dial(context.Background(), tc.url, tc.opts) + if err == nil { + t.Fatalf("expected non nil error: %+v", err) + } + }) + } +} + func Test_verifyServerHandshake(t *testing.T) { t.Parallel() diff --git a/go.mod b/go.mod index 58d79bf1..35d500dd 100644 --- a/go.mod +++ b/go.mod @@ -13,8 +13,6 @@ require ( golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 golang.org/x/tools v0.0.0-20190429184909-35c670923e21 golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 - gotest.tools/gotestsum v0.3.5 + gotest.tools/gotestsum v0.3.6-0.20190825182939-fc6cb5870c52 mvdan.cc/sh v2.6.4+incompatible ) - -replace gotest.tools/gotestsum => github.com/nhooyr/gotestsum v0.3.6-0.20190821172136-aaabbb33254b diff --git a/go.sum b/go.sum index 98b766bf..b9e3737c 100644 --- a/go.sum +++ b/go.sum @@ -22,8 +22,6 @@ github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRU github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/nhooyr/gotestsum v0.3.6-0.20190821172136-aaabbb33254b h1:t6DbmxEtGMM72Uhs638nBOyK7tjsrDwoMfYO1EfQdFE= -github.com/nhooyr/gotestsum v0.3.6-0.20190821172136-aaabbb33254b/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -85,5 +83,7 @@ gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools v2.1.0+incompatible h1:5USw7CrJBYKqjg9R7QlA6jzqZKEAtvW82aNmsxxGPxw= gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools/gotestsum v0.3.6-0.20190825182939-fc6cb5870c52 h1:Qr31uPFyjpOhAgRfKV4ATUnknnLT2X7HFjqwkstdbbE= +gotest.tools/gotestsum v0.3.6-0.20190825182939-fc6cb5870c52/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY= mvdan.cc/sh v2.6.4+incompatible h1:eD6tDeh0pw+/TOTI1BBEryZ02rD2nMcFsgcvde7jffM= mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8= diff --git a/header_test.go b/header_test.go index b45854ea..4457c356 100644 --- a/header_test.go +++ b/header_test.go @@ -21,6 +21,21 @@ func randBool() bool { func TestHeader(t *testing.T) { t.Parallel() + t.Run("writeNegativeLength", func(t *testing.T) { + t.Parallel() + + defer func() { + r := recover() + if r == nil { + t.Fatal("failed to induce panic in writeHeader with negative payload length") + } + }() + + writeHeader(nil, header{ + payloadLength: -1, + }) + }) + t.Run("readNegativeLength", func(t *testing.T) { t.Parallel() diff --git a/websocket_test.go b/websocket_test.go index cd6bdaf5..2ef25cdd 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -654,6 +654,7 @@ func testServer(tb testing.TB, fn http.HandlerFunc) (s *httptest.Server, closeFn // https://github.com/crossbario/autobahn-python/tree/master/wstest func TestAutobahnServer(t *testing.T) { t.Parallel() + t.Skip() s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c, err := websocket.Accept(w, r, websocket.AcceptOptions{ @@ -794,6 +795,7 @@ func unusedListenAddr() (string, error) { // https://github.com/crossbario/autobahn-python/blob/master/wstest/testee_client_aio.py func TestAutobahnClient(t *testing.T) { t.Parallel() + t.Skip() serverAddr, err := unusedListenAddr() if err != nil { From 679ddb825d5cd5ce4cc7136734fff5effe3a2910 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 29 Aug 2019 15:37:26 -0500 Subject: [PATCH 086/519] Drastically improve non autobahn test coverage Also simplified and refactored the Conn tests. More changes soon. --- accept_test.go | 33 ++ ci/test.sh | 31 +- dial_test.go | 9 +- export_test.go | 12 +- header_test.go | 31 ++ netconn.go | 4 +- statuscode.go | 2 +- statuscode_test.go | 108 ++++++- websocket.go | 56 ++-- websocket_test.go | 781 ++++++++++++++++++++++++++++++--------------- 10 files changed, 761 insertions(+), 306 deletions(-) diff --git a/accept_test.go b/accept_test.go index 6f5c3fb9..8634066b 100644 --- a/accept_test.go +++ b/accept_test.go @@ -6,6 +6,39 @@ import ( "testing" ) +func TestAccept(t *testing.T) { + t.Parallel() + + t.Run("badClientHandshake", func(t *testing.T) { + t.Parallel() + + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/", nil) + + _, err := Accept(w, r, AcceptOptions{}) + if err == nil { + t.Fatalf("unexpected error value: %v", err) + } + + }) + + t.Run("requireHttpHijacker", func(t *testing.T) { + t.Parallel() + + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/", nil) + r.Header.Set("Connection", "Upgrade") + r.Header.Set("Upgrade", "websocket") + r.Header.Set("Sec-WebSocket-Version", "13") + r.Header.Set("Sec-WebSocket-Key", "meow123") + + _, err := Accept(w, r, AcceptOptions{}) + if err == nil || !strings.Contains(err.Error(), "http.Hijacker") { + t.Fatalf("unexpected error value: %v", err) + } + }) +} + func Test_verifyClientHandshake(t *testing.T) { t.Parallel() diff --git a/ci/test.sh b/ci/test.sh index 875216f1..1d4a8b07 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -4,19 +4,34 @@ set -euo pipefail cd "$(dirname "${0}")" cd "$(git rev-parse --show-toplevel)" -mkdir -p ci/out/websocket -testFlags=( +argv=( + go run gotest.tools/gotestsum + # https://circleci.com/docs/2.0/collect-test-data/ + "--junitfile=ci/out/websocket/testReport.xml" + "--format=short-verbose" + -- -race "-vet=off" - # "-bench=." + "-bench=." +) +# Interactive usage probably does not want to enable benchmarks, race detection +# turn off vet or use gotestsum by default. +if [[ $# -gt 0 ]]; then + argv=(go test "$@") +fi + +# We always want coverage. +argv+=( "-coverprofile=ci/out/coverage.prof" "-coverpkg=./..." ) -# https://circleci.com/docs/2.0/collect-test-data/ -go run gotest.tools/gotestsum \ - --junitfile ci/out/websocket/testReport.xml \ - --format=short-verbose \ - -- "${testFlags[@]}" + +mkdir -p ci/out/websocket +"${argv[@]}" + +# Removes coverage of generated files. +grep -v _string.go < ci/out/coverage.prof > ci/out/coverage2.prof +mv ci/out/coverage2.prof ci/out/coverage.prof go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html if [[ ${CI:-} ]]; then diff --git a/dial_test.go b/dial_test.go index 6400c223..4607493b 100644 --- a/dial_test.go +++ b/dial_test.go @@ -33,6 +33,10 @@ func TestBadDials(t *testing.T) { }, }, }, + { + name: "badTLS", + url: "wss://totallyfake.nhooyr.io", + }, } for _, tc := range testCases { @@ -40,7 +44,10 @@ func TestBadDials(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - _, _, err := Dial(context.Background(), tc.url, tc.opts) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + _, _, err := Dial(ctx, tc.url, tc.opts) if err == nil { t.Fatalf("expected non nil error: %+v", err) } diff --git a/export_test.go b/export_test.go index 22ad76fc..ab766f14 100644 --- a/export_test.go +++ b/export_test.go @@ -1,3 +1,13 @@ package websocket -var Compute = handleSecWebSocketKey +import ( + "context" +) + +type Addr = websocketAddr + +type Header = header + +func (c *Conn) WriteFrame(ctx context.Context, fin bool, opcode opcode, p []byte) (int, error) { + return c.writeFrame(ctx, fin, opcode, p) +} diff --git a/header_test.go b/header_test.go index 4457c356..45d0535a 100644 --- a/header_test.go +++ b/header_test.go @@ -2,6 +2,7 @@ package websocket import ( "bytes" + "io" "math/rand" "strconv" "testing" @@ -21,6 +22,36 @@ func randBool() bool { func TestHeader(t *testing.T) { t.Parallel() + t.Run("eof", func(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + bytes []byte + }{ + { + "start", + []byte{0xff}, + }, + { + "middle", + []byte{0xff, 0xff, 0xff}, + }, + } + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + b := bytes.NewBuffer(tc.bytes) + _, err := readHeader(nil, b) + if io.ErrUnexpectedEOF != err { + t.Fatalf("expected %v but got: %v", io.ErrUnexpectedEOF, err) + } + }) + } + }) + t.Run("writeNegativeLength", func(t *testing.T) { t.Parallel() diff --git a/netconn.go b/netconn.go index d28eeb84..a6f902da 100644 --- a/netconn.go +++ b/netconn.go @@ -101,8 +101,8 @@ func (c *netConn) Read(p []byte) (int, error) { return 0, err } if typ != c.msgType { - c.c.Close(StatusUnsupportedData, fmt.Sprintf("can only accept %v messages", c.msgType)) - return 0, xerrors.Errorf("unexpected frame type read for net conn adapter (expected %v): %v", c.msgType, typ) + c.c.Close(StatusUnsupportedData, fmt.Sprintf("unexpected frame type read (expected %v): %v", c.msgType, typ)) + return 0, c.c.closeErr } c.reader = r } diff --git a/statuscode.go b/statuscode.go index 42ae40c0..498437d0 100644 --- a/statuscode.go +++ b/statuscode.go @@ -35,7 +35,7 @@ const ( StatusTryAgainLater StatusBadGateway // statusTLSHandshake is unexported because we just return - // handshake error in dial. We do not return a conn + // the handshake error in dial. We do not return a conn // so there is nothing to use this on. At least until WASM. statusTLSHandshake ) diff --git a/statuscode_test.go b/statuscode_test.go index 38ee4c3f..b9637868 100644 --- a/statuscode_test.go +++ b/statuscode_test.go @@ -4,14 +4,13 @@ import ( "math" "strings" "testing" + + "github.com/google/go-cmp/cmp" ) func TestCloseError(t *testing.T) { t.Parallel() - // Other parts of close error are tested by websocket_test.go right now - // with the autobahn tests. - testCases := []struct { name string ce CloseError @@ -50,7 +49,108 @@ func TestCloseError(t *testing.T) { _, err := tc.ce.bytes() if (err == nil) != tc.success { - t.Fatalf("unexpected error value: %v", err) + t.Fatalf("unexpected error value: %+v", err) + } + }) + } +} + +func Test_parseClosePayload(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + p []byte + success bool + ce CloseError + }{ + { + name: "normal", + p: append([]byte{0x3, 0xE8}, []byte("hello")...), + success: true, + ce: CloseError{ + Code: StatusNormalClosure, + Reason: "hello", + }, + }, + { + name: "nothing", + success: true, + ce: CloseError{ + Code: StatusNoStatusRcvd, + }, + }, + { + name: "oneByte", + p: []byte{0}, + success: false, + }, + { + name: "badStatusCode", + p: []byte{0x17, 0x70}, + success: false, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ce, err := parseClosePayload(tc.p) + if (err == nil) != tc.success { + t.Fatalf("unexpected expected error value: %+v", err) + } + + if tc.success && tc.ce != ce { + t.Fatalf("unexpected close error: %v", cmp.Diff(tc.ce, ce)) + } + }) + } +} + +func Test_validWireCloseCode(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + code StatusCode + valid bool + }{ + { + name: "normal", + code: StatusNormalClosure, + valid: true, + }, + { + name: "noStatus", + code: StatusNoStatusRcvd, + valid: false, + }, + { + name: "3000", + code: 3000, + valid: true, + }, + { + name: "4999", + code: 4999, + valid: true, + }, + { + name: "unknown", + code: 5000, + valid: false, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + if valid := validWireCloseCode(tc.code); tc.valid != valid { + t.Fatalf("expected %v for %v but got %v", tc.valid, tc.code, valid) } }) } diff --git a/websocket.go b/websocket.go index 393ea547..833c1209 100644 --- a/websocket.go +++ b/websocket.go @@ -7,8 +7,8 @@ import ( "fmt" "io" "io/ioutil" + "log" "math/rand" - "os" "runtime" "strconv" "sync" @@ -210,9 +210,8 @@ func (c *Conn) readTillMsg(ctx context.Context) (header, error) { } if h.rsv1 || h.rsv2 || h.rsv3 { - err := xerrors.Errorf("received header with rsv bits set: %v:%v:%v", h.rsv1, h.rsv2, h.rsv3) - c.Close(StatusProtocolError, err.Error()) - return header{}, err + c.Close(StatusProtocolError, fmt.Sprintf("received header with rsv bits set: %v:%v:%v", h.rsv1, h.rsv2, h.rsv3)) + return header{}, c.closeErr } if h.opcode.controlOp() { @@ -227,9 +226,8 @@ func (c *Conn) readTillMsg(ctx context.Context) (header, error) { case opBinary, opText, opContinuation: return h, nil default: - err := xerrors.Errorf("received unknown opcode %v", h.opcode) - c.Close(StatusProtocolError, err.Error()) - return header{}, err + c.Close(StatusProtocolError, fmt.Sprintf("received unknown opcode %v", h.opcode)) + return header{}, c.closeErr } } } @@ -273,15 +271,13 @@ func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { func (c *Conn) handleControl(ctx context.Context, h header) error { if h.payloadLength > maxControlFramePayload { - err := xerrors.Errorf("control frame too large at %v bytes", h.payloadLength) - c.Close(StatusProtocolError, err.Error()) - return err + c.Close(StatusProtocolError, fmt.Sprintf("control frame too large at %v bytes", h.payloadLength)) + return c.closeErr } if !h.fin { - err := xerrors.Errorf("received fragmented control frame") - c.Close(StatusProtocolError, err.Error()) - return err + c.Close(StatusProtocolError, "received fragmented control frame") + return c.closeErr } ctx, cancel := context.WithTimeout(ctx, time.Second*5) @@ -311,8 +307,9 @@ func (c *Conn) handleControl(ctx context.Context, h header) error { case opClose: ce, err := parseClosePayload(b) if err != nil { - c.Close(StatusProtocolError, "received invalid close payload") - return xerrors.Errorf("received invalid close payload: %w", err) + err = xerrors.Errorf("received invalid close payload: %w", err) + c.Close(StatusProtocolError, err.Error()) + return c.closeErr } // This ensures the closeErr of the Conn is always the received CloseError // in case the echo close frame write fails. @@ -376,9 +373,8 @@ func (c *Conn) reader(ctx context.Context) (MessageType, io.Reader, error) { if c.activeReader != nil && !c.activeReader.eof() { if h.opcode != opContinuation { - err := xerrors.Errorf("received new data message without finishing the previous message") - c.Close(StatusProtocolError, err.Error()) - return 0, nil, err + c.Close(StatusProtocolError, "received new data message without finishing the previous message") + return 0, nil, c.closeErr } if !h.fin || h.payloadLength > 0 { @@ -392,9 +388,8 @@ func (c *Conn) reader(ctx context.Context) (MessageType, io.Reader, error) { return 0, nil, err } } else if h.opcode == opContinuation { - err := xerrors.Errorf("received continuation frame not after data or text frame") - c.Close(StatusProtocolError, err.Error()) - return 0, nil, err + c.Close(StatusProtocolError, "received continuation frame not after data or text frame") + return 0, nil, c.closeErr } c.readerMsgCtx = ctx @@ -460,9 +455,8 @@ func (r *messageReader) read(p []byte) (int, error) { } if r.c.readMsgLeft <= 0 { - err := xerrors.Errorf("read limited at %v bytes", r.c.msgReadLimit) - r.c.Close(StatusMessageTooBig, err.Error()) - return 0, err + r.c.Close(StatusMessageTooBig, fmt.Sprintf("read limited at %v bytes", r.c.msgReadLimit)) + return 0, r.c.closeErr } if int64(len(p)) > r.c.readMsgLeft { @@ -476,9 +470,8 @@ func (r *messageReader) read(p []byte) (int, error) { } if h.opcode != opContinuation { - err := xerrors.Errorf("received new data message without finishing the previous message") - r.c.Close(StatusProtocolError, err.Error()) - return 0, err + r.c.Close(StatusProtocolError, "received new data message without finishing the previous message") + return 0, r.c.closeErr } r.c.readerMsgHeader = h @@ -828,7 +821,7 @@ func (c *Conn) writePong(p []byte) error { func (c *Conn) Close(code StatusCode, reason string) error { err := c.exportedClose(code, reason) if err != nil { - return xerrors.Errorf("failed to close connection: %w", err) + return xerrors.Errorf("failed to close websocket connection: %w", err) } return nil } @@ -844,7 +837,7 @@ func (c *Conn) exportedClose(code StatusCode, reason string) error { // Definitely worth seeing what popular browsers do later. p, err := ce.bytes() if err != nil { - fmt.Fprintf(os.Stderr, "websocket: failed to marshal close frame: %v\n", err) + log.Printf("websocket: failed to marshal close frame: %+v", err) ce = CloseError{ Code: StatusInternalError, } @@ -853,12 +846,13 @@ func (c *Conn) exportedClose(code StatusCode, reason string) error { // CloseErrors sent are made opaque to prevent applications from thinking // they received a given status. - err = c.writeClose(p, xerrors.Errorf("sent close frame: %v", ce)) + sentErr := xerrors.Errorf("sent close frame: %v", ce) + err = c.writeClose(p, sentErr) if err != nil { return err } - if !xerrors.Is(c.closeErr, ce) { + if !xerrors.Is(c.closeErr, sentErr) { return c.closeErr } diff --git a/websocket_test.go b/websocket_test.go index 2ef25cdd..b45f024f 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -4,8 +4,11 @@ import ( "context" "encoding/json" "fmt" + "github.com/golang/protobuf/proto" + "github.com/golang/protobuf/ptypes/timestamp" "io" "io/ioutil" + "math/rand" "net" "net/http" "net/http/cookiejar" @@ -75,127 +78,6 @@ func TestHandshake(t *testing.T) { return nil }, }, - { - name: "closeError", - server: func(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, websocket.AcceptOptions{}) - if err != nil { - return err - } - defer c.Close(websocket.StatusInternalError, "") - - err = wsjson.Write(r.Context(), c, "hello") - if err != nil { - return err - } - - return nil - }, - client: func(ctx context.Context, u string) error { - c, _, err := websocket.Dial(ctx, u, websocket.DialOptions{ - Subprotocols: []string{"meow"}, - }) - if err != nil { - return err - } - defer c.Close(websocket.StatusInternalError, "") - - var m string - err = wsjson.Read(ctx, c, &m) - if err != nil { - return err - } - - if m != "hello" { - return xerrors.Errorf("recieved unexpected msg but expected hello: %+v", m) - } - - _, _, err = c.Reader(ctx) - var cerr websocket.CloseError - if !xerrors.As(err, &cerr) || cerr.Code != websocket.StatusInternalError { - return xerrors.Errorf("unexpected error: %+v", err) - } - - return nil - }, - }, - { - name: "netConn", - server: func(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, websocket.AcceptOptions{}) - if err != nil { - return err - } - defer c.Close(websocket.StatusInternalError, "") - - nc := websocket.NetConn(c, websocket.MessageBinary) - defer nc.Close() - - nc.SetWriteDeadline(time.Time{}) - time.Sleep(1) - nc.SetWriteDeadline(time.Now().Add(time.Second * 15)) - - for i := 0; i < 3; i++ { - _, err = nc.Write([]byte("hello")) - if err != nil { - return err - } - } - - return nil - }, - client: func(ctx context.Context, u string) error { - c, _, err := websocket.Dial(ctx, u, websocket.DialOptions{ - Subprotocols: []string{"meow"}, - }) - if err != nil { - return err - } - defer c.Close(websocket.StatusInternalError, "") - - nc := websocket.NetConn(c, websocket.MessageBinary) - defer nc.Close() - - nc.SetReadDeadline(time.Time{}) - time.Sleep(1) - nc.SetReadDeadline(time.Now().Add(time.Second * 15)) - - read := func() error { - p := make([]byte, len("hello")) - // We do not use io.ReadFull here as it masks EOFs. - // See https://github.com/nhooyr/websocket/issues/100#issuecomment-508148024 - _, err = nc.Read(p) - if err != nil { - return err - } - - if string(p) != "hello" { - return xerrors.Errorf("unexpected payload %q received", string(p)) - } - return nil - } - - for i := 0; i < 3; i++ { - err = read() - if err != nil { - return err - } - } - - // Ensure the close frame is converted to an EOF and multiple read's after all return EOF. - err = read() - if err != io.EOF { - return err - } - - err = read() - if err != io.EOF { - return err - } - - return nil - }, - }, { name: "defaultSubprotocol", server: func(w http.ResponseWriter, r *http.Request) error { @@ -323,22 +205,240 @@ func TestHandshake(t *testing.T) { if err != nil { return err } - defer c.Close(websocket.StatusInternalError, "") + defer c.Close(websocket.StatusInternalError, "") + return nil + }, + }, + { + name: "cookies", + server: func(w http.ResponseWriter, r *http.Request) error { + cookie, err := r.Cookie("mycookie") + if err != nil { + return xerrors.Errorf("request is missing mycookie: %w", err) + } + if cookie.Value != "myvalue" { + return xerrors.Errorf("expected %q but got %q", "myvalue", cookie.Value) + } + c, err := websocket.Accept(w, r, websocket.AcceptOptions{}) + if err != nil { + return err + } + c.Close(websocket.StatusInternalError, "") + return nil + }, + client: func(ctx context.Context, u string) error { + jar, err := cookiejar.New(nil) + if err != nil { + return xerrors.Errorf("failed to create cookie jar: %w", err) + } + parsedURL, err := url.Parse(u) + if err != nil { + return xerrors.Errorf("failed to parse url: %w", err) + } + parsedURL.Scheme = "http" + jar.SetCookies(parsedURL, []*http.Cookie{ + { + Name: "mycookie", + Value: "myvalue", + }, + }) + hc := &http.Client{ + Jar: jar, + } + c, _, err := websocket.Dial(ctx, u, websocket.DialOptions{ + HTTPClient: hc, + }) + if err != nil { + return err + } + c.Close(websocket.StatusInternalError, "") + return nil + }, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + s, closeFn := testServer(t, tc.server, false) + defer closeFn() + + wsURL := strings.Replace(s.URL, "http", "ws", 1) + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + err := tc.client(ctx, wsURL) + if err != nil { + t.Fatalf("client failed: %+v", err) + } + }) + } +} + +func TestConn(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + client func(ctx context.Context, c *websocket.Conn) error + server func(ctx context.Context, c *websocket.Conn) error + }{ + { + name: "closeError", + server: func(ctx context.Context, c *websocket.Conn) error { + return wsjson.Write(ctx, c, "hello") + }, + client: func(ctx context.Context, c *websocket.Conn) error { + var m string + err := wsjson.Read(ctx, c, &m) + if err != nil { + return err + } + + if m != "hello" { + return xerrors.Errorf("recieved unexpected msg but expected hello: %+v", m) + } + + _, _, err = c.Reader(ctx) + var cerr websocket.CloseError + if !xerrors.As(err, &cerr) || cerr.Code != websocket.StatusInternalError { + return xerrors.Errorf("unexpected error: %+v", err) + } + + return nil + }, + }, + { + name: "netConn", + server: func(ctx context.Context, c *websocket.Conn) error { + nc := websocket.NetConn(c, websocket.MessageBinary) + defer nc.Close() + + nc.SetWriteDeadline(time.Time{}) + time.Sleep(1) + nc.SetWriteDeadline(time.Now().Add(time.Second * 15)) + + if nc.LocalAddr() != (websocket.Addr{}) { + return xerrors.Errorf("net conn local address is not equal to websocket.Addr") + } + if nc.RemoteAddr() != (websocket.Addr{}) { + return xerrors.Errorf("net conn remote address is not equal to websocket.Addr") + } + + for i := 0; i < 3; i++ { + _, err := nc.Write([]byte("hello")) + if err != nil { + return err + } + } + + return nil + }, + client: func(ctx context.Context, c *websocket.Conn) error { + nc := websocket.NetConn(c, websocket.MessageBinary) + defer nc.Close() + + nc.SetReadDeadline(time.Time{}) + time.Sleep(1) + nc.SetReadDeadline(time.Now().Add(time.Second * 15)) + + read := func() error { + p := make([]byte, len("hello")) + // We do not use io.ReadFull here as it masks EOFs. + // See https://github.com/nhooyr/websocket/issues/100#issuecomment-508148024 + _, err := nc.Read(p) + if err != nil { + return err + } + + if string(p) != "hello" { + return xerrors.Errorf("unexpected payload %q received", string(p)) + } + return nil + } + + for i := 0; i < 3; i++ { + err := read() + if err != nil { + return err + } + } + + // Ensure the close frame is converted to an EOF and multiple read's after all return EOF. + err := read() + if err != io.EOF { + return err + } + + err = read() + if err != io.EOF { + return err + } + + return nil + }, + }, + { + name: "netConn/badReadMsgType", + server: func(ctx context.Context, c *websocket.Conn) error { + nc := websocket.NetConn(c, websocket.MessageBinary) + defer nc.Close() + + nc.SetDeadline(time.Now().Add(time.Second * 15)) + + _, err := nc.Read(make([]byte, 1)) + if err == nil { + return xerrors.Errorf("expected error") + } + + return nil + }, + client: func(ctx context.Context, c *websocket.Conn) error { + err := wsjson.Write(ctx, c, "meow") + if err != nil { + return err + } + + _, _, err = c.Read(ctx) + cerr := &websocket.CloseError{} + if !xerrors.As(err, cerr) || cerr.Code != websocket.StatusUnsupportedData { + return xerrors.Errorf("expected close error with code StatusUnsupportedData: %+v", err) + } + + return nil + }, + }, + { + name: "netConn/badRead", + server: func(ctx context.Context, c *websocket.Conn) error { + nc := websocket.NetConn(c, websocket.MessageBinary) + defer nc.Close() + + nc.SetDeadline(time.Now().Add(time.Second * 15)) + + _, err := nc.Read(make([]byte, 1)) + cerr := &websocket.CloseError{} + if !xerrors.As(err, cerr) || cerr.Code != websocket.StatusBadGateway { + return xerrors.Errorf("expected close error with code StatusBadGateway: %+v", err) + } + + _, err = nc.Write([]byte{0xff}) + if err == nil { + return xerrors.Errorf("expected writes to fail after reading a close frame: %v", err) + } + return nil }, + client: func(ctx context.Context, c *websocket.Conn) error { + return c.Close(websocket.StatusBadGateway, "") + }, }, { name: "jsonEcho", - server: func(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, websocket.AcceptOptions{}) - if err != nil { - return err - } - defer c.Close(websocket.StatusInternalError, "") - - ctx, cancel := context.WithTimeout(r.Context(), time.Second*5) - defer cancel() - + server: func(ctx context.Context, c *websocket.Conn) error { write := func() error { v := map[string]interface{}{ "anmol": "wowow", @@ -346,7 +446,7 @@ func TestHandshake(t *testing.T) { err := wsjson.Write(ctx, c, v) return err } - err = write() + err := write() if err != nil { return err } @@ -358,13 +458,7 @@ func TestHandshake(t *testing.T) { c.Close(websocket.StatusNormalClosure, "") return nil }, - client: func(ctx context.Context, u string) error { - c, _, err := websocket.Dial(ctx, u, websocket.DialOptions{}) - if err != nil { - return err - } - defer c.Close(websocket.StatusInternalError, "") - + client: func(ctx context.Context, c *websocket.Conn) error { read := func() error { var v interface{} err := wsjson.Read(ctx, c, &v) @@ -380,7 +474,7 @@ func TestHandshake(t *testing.T) { } return nil } - err = read() + err := read() if err != nil { return err } @@ -395,21 +489,12 @@ func TestHandshake(t *testing.T) { }, { name: "protobufEcho", - server: func(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, websocket.AcceptOptions{}) - if err != nil { - return err - } - defer c.Close(websocket.StatusInternalError, "") - - ctx, cancel := context.WithTimeout(r.Context(), time.Second*5) - defer cancel() - + server: func(ctx context.Context, c *websocket.Conn) error { write := func() error { err := wspb.Write(ctx, c, ptypes.DurationProto(100)) return err } - err = write() + err := write() if err != nil { return err } @@ -417,13 +502,7 @@ func TestHandshake(t *testing.T) { c.Close(websocket.StatusNormalClosure, "") return nil }, - client: func(ctx context.Context, u string) error { - c, _, err := websocket.Dial(ctx, u, websocket.DialOptions{}) - if err != nil { - return err - } - defer c.Close(websocket.StatusInternalError, "") - + client: func(ctx context.Context, c *websocket.Conn) error { read := func() error { var v duration.Duration err := wspb.Read(ctx, c, &v) @@ -441,7 +520,7 @@ func TestHandshake(t *testing.T) { } return nil } - err = read() + err := read() if err != nil { return err } @@ -450,73 +529,21 @@ func TestHandshake(t *testing.T) { return nil }, }, - { - name: "cookies", - server: func(w http.ResponseWriter, r *http.Request) error { - cookie, err := r.Cookie("mycookie") - if err != nil { - return xerrors.Errorf("request is missing mycookie: %w", err) - } - if cookie.Value != "myvalue" { - return xerrors.Errorf("expected %q but got %q", "myvalue", cookie.Value) - } - c, err := websocket.Accept(w, r, websocket.AcceptOptions{}) - if err != nil { - return err - } - c.Close(websocket.StatusInternalError, "") - return nil - }, - client: func(ctx context.Context, u string) error { - jar, err := cookiejar.New(nil) - if err != nil { - return xerrors.Errorf("failed to create cookie jar: %w", err) - } - parsedURL, err := url.Parse(u) - if err != nil { - return xerrors.Errorf("failed to parse url: %w", err) - } - parsedURL.Scheme = "http" - jar.SetCookies(parsedURL, []*http.Cookie{ - { - Name: "mycookie", - Value: "myvalue", - }, - }) - hc := &http.Client{ - Jar: jar, - } - c, _, err := websocket.Dial(ctx, u, websocket.DialOptions{ - HTTPClient: hc, - }) - if err != nil { - return err - } - c.Close(websocket.StatusInternalError, "") - return nil - }, - }, { name: "ping", - server: func(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, websocket.AcceptOptions{}) - if err != nil { - return err - } - defer c.Close(websocket.StatusInternalError, "") - + server: func(ctx context.Context, c *websocket.Conn) error { errc := make(chan error, 1) go func() { - _, _, err2 := c.Read(r.Context()) + _, _, err2 := c.Read(ctx) errc <- err2 }() - err = c.Ping(r.Context()) + err := c.Ping(ctx) if err != nil { return err } - err = c.Write(r.Context(), websocket.MessageText, []byte("hi")) + err = c.Write(ctx, websocket.MessageText, []byte("hi")) if err != nil { return err } @@ -528,13 +555,7 @@ func TestHandshake(t *testing.T) { } return xerrors.Errorf("unexpected error: %w", err) }, - client: func(ctx context.Context, u string) error { - c, _, err := websocket.Dial(ctx, u, websocket.DialOptions{}) - if err != nil { - return err - } - defer c.Close(websocket.StatusInternalError, "") - + client: func(ctx context.Context, c *websocket.Conn) error { // We read a message from the connection and then keep reading until // the Ping completes. done := make(chan struct{}) @@ -550,7 +571,7 @@ func TestHandshake(t *testing.T) { c.Read(ctx) }() - err = c.Ping(ctx) + err := c.Ping(ctx) if err != nil { return err } @@ -563,29 +584,17 @@ func TestHandshake(t *testing.T) { }, { name: "readLimit", - server: func(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, websocket.AcceptOptions{}) - if err != nil { - return err - } - defer c.Close(websocket.StatusInternalError, "") - - _, _, err = c.Read(r.Context()) + server: func(ctx context.Context, c *websocket.Conn) error { + _, _, err := c.Read(ctx) if err == nil { return xerrors.Errorf("expected error but got nil") } return nil }, - client: func(ctx context.Context, u string) error { - c, _, err := websocket.Dial(ctx, u, websocket.DialOptions{}) - if err != nil { - return err - } - defer c.Close(websocket.StatusInternalError, "") + client: func(ctx context.Context, c *websocket.Conn) error { + go c.CloseRead(ctx) - go c.Reader(ctx) - - err = c.Write(ctx, websocket.MessageBinary, []byte(strings.Repeat("x", 32769))) + err := c.Write(ctx, websocket.MessageBinary, []byte(strings.Repeat("x", 32769))) if err != nil { return err } @@ -600,20 +609,244 @@ func TestHandshake(t *testing.T) { return nil }, }, - } + { + name: "wsjson/binary", + server: func(ctx context.Context, c *websocket.Conn) error { + var v interface{} + err := wsjson.Read(ctx, c, &v) + if err == nil { + return xerrors.Errorf("expected error: %v", err) + } + return nil + }, + client: func(ctx context.Context, c *websocket.Conn) error { + return wspb.Write(ctx, c, ptypes.DurationProto(100)) + }, + }, + { + name: "wsjson/badRead", + server: func(ctx context.Context, c *websocket.Conn) error { + var v interface{} + err := wsjson.Read(ctx, c, &v) + if err == nil { + return xerrors.Errorf("expected error: %v", err) + } + return nil + }, + client: func(ctx context.Context, c *websocket.Conn) error { + return c.Write(ctx, websocket.MessageText, []byte("notjson")) + }, + }, + { + name: "wsjson/badWrite", + server: func(ctx context.Context, c *websocket.Conn) error { + _, _, err := c.Read(ctx) + if err == nil { + return xerrors.Errorf("expected error: %v", err) + } + return nil + }, + client: func(ctx context.Context, c *websocket.Conn) error { + err := wsjson.Write(ctx, c, fmt.Println) + if err == nil { + return xerrors.Errorf("expected error: %v", err) + } + return nil + }, + }, + { + name: "wspb/text", + server: func(ctx context.Context, c *websocket.Conn) error { + var v proto.Message + err := wspb.Read(ctx, c, v) + if err == nil { + return xerrors.Errorf("expected error: %v", err) + } + return nil + }, + client: func(ctx context.Context, c *websocket.Conn) error { + return wsjson.Write(ctx, c, "hi") + }, + }, + { + name: "wspb/badRead", + server: func(ctx context.Context, c *websocket.Conn) error { + var v timestamp.Timestamp + err := wspb.Read(ctx, c, &v) + if err == nil { + return xerrors.Errorf("expected error: %v", err) + } + return nil + }, + client: func(ctx context.Context, c *websocket.Conn) error { + return c.Write(ctx, websocket.MessageBinary, []byte("notpb")) + }, + }, + { + name: "wspb/badWrite", + server: func(ctx context.Context, c *websocket.Conn) error { + _, _, err := c.Read(ctx) + if err == nil { + return xerrors.Errorf("expected error: %v", err) + } + return nil + }, + client: func(ctx context.Context, c *websocket.Conn) error { + err := wspb.Write(ctx, c, nil) + if err == nil { + return xerrors.Errorf("expected error: %v", err) + } + return nil + }, + }, + { + name: "wspb/badWrite", + server: func(ctx context.Context, c *websocket.Conn) error { + _, _, err := c.Read(ctx) + if err == nil { + return xerrors.Errorf("expected error: %v", err) + } + return nil + }, + client: func(ctx context.Context, c *websocket.Conn) error { + err := wspb.Write(ctx, c, nil) + if err == nil { + return xerrors.Errorf("expected error: %v", err) + } + return nil + }, + }, + { + name: "badClose", + server: func(ctx context.Context, c *websocket.Conn) error { + return c.Close(9999, "") + }, + client: func(ctx context.Context, c *websocket.Conn) error { + _, _, err := c.Read(ctx) + cerr := &websocket.CloseError{} + if !xerrors.As(err, cerr) || cerr.Code != websocket.StatusInternalError { + return xerrors.Errorf("expected close error with StatusInternalError: %+v", err) + } + return nil + }, + }, + { + name: "pingTimeout", + server: func(ctx context.Context, c *websocket.Conn) error { + ctx, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() + err := c.Ping(ctx) + if err == nil { + return xerrors.Errorf("expected nil error: %+v", err) + } + return nil + }, + client: func(ctx context.Context, c *websocket.Conn) error { + time.Sleep(time.Second) + return nil + }, + }, + { + name: "writeTimeout", + server: func(ctx context.Context, c *websocket.Conn) error { + c.Writer(ctx, websocket.MessageBinary) + ctx, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() + err := c.Write(ctx, websocket.MessageBinary, []byte("meow")) + if !xerrors.Is(err, context.DeadlineExceeded) { + return xerrors.Errorf("expected deadline exceeded error: %+v", err) + } + return nil + }, + client: func(ctx context.Context, c *websocket.Conn) error { + time.Sleep(time.Second) + return nil + }, + }, + { + name: "readTimeout", + server: func(ctx context.Context, c *websocket.Conn) error { + ctx, cancel := context.WithTimeout(ctx, time.Second) + defer cancel() + _, r, err := c.Reader(ctx) + if err != nil { + return err + } + <-ctx.Done() + _, err = r.Read(make([]byte, 1)) + if !xerrors.Is(err, context.DeadlineExceeded){ + return xerrors.Errorf("expected deadline exceeded error: %+v", err) + } + return nil + }, + client: func(ctx context.Context, c *websocket.Conn) error { + time.Sleep(time.Second) + return nil + }, + }, + { + name: "badOpCode", + server: func(ctx context.Context, c *websocket.Conn) error { + _, err := c.WriteFrame(ctx, true, 13, []byte("meow")) + if err != nil { + return err + } + _, _, err = c.Read(ctx) + cerr := &websocket.CloseError{} + if !xerrors.As(err, cerr) || cerr.Code != websocket.StatusProtocolError { + return xerrors.Errorf("expected close error with StatusProtocolError: %+v", err) + } + return nil + }, + client: func(ctx context.Context, c *websocket.Conn) error { + _, _, err := c.Read(ctx) + if err == nil || strings.Contains(err.Error(), "opcode") { + return xerrors.Errorf("expected error that contains opcode: %+v", err) + } + return nil + }, + }, + { + name: "noRsv", + server: func(ctx context.Context, c *websocket.Conn) error { + _, err := c.WriteFrame(ctx, true, 99, []byte("meow")) + if err != nil { + return err + } + _, _, err = c.Read(ctx) + cerr := &websocket.CloseError{} + if !xerrors.As(err, cerr) || cerr.Code != websocket.StatusProtocolError { + return xerrors.Errorf("expected close error with StatusProtocolError: %+v", err) + } + return nil + }, + client: func(ctx context.Context, c *websocket.Conn) error { + _, _, err := c.Read(ctx) + if err == nil || !strings.Contains(err.Error(), "rsv") { + return xerrors.Errorf("expected error that contains rsv: %+v", err) + } + return nil + }, + }, + } for _, tc := range testCases { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() - s, closeFn := testServer(t, func(w http.ResponseWriter, r *http.Request) { - err := tc.server(w, r) + // Run random tests over TLS. + tls := rand.Intn(2) == 1 + + s, closeFn := testServer(t, func(w http.ResponseWriter, r *http.Request) error { + c, err := websocket.Accept(w, r, websocket.AcceptOptions{}) if err != nil { - t.Errorf("server failed: %+v", err) - return + return err } - }) + defer c.Close(websocket.StatusInternalError, "") + tc.server(r.Context(), c) + return nil + }, tls) defer closeFn() wsURL := strings.Replace(s.URL, "http", "ws", 1) @@ -621,7 +854,18 @@ func TestHandshake(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - err := tc.client(ctx, wsURL) + opts := websocket.DialOptions{} + if tls { + opts.HTTPClient = s.Client() + } + + c, _, err := websocket.Dial(ctx, wsURL, opts) + if err != nil { + t.Fatal(err) + } + defer c.Close(websocket.StatusInternalError, "") + + err = tc.client(ctx, c) if err != nil { t.Fatalf("client failed: %+v", err) } @@ -629,14 +873,31 @@ func TestHandshake(t *testing.T) { } } -func testServer(tb testing.TB, fn http.HandlerFunc) (s *httptest.Server, closeFn func()) { +func init() { + rand.Seed(time.Now().UnixNano()) +} + +func testServer(tb testing.TB, fn func(w http.ResponseWriter, r *http.Request) error, tls bool) (s *httptest.Server, closeFn func()) { var conns int64 - s = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { atomic.AddInt64(&conns, 1) defer atomic.AddInt64(&conns, -1) - fn.ServeHTTP(w, r) - })) + ctx, cancel := context.WithTimeout(r.Context(), time.Second*30) + defer cancel() + + r = r.WithContext(ctx) + + err := fn(w, r) + if err != nil { + tb.Errorf("server failed: %+v", err) + } + }) + if tls { + s = httptest.NewTLSServer(h) + } else { + s = httptest.NewServer(h) + } return s, func() { s.Close() @@ -654,7 +915,9 @@ func testServer(tb testing.TB, fn http.HandlerFunc) (s *httptest.Server, closeFn // https://github.com/crossbario/autobahn-python/tree/master/wstest func TestAutobahnServer(t *testing.T) { t.Parallel() - t.Skip() + if os.Getenv("AUTOBAHN") == "" { + t.Skip("Set $AUTOBAHN to run the autobahn test suite.") + } s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c, err := websocket.Accept(w, r, websocket.AcceptOptions{ @@ -795,7 +1058,9 @@ func unusedListenAddr() (string, error) { // https://github.com/crossbario/autobahn-python/blob/master/wstest/testee_client_aio.py func TestAutobahnClient(t *testing.T) { t.Parallel() - t.Skip() + if os.Getenv("AUTOBAHN") == "" { + t.Skip("Set $AUTOBAHN to run the autobahn test suite.") + } serverAddr, err := unusedListenAddr() if err != nil { @@ -941,18 +1206,18 @@ func checkWSTestIndex(t *testing.T, path string) { } func benchConn(b *testing.B, echo, stream bool, size int) { - s, closeFn := testServer(b, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + s, closeFn := testServer(b, func(w http.ResponseWriter, r *http.Request) error { c, err := websocket.Accept(w, r, websocket.AcceptOptions{}) if err != nil { - b.Logf("server handshake failed: %+v", err) - return + return err } if echo { echoLoop(r.Context(), c) } else { discardLoop(r.Context(), c) } - })) + return nil + }, false) defer closeFn() wsURL := strings.Replace(s.URL, "http", "ws", 1) From 537b26b9c25f621a1e6299b8397ed9684838c12a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 29 Aug 2019 17:07:20 -0500 Subject: [PATCH 087/519] Change options to be pointer structures Closes #122 --- README.md | 4 ++-- accept.go | 8 ++++++-- accept_test.go | 4 ++-- dial.go | 12 ++++++++++-- dial_test.go | 4 ++-- example_echo_test.go | 4 ++-- example_test.go | 6 +++--- websocket_test.go | 46 ++++++++++++++++++++++---------------------- 8 files changed, 50 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index cf20b877..d53046c8 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ For a production quality example that shows off the full API, see the [echo exam ```go http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) { - c, err := websocket.Accept(w, r, websocket.AcceptOptions{}) + c, err := websocket.Accept(w, r, nil) if err != nil { // ... } @@ -64,7 +64,7 @@ in net/http](https://github.com/golang/go/issues/26937#issuecomment-415855861) t ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() -c, _, err := websocket.Dial(ctx, "ws://localhost:8080", websocket.DialOptions{}) +c, _, err := websocket.Dial(ctx, "ws://localhost:8080", nil) if err != nil { // ... } diff --git a/accept.go b/accept.go index 7b727d16..afad1be2 100644 --- a/accept.go +++ b/accept.go @@ -84,7 +84,7 @@ func verifyClientRequest(w http.ResponseWriter, r *http.Request) error { // // If an error occurs, Accept will always write an appropriate response so you do not // have to. -func Accept(w http.ResponseWriter, r *http.Request, opts AcceptOptions) (*Conn, error) { +func Accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, error) { c, err := accept(w, r, opts) if err != nil { return nil, xerrors.Errorf("failed to accept websocket connection: %w", err) @@ -92,7 +92,11 @@ func Accept(w http.ResponseWriter, r *http.Request, opts AcceptOptions) (*Conn, return c, nil } -func accept(w http.ResponseWriter, r *http.Request, opts AcceptOptions) (*Conn, error) { +func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, error) { + if opts == nil { + opts = &AcceptOptions{} + } + err := verifyClientRequest(w, r) if err != nil { return nil, err diff --git a/accept_test.go b/accept_test.go index 8634066b..6602a8d0 100644 --- a/accept_test.go +++ b/accept_test.go @@ -15,7 +15,7 @@ func TestAccept(t *testing.T) { w := httptest.NewRecorder() r := httptest.NewRequest("GET", "/", nil) - _, err := Accept(w, r, AcceptOptions{}) + _, err := Accept(w, r, nil) if err == nil { t.Fatalf("unexpected error value: %v", err) } @@ -32,7 +32,7 @@ func TestAccept(t *testing.T) { r.Header.Set("Sec-WebSocket-Version", "13") r.Header.Set("Sec-WebSocket-Key", "meow123") - _, err := Accept(w, r, AcceptOptions{}) + _, err := Accept(w, r, nil) if err == nil || !strings.Contains(err.Error(), "http.Hijacker") { t.Fatalf("unexpected error value: %v", err) } diff --git a/dial.go b/dial.go index ac632c11..461817f6 100644 --- a/dial.go +++ b/dial.go @@ -41,7 +41,7 @@ type DialOptions struct { // This function requires at least Go 1.12 to succeed as it uses a new feature // in net/http to perform WebSocket handshakes and get a writable body // from the transport. See https://github.com/golang/go/issues/26937#issuecomment-415855861 -func Dial(ctx context.Context, u string, opts DialOptions) (*Conn, *http.Response, error) { +func Dial(ctx context.Context, u string, opts *DialOptions) (*Conn, *http.Response, error) { c, r, err := dial(ctx, u, opts) if err != nil { return nil, r, xerrors.Errorf("failed to websocket dial: %w", err) @@ -49,7 +49,15 @@ func Dial(ctx context.Context, u string, opts DialOptions) (*Conn, *http.Respons return c, r, nil } -func dial(ctx context.Context, u string, opts DialOptions) (_ *Conn, _ *http.Response, err error) { +func dial(ctx context.Context, u string, opts *DialOptions) (_ *Conn, _ *http.Response, err error) { + if opts == nil { + opts = &DialOptions{} + } + + // Shallow copy to ensure defaults do not affect user passed options. + opts2 := *opts + opts = &opts2 + if opts.HTTPClient == nil { opts.HTTPClient = http.DefaultClient } diff --git a/dial_test.go b/dial_test.go index 4607493b..96537bdb 100644 --- a/dial_test.go +++ b/dial_test.go @@ -14,7 +14,7 @@ func TestBadDials(t *testing.T) { testCases := []struct { name string url string - opts DialOptions + opts *DialOptions }{ { name: "badURL", @@ -27,7 +27,7 @@ func TestBadDials(t *testing.T) { { name: "badHTTPClient", url: "ws://nhooyr.io", - opts: DialOptions{ + opts: &DialOptions{ HTTPClient: &http.Client{ Timeout: time.Minute, }, diff --git a/example_echo_test.go b/example_echo_test.go index 6923bc04..3e7e7f9d 100644 --- a/example_echo_test.go +++ b/example_echo_test.go @@ -68,7 +68,7 @@ func Example_echo() { func echoServer(w http.ResponseWriter, r *http.Request) error { log.Printf("serving %v", r.RemoteAddr) - c, err := websocket.Accept(w, r, websocket.AcceptOptions{ + c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ Subprotocols: []string{"echo"}, }) if err != nil { @@ -128,7 +128,7 @@ func client(url string) error { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - c, _, err := websocket.Dial(ctx, url, websocket.DialOptions{ + c, _, err := websocket.Dial(ctx, url, &websocket.DialOptions{ Subprotocols: []string{"echo"}, }) if err != nil { diff --git a/example_test.go b/example_test.go index 0b59e6a0..22c31202 100644 --- a/example_test.go +++ b/example_test.go @@ -14,7 +14,7 @@ import ( // message from the client and then closes the connection. func ExampleAccept() { fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c, err := websocket.Accept(w, r, websocket.AcceptOptions{}) + c, err := websocket.Accept(w, r, nil) if err != nil { log.Println(err) return @@ -46,7 +46,7 @@ func ExampleDial() { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - c, _, err := websocket.Dial(ctx, "ws://localhost:8080", websocket.DialOptions{}) + c, _, err := websocket.Dial(ctx, "ws://localhost:8080", nil) if err != nil { log.Fatal(err) } @@ -64,7 +64,7 @@ func ExampleDial() { // on which you will only write and do not expect to read data messages. func Example_writeOnly() { fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c, err := websocket.Accept(w, r, websocket.AcceptOptions{}) + c, err := websocket.Accept(w, r, nil) if err != nil { log.Println(err) return diff --git a/websocket_test.go b/websocket_test.go index b45f024f..1f1b5245 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -44,7 +44,7 @@ func TestHandshake(t *testing.T) { { name: "handshake", server: func(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, websocket.AcceptOptions{ + c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ Subprotocols: []string{"myproto"}, }) if err != nil { @@ -54,7 +54,7 @@ func TestHandshake(t *testing.T) { return nil }, client: func(ctx context.Context, u string) error { - c, resp, err := websocket.Dial(ctx, u, websocket.DialOptions{ + c, resp, err := websocket.Dial(ctx, u, &websocket.DialOptions{ Subprotocols: []string{"myproto"}, }) if err != nil { @@ -81,7 +81,7 @@ func TestHandshake(t *testing.T) { { name: "defaultSubprotocol", server: func(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, websocket.AcceptOptions{}) + c, err := websocket.Accept(w, r, nil) if err != nil { return err } @@ -93,7 +93,7 @@ func TestHandshake(t *testing.T) { return nil }, client: func(ctx context.Context, u string) error { - c, _, err := websocket.Dial(ctx, u, websocket.DialOptions{ + c, _, err := websocket.Dial(ctx, u, &websocket.DialOptions{ Subprotocols: []string{"meow"}, }) if err != nil { @@ -110,7 +110,7 @@ func TestHandshake(t *testing.T) { { name: "subprotocol", server: func(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, websocket.AcceptOptions{ + c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ Subprotocols: []string{"echo", "lar"}, }) if err != nil { @@ -124,7 +124,7 @@ func TestHandshake(t *testing.T) { return nil }, client: func(ctx context.Context, u string) error { - c, _, err := websocket.Dial(ctx, u, websocket.DialOptions{ + c, _, err := websocket.Dial(ctx, u, &websocket.DialOptions{ Subprotocols: []string{"poof", "echo"}, }) if err != nil { @@ -141,7 +141,7 @@ func TestHandshake(t *testing.T) { { name: "badOrigin", server: func(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, websocket.AcceptOptions{}) + c, err := websocket.Accept(w, r, nil) if err == nil { c.Close(websocket.StatusInternalError, "") return xerrors.New("expected error regarding bad origin") @@ -151,7 +151,7 @@ func TestHandshake(t *testing.T) { client: func(ctx context.Context, u string) error { h := http.Header{} h.Set("Origin", "http://unauthorized.com") - c, _, err := websocket.Dial(ctx, u, websocket.DialOptions{ + c, _, err := websocket.Dial(ctx, u, &websocket.DialOptions{ HTTPHeader: h, }) if err == nil { @@ -164,7 +164,7 @@ func TestHandshake(t *testing.T) { { name: "acceptSecureOrigin", server: func(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, websocket.AcceptOptions{}) + c, err := websocket.Accept(w, r, nil) if err != nil { return err } @@ -174,7 +174,7 @@ func TestHandshake(t *testing.T) { client: func(ctx context.Context, u string) error { h := http.Header{} h.Set("Origin", u) - c, _, err := websocket.Dial(ctx, u, websocket.DialOptions{ + c, _, err := websocket.Dial(ctx, u, &websocket.DialOptions{ HTTPHeader: h, }) if err != nil { @@ -187,7 +187,7 @@ func TestHandshake(t *testing.T) { { name: "acceptInsecureOrigin", server: func(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, websocket.AcceptOptions{ + c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ InsecureSkipVerify: true, }) if err != nil { @@ -199,7 +199,7 @@ func TestHandshake(t *testing.T) { client: func(ctx context.Context, u string) error { h := http.Header{} h.Set("Origin", "https://example.com") - c, _, err := websocket.Dial(ctx, u, websocket.DialOptions{ + c, _, err := websocket.Dial(ctx, u, &websocket.DialOptions{ HTTPHeader: h, }) if err != nil { @@ -219,7 +219,7 @@ func TestHandshake(t *testing.T) { if cookie.Value != "myvalue" { return xerrors.Errorf("expected %q but got %q", "myvalue", cookie.Value) } - c, err := websocket.Accept(w, r, websocket.AcceptOptions{}) + c, err := websocket.Accept(w, r, nil) if err != nil { return err } @@ -245,7 +245,7 @@ func TestHandshake(t *testing.T) { hc := &http.Client{ Jar: jar, } - c, _, err := websocket.Dial(ctx, u, websocket.DialOptions{ + c, _, err := websocket.Dial(ctx, u, &websocket.DialOptions{ HTTPClient: hc, }) if err != nil { @@ -801,7 +801,7 @@ func TestConn(t *testing.T) { }, client: func(ctx context.Context, c *websocket.Conn) error { _, _, err := c.Read(ctx) - if err == nil || strings.Contains(err.Error(), "opcode") { + if err == nil || !strings.Contains(err.Error(), "opcode") { return xerrors.Errorf("expected error that contains opcode: %+v", err) } return nil @@ -839,7 +839,7 @@ func TestConn(t *testing.T) { tls := rand.Intn(2) == 1 s, closeFn := testServer(t, func(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, websocket.AcceptOptions{}) + c, err := websocket.Accept(w, r, nil) if err != nil { return err } @@ -854,7 +854,7 @@ func TestConn(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - opts := websocket.DialOptions{} + opts := &websocket.DialOptions{} if tls { opts.HTTPClient = s.Client() } @@ -920,7 +920,7 @@ func TestAutobahnServer(t *testing.T) { } s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c, err := websocket.Accept(w, r, websocket.AcceptOptions{ + c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ Subprotocols: []string{"echo"}, }) if err != nil { @@ -1120,7 +1120,7 @@ func TestAutobahnClient(t *testing.T) { var cases int func() { - c, _, err := websocket.Dial(ctx, wsServerURL+"/getCaseCount", websocket.DialOptions{}) + c, _, err := websocket.Dial(ctx, wsServerURL+"/getCaseCount", nil) if err != nil { t.Fatal(err) } @@ -1147,7 +1147,7 @@ func TestAutobahnClient(t *testing.T) { ctx, cancel := context.WithTimeout(ctx, time.Second*45) defer cancel() - c, _, err := websocket.Dial(ctx, fmt.Sprintf(wsServerURL+"/runCase?case=%v&agent=main", i), websocket.DialOptions{}) + c, _, err := websocket.Dial(ctx, fmt.Sprintf(wsServerURL+"/runCase?case=%v&agent=main", i), nil) if err != nil { t.Fatal(err) } @@ -1155,7 +1155,7 @@ func TestAutobahnClient(t *testing.T) { }() } - c, _, err := websocket.Dial(ctx, fmt.Sprintf(wsServerURL+"/updateReports?agent=main"), websocket.DialOptions{}) + c, _, err := websocket.Dial(ctx, fmt.Sprintf(wsServerURL+"/updateReports?agent=main"), nil) if err != nil { t.Fatal(err) } @@ -1207,7 +1207,7 @@ func checkWSTestIndex(t *testing.T, path string) { func benchConn(b *testing.B, echo, stream bool, size int) { s, closeFn := testServer(b, func(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, websocket.AcceptOptions{}) + c, err := websocket.Accept(w, r, nil) if err != nil { return err } @@ -1225,7 +1225,7 @@ func benchConn(b *testing.B, echo, stream bool, size int) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) defer cancel() - c, _, err := websocket.Dial(ctx, wsURL, websocket.DialOptions{}) + c, _, err := websocket.Dial(ctx, wsURL, nil) if err != nil { b.Fatal(err) } From de687ea0f90d0873473092dde1ed0ae1f6b9424c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 30 Aug 2019 13:34:36 -0500 Subject: [PATCH 088/519] More test coverage and updated CONTRIBUTING.md --- ci/image/Dockerfile | 5 +- docs/CONTRIBUTING.md | 19 ++++-- export_test.go | 7 ++ websocket_test.go | 154 +++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 171 insertions(+), 14 deletions(-) diff --git a/ci/image/Dockerfile b/ci/image/Dockerfile index d435e949..4477d646 100644 --- a/ci/image/Dockerfile +++ b/ci/image/Dockerfile @@ -6,8 +6,7 @@ ENV GOFLAGS="-mod=readonly" ENV PAGER=cat RUN apt-get update && \ - apt-get install -y shellcheck python-pip npm && \ - pip2 install autobahntestsuite && \ + apt-get install -y shellcheck npm && \ npm install -g prettier -RUN git config --global color.ui always \ No newline at end of file +RUN git config --global color.ui always diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index a0c97261..f003e743 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -34,16 +34,21 @@ browse coverage. You can run CI locally. The various steps are located in `ci/*.sh`. -1. `ci/fmt.sh` requires node (specifically prettier). -1. `ci/lint.sh` requires [shellcheck](https://github.com/koalaman/shellcheck#installing). -1. `ci/test.sh` requires the [Autobahn Test suite pip package](https://github.com/crossbario/autobahn-testsuite). -1. `ci/run.sh` runs the above scripts in order. +1. `ci/fmt.sh` which requires node (specifically prettier). +1. `ci/lint.sh` which requires [shellcheck](https://github.com/koalaman/shellcheck#installing). +1. `ci/test.sh` +1. `ci/run.sh` which runs the above scripts in order. For coverage details locally, please see `ci/out/coverage.html` after running `ci/test.sh`. See [ci/image/Dockerfile](ci/image/Dockerfile) for the installation of the CI dependencies on Ubuntu. -You can also run tests normally with `go test` once you have the -[Autobahn Test suite pip package](https://github.com/crossbario/autobahn-testsuite) -installed. `ci/test.sh` just passes a default set of flags to `go test` to collect coverage, +You can also run tests normally with `go test`. +`ci/test.sh` just passes a default set of flags to `go test` to collect coverage, enable the race detector, run benchmarks and also prettifies the output. + +If you pass flags to `ci/test.sh`, it will pass those flags directly to `go test` but will also +collect coverage for you. This is nice for when you don't want to wait for benchmarks +or the race detector but want to have coverage. + +Coverage percentage from codecov and the CI scripts will be different because they are calculated differently. diff --git a/export_test.go b/export_test.go index ab766f14..9c65360a 100644 --- a/export_test.go +++ b/export_test.go @@ -8,6 +8,13 @@ type Addr = websocketAddr type Header = header +const OPClose = opClose +const OPPing = opPing + func (c *Conn) WriteFrame(ctx context.Context, fin bool, opcode opcode, p []byte) (int, error) { return c.writeFrame(ctx, fin, opcode, p) } + +func (c *Conn) Flush() error { + return c.bw.Flush() +} diff --git a/websocket_test.go b/websocket_test.go index 1f1b5245..73020f5e 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -4,8 +4,6 @@ import ( "context" "encoding/json" "fmt" - "github.com/golang/protobuf/proto" - "github.com/golang/protobuf/ptypes/timestamp" "io" "io/ioutil" "math/rand" @@ -23,8 +21,10 @@ import ( "testing" "time" + "github.com/golang/protobuf/proto" "github.com/golang/protobuf/ptypes" "github.com/golang/protobuf/ptypes/duration" + "github.com/golang/protobuf/ptypes/timestamp" "github.com/google/go-cmp/cmp" "golang.org/x/xerrors" @@ -592,7 +592,7 @@ func TestConn(t *testing.T) { return nil }, client: func(ctx context.Context, c *websocket.Conn) error { - go c.CloseRead(ctx) + c.CloseRead(ctx) err := c.Write(ctx, websocket.MessageBinary, []byte(strings.Repeat("x", 32769))) if err != nil { @@ -775,7 +775,7 @@ func TestConn(t *testing.T) { } <-ctx.Done() _, err = r.Read(make([]byte, 1)) - if !xerrors.Is(err, context.DeadlineExceeded){ + if !xerrors.Is(err, context.DeadlineExceeded) { return xerrors.Errorf("expected deadline exceeded error: %+v", err) } return nil @@ -829,6 +829,152 @@ func TestConn(t *testing.T) { return nil }, }, + { + name: "largeControlFrame", + server: func(ctx context.Context, c *websocket.Conn) error { + _, err := c.WriteFrame(ctx, true, websocket.OPClose, []byte(strings.Repeat("x", 4096))) + if err != nil { + return err + } + _, _, err = c.Read(ctx) + cerr := &websocket.CloseError{} + if !xerrors.As(err, cerr) || cerr.Code != websocket.StatusProtocolError { + return xerrors.Errorf("expected close error with StatusProtocolError: %+v", err) + } + return nil + }, + client: func(ctx context.Context, c *websocket.Conn) error { + _, _, err := c.Read(ctx) + if err == nil || !strings.Contains(err.Error(), "too large") { + return xerrors.Errorf("expected error that contains too large: %+v", err) + } + return nil + }, + }, + { + name: "fragmentedControlFrame", + server: func(ctx context.Context, c *websocket.Conn) error { + _, err := c.WriteFrame(ctx, false, websocket.OPPing, []byte(strings.Repeat("x", 32))) + if err != nil { + return err + } + err = c.Flush() + if err != nil { + return err + } + _, _, err = c.Read(ctx) + cerr := &websocket.CloseError{} + if !xerrors.As(err, cerr) || cerr.Code != websocket.StatusProtocolError { + return xerrors.Errorf("expected close error with StatusProtocolError: %+v", err) + } + return nil + }, + client: func(ctx context.Context, c *websocket.Conn) error { + _, _, err := c.Read(ctx) + if err == nil || !strings.Contains(err.Error(), "fragmented") { + return xerrors.Errorf("expected error that contains fragmented: %+v", err) + } + return nil + }, + }, + { + name: "invalidClosePayload", + server: func(ctx context.Context, c *websocket.Conn) error { + _, err := c.WriteFrame(ctx, true, websocket.OPClose, []byte{0x17, 0x70}) + if err != nil { + return err + } + _, _, err = c.Read(ctx) + cerr := &websocket.CloseError{} + if !xerrors.As(err, cerr) || cerr.Code != websocket.StatusProtocolError { + return xerrors.Errorf("expected close error with StatusProtocolError: %+v", err) + } + return nil + }, + client: func(ctx context.Context, c *websocket.Conn) error { + _, _, err := c.Read(ctx) + if err == nil || !strings.Contains(err.Error(), "invalid status code") { + return xerrors.Errorf("expected error that contains invalid status code: %+v", err) + } + return nil + }, + }, + { + name: "doubleReader", + server: func(ctx context.Context, c *websocket.Conn) error { + _, r, err := c.Reader(ctx) + if err != nil { + return err + } + p := make([]byte, 10) + _, err = io.ReadFull(r, p) + if err != nil { + return err + } + _, _, err = c.Reader(ctx) + if err == nil { + return xerrors.Errorf("expected non nil error: %v", err) + } + return nil + }, + client: func(ctx context.Context, c *websocket.Conn) error { + err := c.Write(ctx, websocket.MessageBinary, []byte(strings.Repeat("x", 11))) + if err != nil { + return err + } + _, _, err = c.Read(ctx) + if err == nil { + return xerrors.Errorf("expected non nil error: %v", err) + } + return nil + }, + }, + { + name: "doubleFragmentedReader", + server: func(ctx context.Context, c *websocket.Conn) error { + _, r, err := c.Reader(ctx) + if err != nil { + return err + } + p := make([]byte, 10) + _, err = io.ReadFull(r, p) + if err != nil { + return err + } + _, _, err = c.Reader(ctx) + if err == nil { + return xerrors.Errorf("expected non nil error: %v", err) + } + return nil + }, + client: func(ctx context.Context, c *websocket.Conn) error { + w, err := c.Writer(ctx, websocket.MessageBinary) + if err != nil { + return err + } + _, err = w.Write([]byte(strings.Repeat("x", 10))) + if err != nil { + return xerrors.Errorf("expected non nil error") + } + err = c.Flush() + if err != nil { + return xerrors.Errorf("failed to flush: %w", err) + } + _, err = w.Write([]byte(strings.Repeat("x", 10))) + if err != nil { + return xerrors.Errorf("expected non nil error") + } + err = c.Flush() + if err != nil { + return xerrors.Errorf("failed to flush: %w", err) + } + _, _, err = c.Read(ctx) + if err == nil { + return xerrors.Errorf("expected non nil error: %v", err) + } + return nil + }, + }, } for _, tc := range testCases { tc := tc From 8cfcf43b9b111d7fd17ebf128a2468015f98ad74 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 30 Aug 2019 19:25:46 -0500 Subject: [PATCH 089/519] Add more tests and prepare for a rewrite of the tests tomorrow --- .circleci/config.yml | 6 +- export_test.go | 11 ++- websocket_test.go | 225 +++++++++++++++++++++++++++++++++++-------- 3 files changed, 197 insertions(+), 45 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 65b17aa0..196ec671 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2 jobs: fmt: docker: - - image: nhooyr/websocket-ci + - image: nhooyr/websocket-ci@sha256:371ca985ce2548840aeb0f8434a551708cdfe0628be722c361958e65cdded945 steps: - checkout - restore_cache: @@ -19,7 +19,7 @@ jobs: lint: docker: - - image: nhooyr/websocket-ci + - image: nhooyr/websocket-ci@sha256:371ca985ce2548840aeb0f8434a551708cdfe0628be722c361958e65cdded945 steps: - checkout - restore_cache: @@ -36,7 +36,7 @@ jobs: test: docker: - - image: nhooyr/websocket-ci + - image: nhooyr/websocket-ci@sha256:371ca985ce2548840aeb0f8434a551708cdfe0628be722c361958e65cdded945 steps: - checkout - restore_cache: diff --git a/export_test.go b/export_test.go index 9c65360a..fc885bff 100644 --- a/export_test.go +++ b/export_test.go @@ -6,15 +6,22 @@ import ( type Addr = websocketAddr -type Header = header - const OPClose = opClose +const OPBinary = opBinary const OPPing = opPing +const OPContinuation = opContinuation func (c *Conn) WriteFrame(ctx context.Context, fin bool, opcode opcode, p []byte) (int, error) { return c.writeFrame(ctx, fin, opcode, p) } +func (c *Conn) WriteHalfFrame(ctx context.Context) (int, error) { + return c.realWriteFrame(ctx, header{ + opcode: opBinary, + payloadLength: 5, + }, make([]byte, 10)) +} + func (c *Conn) Flush() error { return c.bw.Flush() } diff --git a/websocket_test.go b/websocket_test.go index 73020f5e..1963ce70 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -146,6 +146,9 @@ func TestHandshake(t *testing.T) { c.Close(websocket.StatusInternalError, "") return xerrors.New("expected error regarding bad origin") } + if !strings.Contains(err.Error(), "not authorized") { + return xerrors.Errorf("expected error regarding bad origin: %+v", err) + } return nil }, client: func(ctx context.Context, u string) error { @@ -158,6 +161,9 @@ func TestHandshake(t *testing.T) { c.Close(websocket.StatusInternalError, "") return xerrors.New("expected handshake failure") } + if !strings.Contains(err.Error(), "403") { + return xerrors.Errorf("expected handshake failure: %+v", err) + } return nil }, }, @@ -390,8 +396,8 @@ func TestConn(t *testing.T) { nc.SetDeadline(time.Now().Add(time.Second * 15)) _, err := nc.Read(make([]byte, 1)) - if err == nil { - return xerrors.Errorf("expected error") + if err == nil || !strings.Contains(err.Error(), "unexpected frame type read") { + return xerrors.Errorf("expected error: %+v", err) } return nil @@ -426,7 +432,7 @@ func TestConn(t *testing.T) { } _, err = nc.Write([]byte{0xff}) - if err == nil { + if err == nil || !strings.Contains(err.Error(), "websocket closed") { return xerrors.Errorf("expected writes to fail after reading a close frame: %v", err) } @@ -586,8 +592,8 @@ func TestConn(t *testing.T) { name: "readLimit", server: func(ctx context.Context, c *websocket.Conn) error { _, _, err := c.Read(ctx) - if err == nil { - return xerrors.Errorf("expected error but got nil") + if err == nil || !strings.Contains(err.Error(), "read limited at") { + return xerrors.Errorf("expected error but got nil: %+v", err) } return nil }, @@ -614,7 +620,7 @@ func TestConn(t *testing.T) { server: func(ctx context.Context, c *websocket.Conn) error { var v interface{} err := wsjson.Read(ctx, c, &v) - if err == nil { + if err == nil || !strings.Contains(err.Error(), "unexpected frame type") { return xerrors.Errorf("expected error: %v", err) } return nil @@ -628,7 +634,7 @@ func TestConn(t *testing.T) { server: func(ctx context.Context, c *websocket.Conn) error { var v interface{} err := wsjson.Read(ctx, c, &v) - if err == nil { + if err == nil || !strings.Contains(err.Error(), "failed to unmarshal json") { return xerrors.Errorf("expected error: %v", err) } return nil @@ -641,7 +647,7 @@ func TestConn(t *testing.T) { name: "wsjson/badWrite", server: func(ctx context.Context, c *websocket.Conn) error { _, _, err := c.Read(ctx) - if err == nil { + if err == nil || !strings.Contains(err.Error(), "StatusInternalError") { return xerrors.Errorf("expected error: %v", err) } return nil @@ -659,7 +665,7 @@ func TestConn(t *testing.T) { server: func(ctx context.Context, c *websocket.Conn) error { var v proto.Message err := wspb.Read(ctx, c, v) - if err == nil { + if err == nil || !strings.Contains(err.Error(), "unexpected frame type") { return xerrors.Errorf("expected error: %v", err) } return nil @@ -673,7 +679,7 @@ func TestConn(t *testing.T) { server: func(ctx context.Context, c *websocket.Conn) error { var v timestamp.Timestamp err := wspb.Read(ctx, c, &v) - if err == nil { + if err == nil || !strings.Contains(err.Error(), "failed to unmarshal protobuf") { return xerrors.Errorf("expected error: %v", err) } return nil @@ -686,24 +692,7 @@ func TestConn(t *testing.T) { name: "wspb/badWrite", server: func(ctx context.Context, c *websocket.Conn) error { _, _, err := c.Read(ctx) - if err == nil { - return xerrors.Errorf("expected error: %v", err) - } - return nil - }, - client: func(ctx context.Context, c *websocket.Conn) error { - err := wspb.Write(ctx, c, nil) - if err == nil { - return xerrors.Errorf("expected error: %v", err) - } - return nil - }, - }, - { - name: "wspb/badWrite", - server: func(ctx context.Context, c *websocket.Conn) error { - _, _, err := c.Read(ctx) - if err == nil { + if err == nil || !strings.Contains(err.Error(), "StatusInternalError") { return xerrors.Errorf("expected error: %v", err) } return nil @@ -736,13 +725,13 @@ func TestConn(t *testing.T) { ctx, cancel := context.WithTimeout(ctx, time.Second) defer cancel() err := c.Ping(ctx) - if err == nil { + if err == nil || !xerrors.Is(err, context.DeadlineExceeded) { return xerrors.Errorf("expected nil error: %+v", err) } return nil }, client: func(ctx context.Context, c *websocket.Conn) error { - time.Sleep(time.Second) + c.Read(ctx) return nil }, }, @@ -769,19 +758,14 @@ func TestConn(t *testing.T) { server: func(ctx context.Context, c *websocket.Conn) error { ctx, cancel := context.WithTimeout(ctx, time.Second) defer cancel() - _, r, err := c.Reader(ctx) - if err != nil { - return err - } - <-ctx.Done() - _, err = r.Read(make([]byte, 1)) + _, _, err := c.Read(ctx) if !xerrors.Is(err, context.DeadlineExceeded) { return xerrors.Errorf("expected deadline exceeded error: %+v", err) } return nil }, client: func(ctx context.Context, c *websocket.Conn) error { - time.Sleep(time.Second) + c.Read(ctx) return nil }, }, @@ -912,7 +896,7 @@ func TestConn(t *testing.T) { return err } _, _, err = c.Reader(ctx) - if err == nil { + if err == nil || !strings.Contains(err.Error(), "previous message not read to completion") { return xerrors.Errorf("expected non nil error: %v", err) } return nil @@ -942,11 +926,57 @@ func TestConn(t *testing.T) { return err } _, _, err = c.Reader(ctx) + if err == nil || !strings.Contains(err.Error(), "previous message not read to completion") { + return xerrors.Errorf("expected non nil error: %v", err) + } + return nil + }, + client: func(ctx context.Context, c *websocket.Conn) error { + w, err := c.Writer(ctx, websocket.MessageBinary) + if err != nil { + return err + } + _, err = w.Write([]byte(strings.Repeat("x", 10))) + if err != nil { + return xerrors.Errorf("expected non nil error") + } + err = c.Flush() + if err != nil { + return xerrors.Errorf("failed to flush: %w", err) + } + _, err = w.Write([]byte(strings.Repeat("x", 10))) + if err != nil { + return xerrors.Errorf("expected non nil error") + } + err = c.Flush() + if err != nil { + return xerrors.Errorf("failed to flush: %w", err) + } + _, _, err = c.Read(ctx) if err == nil { return xerrors.Errorf("expected non nil error: %v", err) } return nil }, + }, + { + name: "newMessageInFragmentedMessage", + server: func(ctx context.Context, c *websocket.Conn) error { + _, r, err := c.Reader(ctx) + if err != nil { + return err + } + p := make([]byte, 10) + _, err = io.ReadFull(r, p) + if err != nil { + return err + } + _, _, err = c.Reader(ctx) + if err == nil || !strings.Contains(err.Error(), "received new data message without finishing") { + return xerrors.Errorf("expected non nil error: %v", err) + } + return nil + }, client: func(ctx context.Context, c *websocket.Conn) error { w, err := c.Writer(ctx, websocket.MessageBinary) if err != nil { @@ -960,6 +990,83 @@ func TestConn(t *testing.T) { if err != nil { return xerrors.Errorf("failed to flush: %w", err) } + _, err = c.WriteFrame(ctx, true, websocket.OPBinary, []byte(strings.Repeat("x", 10))) + if err != nil { + return xerrors.Errorf("expected non nil error") + } + _, _, err = c.Read(ctx) + if err == nil || !strings.Contains(err.Error(), "received new data message without finishing") { + return xerrors.Errorf("expected non nil error: %v", err) + } + return nil + }, + }, + { + name: "continuationFrameWithoutDataFrame", + server: func(ctx context.Context, c *websocket.Conn) error { + _, _, err := c.Reader(ctx) + if err == nil || !strings.Contains(err.Error(), "received continuation frame not after data") { + return xerrors.Errorf("expected non nil error: %v", err) + } + return nil + }, + client: func(ctx context.Context, c *websocket.Conn) error { + _, err := c.WriteFrame(ctx, false, websocket.OPContinuation, []byte(strings.Repeat("x", 10))) + if err != nil { + return xerrors.Errorf("expected non nil error") + } + return nil + }, + }, + { + name: "readBeforeEOF", + server: func(ctx context.Context, c *websocket.Conn) error { + _, r, err := c.Reader(ctx) + if err != nil { + return err + } + var v interface{} + d := json.NewDecoder(r) + err = d.Decode(&v) + if err != nil { + return err + } + _, b, err := c.Read(ctx) + if err != nil { + return err + } + if string(b) != "hi" { + return xerrors.Errorf("expected hi but got %q", string(b)) + } + return nil + }, + client: func(ctx context.Context, c *websocket.Conn) error { + err := wsjson.Write(ctx, c, "hi") + if err != nil { + return err + } + return c.Write(ctx, websocket.MessageBinary, []byte("hi")) + }, + }, + { + name: "newMessageInFragmentedMessage2", + server: func(ctx context.Context, c *websocket.Conn) error { + _, r, err := c.Reader(ctx) + if err != nil { + return err + } + p := make([]byte, 11) + _, err = io.ReadFull(r, p) + if err == nil || !strings.Contains(err.Error(), "received new data message without finishing") { + return xerrors.Errorf("expected non nil error: %v", err) + } + return nil + }, + client: func(ctx context.Context, c *websocket.Conn) error { + w, err := c.Writer(ctx, websocket.MessageBinary) + if err != nil { + return err + } _, err = w.Write([]byte(strings.Repeat("x", 10))) if err != nil { return xerrors.Errorf("expected non nil error") @@ -968,6 +1075,10 @@ func TestConn(t *testing.T) { if err != nil { return xerrors.Errorf("failed to flush: %w", err) } + _, err = c.WriteFrame(ctx, true, websocket.OPBinary, []byte(strings.Repeat("x", 10))) + if err != nil { + return xerrors.Errorf("expected non nil error") + } _, _, err = c.Read(ctx) if err == nil { return xerrors.Errorf("expected non nil error: %v", err) @@ -975,6 +1086,41 @@ func TestConn(t *testing.T) { return nil }, }, + { + name: "doubleRead", + server: func(ctx context.Context, c *websocket.Conn) error { + _, r, err := c.Reader(ctx) + if err != nil { + return err + } + _, err = ioutil.ReadAll(r) + if err != nil { + return err + } + _, err = r.Read(make([]byte, 1)) + if err == nil || !strings.Contains(err.Error(), "cannot use EOFed reader") { + return xerrors.Errorf("expected non nil error: %+v", err) + } + return nil + }, + client: func(ctx context.Context, c *websocket.Conn) error { + return c.Write(ctx, websocket.MessageBinary, []byte("hi")) + }, + }, + { + name: "eofInPayload", + server: func(ctx context.Context, c *websocket.Conn) error { + _, _, err := c.Read(ctx) + if err == nil || !strings.Contains(err.Error(), "failed to read frame payload") { + return xerrors.Errorf("expected failed to read frame payload: %v", err) + } + return nil + }, + client: func(ctx context.Context, c *websocket.Conn) error { + _, err := c.WriteHalfFrame(ctx) + return err + }, + }, } for _, tc := range testCases { tc := tc @@ -990,8 +1136,7 @@ func TestConn(t *testing.T) { return err } defer c.Close(websocket.StatusInternalError, "") - tc.server(r.Context(), c) - return nil + return tc.server(r.Context(), c) }, tls) defer closeFn() From 5a463b7a0901d0c9d5fb464d15e9246705a752ae Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 30 Aug 2019 21:28:19 -0500 Subject: [PATCH 090/519] Add WASM Compilation to the README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index d53046c8..f7c9fe4f 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,7 @@ go get nhooyr.io/websocket@v1.4.0 ## Roadmap - [ ] WebSockets over HTTP/2 [#4](https://github.com/nhooyr/websocket/issues/4) +- [ ] WASM Compilation [#121](https://github.com/nhooyr/websocket/issues/121) ## Examples From 412f574b00f88a4582db6dd6be93312d2ec1b0eb Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 30 Aug 2019 23:45:01 -0500 Subject: [PATCH 091/519] Simplify tests Many new helpers. Closes #124 --- cmp_test.go | 53 ++++ go.mod | 2 + go.sum | 4 + websocket.go | 4 +- websocket_test.go | 701 +++++++++++++++++----------------------------- 5 files changed, 315 insertions(+), 449 deletions(-) create mode 100644 cmp_test.go diff --git a/cmp_test.go b/cmp_test.go new file mode 100644 index 00000000..ad4cd75a --- /dev/null +++ b/cmp_test.go @@ -0,0 +1,53 @@ +package websocket_test + +import ( + "reflect" + + "github.com/google/go-cmp/cmp" +) + +// https://github.com/google/go-cmp/issues/40#issuecomment-328615283 +func cmpDiff(exp, act interface{}) string { + return cmp.Diff(exp, act, deepAllowUnexported(exp, act)) +} + +func deepAllowUnexported(vs ...interface{}) cmp.Option { + m := make(map[reflect.Type]struct{}) + for _, v := range vs { + structTypes(reflect.ValueOf(v), m) + } + var typs []interface{} + for t := range m { + typs = append(typs, reflect.New(t).Elem().Interface()) + } + return cmp.AllowUnexported(typs...) +} + +func structTypes(v reflect.Value, m map[reflect.Type]struct{}) { + if !v.IsValid() { + return + } + switch v.Kind() { + case reflect.Ptr: + if !v.IsNil() { + structTypes(v.Elem(), m) + } + case reflect.Interface: + if !v.IsNil() { + structTypes(v.Elem(), m) + } + case reflect.Slice, reflect.Array: + for i := 0; i < v.Len(); i++ { + structTypes(v.Index(i), m) + } + case reflect.Map: + for _, k := range v.MapKeys() { + structTypes(v.MapIndex(k), m) + } + case reflect.Struct: + m[v.Type()] = struct{}{} + for i := 0; i < v.NumField(); i++ { + structTypes(v.Field(i), m) + } + } +} diff --git a/go.mod b/go.mod index 35d500dd..c9cc6fc4 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,8 @@ require ( github.com/google/go-cmp v0.2.0 github.com/kr/pretty v0.1.0 // indirect go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16 + go.uber.org/atomic v1.4.0 // indirect + go.uber.org/multierr v1.1.0 golang.org/x/lint v0.0.0-20190409202823-959b441ac422 golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6 golang.org/x/text v0.3.2 // indirect diff --git a/go.sum b/go.sum index b9e3737c..187a2285 100644 --- a/go.sum +++ b/go.sum @@ -40,6 +40,10 @@ github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16 h1:3gGa1bM0nG7Ruhu5b7wKnoOOwAD/fJ8iyyAcpOzDG3A= go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw= +go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/websocket.go b/websocket.go index 833c1209..6f28a4bf 100644 --- a/websocket.go +++ b/websocket.go @@ -166,9 +166,9 @@ func (c *Conn) timeoutLoop() { case readCtx = <-c.setReadTimeout: case <-readCtx.Done(): - c.close(xerrors.Errorf("data read timed out: %w", readCtx.Err())) + c.close(xerrors.Errorf("read timed out: %w", readCtx.Err())) case <-writeCtx.Done(): - c.close(xerrors.Errorf("data write timed out: %w", writeCtx.Err())) + c.close(xerrors.Errorf("write timed out: %w", writeCtx.Err())) } } } diff --git a/websocket_test.go b/websocket_test.go index 1963ce70..e6529f3b 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -23,9 +23,8 @@ import ( "github.com/golang/protobuf/proto" "github.com/golang/protobuf/ptypes" - "github.com/golang/protobuf/ptypes/duration" "github.com/golang/protobuf/ptypes/timestamp" - "github.com/google/go-cmp/cmp" + "go.uber.org/multierr" "golang.org/x/xerrors" "nhooyr.io/websocket" @@ -41,103 +40,6 @@ func TestHandshake(t *testing.T) { client func(ctx context.Context, url string) error server func(w http.ResponseWriter, r *http.Request) error }{ - { - name: "handshake", - server: func(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ - Subprotocols: []string{"myproto"}, - }) - if err != nil { - return err - } - defer c.Close(websocket.StatusInternalError, "") - return nil - }, - client: func(ctx context.Context, u string) error { - c, resp, err := websocket.Dial(ctx, u, &websocket.DialOptions{ - Subprotocols: []string{"myproto"}, - }) - if err != nil { - return err - } - defer c.Close(websocket.StatusInternalError, "") - - checkHeader := func(h, exp string) { - t.Helper() - value := resp.Header.Get(h) - if exp != value { - t.Errorf("expected different value for header %v: %v", h, cmp.Diff(exp, value)) - } - } - - checkHeader("Connection", "Upgrade") - checkHeader("Upgrade", "websocket") - checkHeader("Sec-WebSocket-Protocol", "myproto") - - c.Close(websocket.StatusNormalClosure, "") - return nil - }, - }, - { - name: "defaultSubprotocol", - server: func(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, nil) - if err != nil { - return err - } - defer c.Close(websocket.StatusInternalError, "") - - if c.Subprotocol() != "" { - return xerrors.Errorf("unexpected subprotocol: %v", c.Subprotocol()) - } - return nil - }, - client: func(ctx context.Context, u string) error { - c, _, err := websocket.Dial(ctx, u, &websocket.DialOptions{ - Subprotocols: []string{"meow"}, - }) - if err != nil { - return err - } - defer c.Close(websocket.StatusInternalError, "") - - if c.Subprotocol() != "" { - return xerrors.Errorf("unexpected subprotocol: %v", c.Subprotocol()) - } - return nil - }, - }, - { - name: "subprotocol", - server: func(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ - Subprotocols: []string{"echo", "lar"}, - }) - if err != nil { - return err - } - defer c.Close(websocket.StatusInternalError, "") - - if c.Subprotocol() != "echo" { - return xerrors.Errorf("unexpected subprotocol: %q", c.Subprotocol()) - } - return nil - }, - client: func(ctx context.Context, u string) error { - c, _, err := websocket.Dial(ctx, u, &websocket.DialOptions{ - Subprotocols: []string{"poof", "echo"}, - }) - if err != nil { - return err - } - defer c.Close(websocket.StatusInternalError, "") - - if c.Subprotocol() != "echo" { - return xerrors.Errorf("unexpected subprotocol: %q", c.Subprotocol()) - } - return nil - }, - }, { name: "badOrigin", server: func(w http.ResponseWriter, r *http.Request) error { @@ -174,7 +76,7 @@ func TestHandshake(t *testing.T) { if err != nil { return err } - defer c.Close(websocket.StatusInternalError, "") + c.Close(websocket.StatusNormalClosure, "") return nil }, client: func(ctx context.Context, u string) error { @@ -186,7 +88,7 @@ func TestHandshake(t *testing.T) { if err != nil { return err } - defer c.Close(websocket.StatusInternalError, "") + c.Close(websocket.StatusNormalClosure, "") return nil }, }, @@ -199,7 +101,7 @@ func TestHandshake(t *testing.T) { if err != nil { return err } - defer c.Close(websocket.StatusInternalError, "") + c.Close(websocket.StatusNormalClosure, "") return nil }, client: func(ctx context.Context, u string) error { @@ -211,7 +113,7 @@ func TestHandshake(t *testing.T) { if err != nil { return err } - defer c.Close(websocket.StatusInternalError, "") + c.Close(websocket.StatusNormalClosure, "") return nil }, }, @@ -229,7 +131,7 @@ func TestHandshake(t *testing.T) { if err != nil { return err } - c.Close(websocket.StatusInternalError, "") + c.Close(websocket.StatusNormalClosure, "") return nil }, client: func(ctx context.Context, u string) error { @@ -257,7 +159,7 @@ func TestHandshake(t *testing.T) { if err != nil { return err } - c.Close(websocket.StatusInternalError, "") + c.Close(websocket.StatusNormalClosure, "") return nil }, }, @@ -288,33 +190,76 @@ func TestConn(t *testing.T) { t.Parallel() testCases := []struct { - name string - client func(ctx context.Context, c *websocket.Conn) error - server func(ctx context.Context, c *websocket.Conn) error + name string + + acceptOpts *websocket.AcceptOptions + server func(ctx context.Context, c *websocket.Conn) error + + dialOpts *websocket.DialOptions + response func(resp *http.Response) error + client func(ctx context.Context, c *websocket.Conn) error }{ + { + name: "handshake", + acceptOpts: &websocket.AcceptOptions{ + Subprotocols: []string{"myproto"}, + }, + dialOpts: &websocket.DialOptions{ + Subprotocols: []string{"myproto"}, + }, + response: func(resp *http.Response) error { + headers := map[string]string{ + "Connection": "Upgrade", + "Upgrade": "websocket", + "Sec-WebSocket-Protocol": "myproto", + } + for h, exp := range headers { + value := resp.Header.Get(h) + err := assertEqualf(exp, value, "unexpected value for header %v", h) + if err != nil { + return err + } + } + return nil + }, + }, + { + name: "handshake/defaultSubprotocol", + server: func(ctx context.Context, c *websocket.Conn) error { + return assertSubprotocol(c, "") + }, + client: func(ctx context.Context, c *websocket.Conn) error { + return assertSubprotocol(c, "") + }, + }, + { + name: "handshake/subprotocolPriority", + acceptOpts: &websocket.AcceptOptions{ + Subprotocols: []string{"echo", "lar"}, + }, + server: func(ctx context.Context, c *websocket.Conn) error { + return assertSubprotocol(c, "echo") + }, + dialOpts: &websocket.DialOptions{ + Subprotocols: []string{"poof", "echo"}, + }, + client: func(ctx context.Context, c *websocket.Conn) error { + return assertSubprotocol(c, "echo") + }, + }, { name: "closeError", server: func(ctx context.Context, c *websocket.Conn) error { return wsjson.Write(ctx, c, "hello") }, client: func(ctx context.Context, c *websocket.Conn) error { - var m string - err := wsjson.Read(ctx, c, &m) + err := assertJSONRead(ctx, c, "hello") if err != nil { return err } - if m != "hello" { - return xerrors.Errorf("recieved unexpected msg but expected hello: %+v", m) - } - _, _, err = c.Reader(ctx) - var cerr websocket.CloseError - if !xerrors.As(err, &cerr) || cerr.Code != websocket.StatusInternalError { - return xerrors.Errorf("unexpected error: %+v", err) - } - - return nil + return assertCloseStatus(err, websocket.StatusInternalError) }, }, { @@ -327,11 +272,13 @@ func TestConn(t *testing.T) { time.Sleep(1) nc.SetWriteDeadline(time.Now().Add(time.Second * 15)) - if nc.LocalAddr() != (websocket.Addr{}) { - return xerrors.Errorf("net conn local address is not equal to websocket.Addr") + err := assertEqualf(websocket.Addr{}, nc.LocalAddr(), "net conn local address is not equal to websocket.Addr") + if err != nil { + return err } - if nc.RemoteAddr() != (websocket.Addr{}) { - return xerrors.Errorf("net conn remote address is not equal to websocket.Addr") + err = assertEqualf(websocket.Addr{}, nc.RemoteAddr(), "net conn remote address is not equal to websocket.Addr") + if err != nil { + return err } for i := 0; i < 3; i++ { @@ -345,62 +292,38 @@ func TestConn(t *testing.T) { }, client: func(ctx context.Context, c *websocket.Conn) error { nc := websocket.NetConn(c, websocket.MessageBinary) - defer nc.Close() nc.SetReadDeadline(time.Time{}) time.Sleep(1) nc.SetReadDeadline(time.Now().Add(time.Second * 15)) - read := func() error { - p := make([]byte, len("hello")) - // We do not use io.ReadFull here as it masks EOFs. - // See https://github.com/nhooyr/websocket/issues/100#issuecomment-508148024 - _, err := nc.Read(p) - if err != nil { - return err - } - - if string(p) != "hello" { - return xerrors.Errorf("unexpected payload %q received", string(p)) - } - return nil - } - for i := 0; i < 3; i++ { - err := read() + err := assertNetConnRead(nc, "hello") if err != nil { return err } } // Ensure the close frame is converted to an EOF and multiple read's after all return EOF. - err := read() - if err != io.EOF { - return err - } - - err = read() - if err != io.EOF { + err2 := assertNetConnRead(nc, "hello") + err := assertEqualf(io.EOF, err2, "unexpected error") + if err != nil { return err } - return nil + err2 = assertNetConnRead(nc, "hello") + return assertEqualf(io.EOF, err2, "unexpected error") }, }, { name: "netConn/badReadMsgType", server: func(ctx context.Context, c *websocket.Conn) error { nc := websocket.NetConn(c, websocket.MessageBinary) - defer nc.Close() nc.SetDeadline(time.Now().Add(time.Second * 15)) _, err := nc.Read(make([]byte, 1)) - if err == nil || !strings.Contains(err.Error(), "unexpected frame type read") { - return xerrors.Errorf("expected error: %+v", err) - } - - return nil + return assertErrorContains(err, "unexpected frame type") }, client: func(ctx context.Context, c *websocket.Conn) error { err := wsjson.Write(ctx, c, "meow") @@ -409,12 +332,7 @@ func TestConn(t *testing.T) { } _, _, err = c.Read(ctx) - cerr := &websocket.CloseError{} - if !xerrors.As(err, cerr) || cerr.Code != websocket.StatusUnsupportedData { - return xerrors.Errorf("expected close error with code StatusUnsupportedData: %+v", err) - } - - return nil + return assertCloseStatus(err, websocket.StatusUnsupportedData) }, }, { @@ -425,205 +343,105 @@ func TestConn(t *testing.T) { nc.SetDeadline(time.Now().Add(time.Second * 15)) - _, err := nc.Read(make([]byte, 1)) - cerr := &websocket.CloseError{} - if !xerrors.As(err, cerr) || cerr.Code != websocket.StatusBadGateway { - return xerrors.Errorf("expected close error with code StatusBadGateway: %+v", err) - } - - _, err = nc.Write([]byte{0xff}) - if err == nil || !strings.Contains(err.Error(), "websocket closed") { - return xerrors.Errorf("expected writes to fail after reading a close frame: %v", err) + _, err2 := nc.Read(make([]byte, 1)) + err := assertCloseStatus(err2, websocket.StatusBadGateway) + if err != nil { + return err } - return nil + _, err2 = nc.Write([]byte{0xff}) + return assertErrorContains(err2, "websocket closed") }, client: func(ctx context.Context, c *websocket.Conn) error { return c.Close(websocket.StatusBadGateway, "") }, }, { - name: "jsonEcho", + name: "wsjson/echo", server: func(ctx context.Context, c *websocket.Conn) error { - write := func() error { - v := map[string]interface{}{ - "anmol": "wowow", - } - err := wsjson.Write(ctx, c, v) - return err - } - err := write() - if err != nil { - return err - } - err = write() - if err != nil { - return err - } - - c.Close(websocket.StatusNormalClosure, "") - return nil + return wsjson.Write(ctx, c, "meow") }, client: func(ctx context.Context, c *websocket.Conn) error { - read := func() error { - var v interface{} - err := wsjson.Read(ctx, c, &v) - if err != nil { - return err - } - - exp := map[string]interface{}{ - "anmol": "wowow", - } - if !reflect.DeepEqual(exp, v) { - return xerrors.Errorf("expected %v but got %v", exp, v) - } - return nil - } - err := read() - if err != nil { - return err - } - err = read() - if err != nil { - return err - } - - c.Close(websocket.StatusNormalClosure, "") - return nil + return assertJSONRead(ctx, c, "meow") }, }, { - name: "protobufEcho", + name: "protobuf/echo", server: func(ctx context.Context, c *websocket.Conn) error { - write := func() error { - err := wspb.Write(ctx, c, ptypes.DurationProto(100)) - return err - } - err := write() - if err != nil { - return err - } - - c.Close(websocket.StatusNormalClosure, "") - return nil + return wspb.Write(ctx, c, ptypes.DurationProto(100)) }, client: func(ctx context.Context, c *websocket.Conn) error { - read := func() error { - var v duration.Duration - err := wspb.Read(ctx, c, &v) - if err != nil { - return err - } - - d, err := ptypes.Duration(&v) - if err != nil { - return xerrors.Errorf("failed to convert duration.Duration to time.Duration: %w", err) - } - const exp = time.Duration(100) - if !reflect.DeepEqual(exp, d) { - return xerrors.Errorf("expected %v but got %v", exp, d) - } - return nil - } - err := read() - if err != nil { - return err - } - - c.Close(websocket.StatusNormalClosure, "") - return nil + return assertProtobufRead(ctx, c, ptypes.DurationProto(100)) }, }, { name: "ping", server: func(ctx context.Context, c *websocket.Conn) error { - errc := make(chan error, 1) - go func() { - _, _, err2 := c.Read(ctx) - errc <- err2 - }() + ctx = c.CloseRead(ctx) err := c.Ping(ctx) if err != nil { return err } - err = c.Write(ctx, websocket.MessageText, []byte("hi")) + err = wsjson.Write(ctx, c, "hi") if err != nil { return err } - err = <-errc - var ce websocket.CloseError - if xerrors.As(err, &ce) && ce.Code == websocket.StatusNormalClosure { - return nil - } - return xerrors.Errorf("unexpected error: %w", err) + <-ctx.Done() + err = c.Ping(context.Background()) + return assertCloseStatus(err, websocket.StatusNormalClosure) }, client: func(ctx context.Context, c *websocket.Conn) error { // We read a message from the connection and then keep reading until // the Ping completes. - done := make(chan struct{}) + pingErrc := make(chan error, 1) go func() { - _, _, err := c.Read(ctx) - if err != nil { - c.Close(websocket.StatusInternalError, err.Error()) - return - } - - close(done) - - c.Read(ctx) + pingErrc <- c.Ping(ctx) }() - err := c.Ping(ctx) + // Once this completes successfully, that means they sent their ping and we responded to it. + err := assertJSONRead(ctx, c, "hi") if err != nil { return err } - <-done + // Now we need to ensure we're reading for their pong from our ping. + // Need new var to not race with above goroutine. + ctx2 := c.CloseRead(ctx) - c.Close(websocket.StatusNormalClosure, "") - return nil + // Now we wait for our pong. + select { + case err = <-pingErrc: + return err + case <-ctx2.Done(): + return xerrors.Errorf("failed to wait for pong: %w", ctx2.Err()) + } }, }, { name: "readLimit", server: func(ctx context.Context, c *websocket.Conn) error { - _, _, err := c.Read(ctx) - if err == nil || !strings.Contains(err.Error(), "read limited at") { - return xerrors.Errorf("expected error but got nil: %+v", err) - } - return nil + _, _, err2 := c.Read(ctx) + return assertErrorContains(err2, "read limited at 32768 bytes") }, client: func(ctx context.Context, c *websocket.Conn) error { - c.CloseRead(ctx) - err := c.Write(ctx, websocket.MessageBinary, []byte(strings.Repeat("x", 32769))) if err != nil { return err } - err = c.Ping(ctx) - - var ce websocket.CloseError - if !xerrors.As(err, &ce) || ce.Code != websocket.StatusMessageTooBig { - return xerrors.Errorf("unexpected error: %w", err) - } - - return nil + _, _, err2 := c.Read(ctx) + return assertCloseStatus(err2, websocket.StatusMessageTooBig) }, }, { name: "wsjson/binary", server: func(ctx context.Context, c *websocket.Conn) error { var v interface{} - err := wsjson.Read(ctx, c, &v) - if err == nil || !strings.Contains(err.Error(), "unexpected frame type") { - return xerrors.Errorf("expected error: %v", err) - } - return nil + err2 := wsjson.Read(ctx, c, &v) + return assertErrorContains(err2, "unexpected frame type") }, client: func(ctx context.Context, c *websocket.Conn) error { return wspb.Write(ctx, c, ptypes.DurationProto(100)) @@ -633,11 +451,8 @@ func TestConn(t *testing.T) { name: "wsjson/badRead", server: func(ctx context.Context, c *websocket.Conn) error { var v interface{} - err := wsjson.Read(ctx, c, &v) - if err == nil || !strings.Contains(err.Error(), "failed to unmarshal json") { - return xerrors.Errorf("expected error: %v", err) - } - return nil + err2 := wsjson.Read(ctx, c, &v) + return assertErrorContains(err2, "failed to unmarshal json") }, client: func(ctx context.Context, c *websocket.Conn) error { return c.Write(ctx, websocket.MessageText, []byte("notjson")) @@ -646,18 +461,12 @@ func TestConn(t *testing.T) { { name: "wsjson/badWrite", server: func(ctx context.Context, c *websocket.Conn) error { - _, _, err := c.Read(ctx) - if err == nil || !strings.Contains(err.Error(), "StatusInternalError") { - return xerrors.Errorf("expected error: %v", err) - } - return nil + _, _, err2 := c.Read(ctx) + return assertCloseStatus(err2, websocket.StatusNormalClosure) }, client: func(ctx context.Context, c *websocket.Conn) error { err := wsjson.Write(ctx, c, fmt.Println) - if err == nil { - return xerrors.Errorf("expected error: %v", err) - } - return nil + return assertErrorContains(err, "failed to encode json") }, }, { @@ -665,10 +474,7 @@ func TestConn(t *testing.T) { server: func(ctx context.Context, c *websocket.Conn) error { var v proto.Message err := wspb.Read(ctx, c, v) - if err == nil || !strings.Contains(err.Error(), "unexpected frame type") { - return xerrors.Errorf("expected error: %v", err) - } - return nil + return assertErrorContains(err, "unexpected frame type") }, client: func(ctx context.Context, c *websocket.Conn) error { return wsjson.Write(ctx, c, "hi") @@ -679,10 +485,7 @@ func TestConn(t *testing.T) { server: func(ctx context.Context, c *websocket.Conn) error { var v timestamp.Timestamp err := wspb.Read(ctx, c, &v) - if err == nil || !strings.Contains(err.Error(), "failed to unmarshal protobuf") { - return xerrors.Errorf("expected error: %v", err) - } - return nil + return assertErrorContains(err, "failed to unmarshal protobuf") }, client: func(ctx context.Context, c *websocket.Conn) error { return c.Write(ctx, websocket.MessageBinary, []byte("notpb")) @@ -692,17 +495,11 @@ func TestConn(t *testing.T) { name: "wspb/badWrite", server: func(ctx context.Context, c *websocket.Conn) error { _, _, err := c.Read(ctx) - if err == nil || !strings.Contains(err.Error(), "StatusInternalError") { - return xerrors.Errorf("expected error: %v", err) - } - return nil + return assertCloseStatus(err, websocket.StatusNormalClosure) }, client: func(ctx context.Context, c *websocket.Conn) error { err := wspb.Write(ctx, c, nil) - if err == nil { - return xerrors.Errorf("expected error: %v", err) - } - return nil + return assertErrorIs(proto.ErrNil, err) }, }, { @@ -712,11 +509,7 @@ func TestConn(t *testing.T) { }, client: func(ctx context.Context, c *websocket.Conn) error { _, _, err := c.Read(ctx) - cerr := &websocket.CloseError{} - if !xerrors.As(err, cerr) || cerr.Code != websocket.StatusInternalError { - return xerrors.Errorf("expected close error with StatusInternalError: %+v", err) - } - return nil + return assertCloseStatus(err, websocket.StatusInternalError) }, }, { @@ -725,14 +518,16 @@ func TestConn(t *testing.T) { ctx, cancel := context.WithTimeout(ctx, time.Second) defer cancel() err := c.Ping(ctx) - if err == nil || !xerrors.Is(err, context.DeadlineExceeded) { - return xerrors.Errorf("expected nil error: %+v", err) - } - return nil + return assertErrorIs(context.DeadlineExceeded, err) }, client: func(ctx context.Context, c *websocket.Conn) error { - c.Read(ctx) - return nil + _, _, err := c.Read(ctx) + err1 := assertErrorContains(err, "connection reset") + err2 := assertErrorIs(io.EOF, err) + if err1 != nil || err2 != nil { + return nil + } + return multierr.Combine(err1, err2) }, }, { @@ -743,14 +538,11 @@ func TestConn(t *testing.T) { ctx, cancel := context.WithTimeout(ctx, time.Second) defer cancel() err := c.Write(ctx, websocket.MessageBinary, []byte("meow")) - if !xerrors.Is(err, context.DeadlineExceeded) { - return xerrors.Errorf("expected deadline exceeded error: %+v", err) - } - return nil + return assertErrorIs(context.DeadlineExceeded, err) }, client: func(ctx context.Context, c *websocket.Conn) error { - time.Sleep(time.Second) - return nil + _, _, err := c.Read(ctx) + return assertErrorIs(io.EOF, err) }, }, { @@ -759,14 +551,11 @@ func TestConn(t *testing.T) { ctx, cancel := context.WithTimeout(ctx, time.Second) defer cancel() _, _, err := c.Read(ctx) - if !xerrors.Is(err, context.DeadlineExceeded) { - return xerrors.Errorf("expected deadline exceeded error: %+v", err) - } - return nil + return assertErrorIs(context.DeadlineExceeded, err) }, client: func(ctx context.Context, c *websocket.Conn) error { - c.Read(ctx) - return nil + _, _, err := c.Read(ctx) + return assertErrorIs(io.EOF, err) }, }, { @@ -777,18 +566,11 @@ func TestConn(t *testing.T) { return err } _, _, err = c.Read(ctx) - cerr := &websocket.CloseError{} - if !xerrors.As(err, cerr) || cerr.Code != websocket.StatusProtocolError { - return xerrors.Errorf("expected close error with StatusProtocolError: %+v", err) - } - return nil + return assertErrorContains(err, "unknown opcode") }, client: func(ctx context.Context, c *websocket.Conn) error { _, _, err := c.Read(ctx) - if err == nil || !strings.Contains(err.Error(), "opcode") { - return xerrors.Errorf("expected error that contains opcode: %+v", err) - } - return nil + return assertErrorContains(err, "unknown opcode") }, }, { @@ -821,18 +603,11 @@ func TestConn(t *testing.T) { return err } _, _, err = c.Read(ctx) - cerr := &websocket.CloseError{} - if !xerrors.As(err, cerr) || cerr.Code != websocket.StatusProtocolError { - return xerrors.Errorf("expected close error with StatusProtocolError: %+v", err) - } - return nil + return assertCloseStatus(err, websocket.StatusProtocolError) }, client: func(ctx context.Context, c *websocket.Conn) error { _, _, err := c.Read(ctx) - if err == nil || !strings.Contains(err.Error(), "too large") { - return xerrors.Errorf("expected error that contains too large: %+v", err) - } - return nil + return assertErrorContains(err, "too large") }, }, { @@ -847,18 +622,11 @@ func TestConn(t *testing.T) { return err } _, _, err = c.Read(ctx) - cerr := &websocket.CloseError{} - if !xerrors.As(err, cerr) || cerr.Code != websocket.StatusProtocolError { - return xerrors.Errorf("expected close error with StatusProtocolError: %+v", err) - } - return nil + return assertCloseStatus(err, websocket.StatusProtocolError) }, client: func(ctx context.Context, c *websocket.Conn) error { _, _, err := c.Read(ctx) - if err == nil || !strings.Contains(err.Error(), "fragmented") { - return xerrors.Errorf("expected error that contains fragmented: %+v", err) - } - return nil + return assertErrorContains(err, "fragmented") }, }, { @@ -869,18 +637,11 @@ func TestConn(t *testing.T) { return err } _, _, err = c.Read(ctx) - cerr := &websocket.CloseError{} - if !xerrors.As(err, cerr) || cerr.Code != websocket.StatusProtocolError { - return xerrors.Errorf("expected close error with StatusProtocolError: %+v", err) - } - return nil + return assertCloseStatus(err, websocket.StatusProtocolError) }, client: func(ctx context.Context, c *websocket.Conn) error { _, _, err := c.Read(ctx) - if err == nil || !strings.Contains(err.Error(), "invalid status code") { - return xerrors.Errorf("expected error that contains invalid status code: %+v", err) - } - return nil + return assertErrorContains(err, "invalid status code") }, }, { @@ -896,10 +657,7 @@ func TestConn(t *testing.T) { return err } _, _, err = c.Reader(ctx) - if err == nil || !strings.Contains(err.Error(), "previous message not read to completion") { - return xerrors.Errorf("expected non nil error: %v", err) - } - return nil + return assertErrorContains(err, "previous message not read to completion") }, client: func(ctx context.Context, c *websocket.Conn) error { err := c.Write(ctx, websocket.MessageBinary, []byte(strings.Repeat("x", 11))) @@ -907,10 +665,7 @@ func TestConn(t *testing.T) { return err } _, _, err = c.Read(ctx) - if err == nil { - return xerrors.Errorf("expected non nil error: %v", err) - } - return nil + return assertCloseStatus(err, websocket.StatusInternalError) }, }, { @@ -926,10 +681,7 @@ func TestConn(t *testing.T) { return err } _, _, err = c.Reader(ctx) - if err == nil || !strings.Contains(err.Error(), "previous message not read to completion") { - return xerrors.Errorf("expected non nil error: %v", err) - } - return nil + return assertErrorContains(err, "previous message not read to completion") }, client: func(ctx context.Context, c *websocket.Conn) error { w, err := c.Writer(ctx, websocket.MessageBinary) @@ -953,10 +705,7 @@ func TestConn(t *testing.T) { return xerrors.Errorf("failed to flush: %w", err) } _, _, err = c.Read(ctx) - if err == nil { - return xerrors.Errorf("expected non nil error: %v", err) - } - return nil + return assertCloseStatus(err, websocket.StatusInternalError) }, }, { @@ -972,10 +721,7 @@ func TestConn(t *testing.T) { return err } _, _, err = c.Reader(ctx) - if err == nil || !strings.Contains(err.Error(), "received new data message without finishing") { - return xerrors.Errorf("expected non nil error: %v", err) - } - return nil + return assertErrorContains(err, "received new data message without finishing") }, client: func(ctx context.Context, c *websocket.Conn) error { w, err := c.Writer(ctx, websocket.MessageBinary) @@ -995,27 +741,18 @@ func TestConn(t *testing.T) { return xerrors.Errorf("expected non nil error") } _, _, err = c.Read(ctx) - if err == nil || !strings.Contains(err.Error(), "received new data message without finishing") { - return xerrors.Errorf("expected non nil error: %v", err) - } - return nil + return assertErrorContains(err, "received new data message without finishing") }, }, { name: "continuationFrameWithoutDataFrame", server: func(ctx context.Context, c *websocket.Conn) error { _, _, err := c.Reader(ctx) - if err == nil || !strings.Contains(err.Error(), "received continuation frame not after data") { - return xerrors.Errorf("expected non nil error: %v", err) - } - return nil + return assertErrorContains(err, "received continuation frame not after data") }, client: func(ctx context.Context, c *websocket.Conn) error { _, err := c.WriteFrame(ctx, false, websocket.OPContinuation, []byte(strings.Repeat("x", 10))) - if err != nil { - return xerrors.Errorf("expected non nil error") - } - return nil + return err }, }, { @@ -1031,21 +768,22 @@ func TestConn(t *testing.T) { if err != nil { return err } - _, b, err := c.Read(ctx) + err = assertEqualf("hi", v, "unexpected JSON") if err != nil { return err } - if string(b) != "hi" { - return xerrors.Errorf("expected hi but got %q", string(b)) + _, b, err := c.Read(ctx) + if err != nil { + return err } - return nil + return assertEqualf("hi", string(b), "unexpected JSON") }, client: func(ctx context.Context, c *websocket.Conn) error { err := wsjson.Write(ctx, c, "hi") if err != nil { return err } - return c.Write(ctx, websocket.MessageBinary, []byte("hi")) + return c.Write(ctx, websocket.MessageText, []byte("hi")) }, }, { @@ -1057,10 +795,7 @@ func TestConn(t *testing.T) { } p := make([]byte, 11) _, err = io.ReadFull(r, p) - if err == nil || !strings.Contains(err.Error(), "received new data message without finishing") { - return xerrors.Errorf("expected non nil error: %v", err) - } - return nil + return assertErrorContains(err, "received new data message without finishing") }, client: func(ctx context.Context, c *websocket.Conn) error { w, err := c.Writer(ctx, websocket.MessageBinary) @@ -1080,10 +815,7 @@ func TestConn(t *testing.T) { return xerrors.Errorf("expected non nil error") } _, _, err = c.Read(ctx) - if err == nil { - return xerrors.Errorf("expected non nil error: %v", err) - } - return nil + return assertCloseStatus(err, websocket.StatusProtocolError) }, }, { @@ -1098,10 +830,7 @@ func TestConn(t *testing.T) { return err } _, err = r.Read(make([]byte, 1)) - if err == nil || !strings.Contains(err.Error(), "cannot use EOFed reader") { - return xerrors.Errorf("expected non nil error: %+v", err) - } - return nil + return assertErrorContains(err, "cannot use EOFed reader") }, client: func(ctx context.Context, c *websocket.Conn) error { return c.Write(ctx, websocket.MessageBinary, []byte("hi")) @@ -1111,10 +840,7 @@ func TestConn(t *testing.T) { name: "eofInPayload", server: func(ctx context.Context, c *websocket.Conn) error { _, _, err := c.Read(ctx) - if err == nil || !strings.Contains(err.Error(), "failed to read frame payload") { - return xerrors.Errorf("expected failed to read frame payload: %v", err) - } - return nil + return assertErrorContains(err, "failed to read frame payload") }, client: func(ctx context.Context, c *websocket.Conn) error { _, err := c.WriteHalfFrame(ctx) @@ -1131,11 +857,14 @@ func TestConn(t *testing.T) { tls := rand.Intn(2) == 1 s, closeFn := testServer(t, func(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, nil) + c, err := websocket.Accept(w, r, tc.acceptOpts) if err != nil { return err } defer c.Close(websocket.StatusInternalError, "") + if tc.server == nil { + return nil + } return tc.server(r.Context(), c) }, tls) defer closeFn() @@ -1145,21 +874,35 @@ func TestConn(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - opts := &websocket.DialOptions{} + opts := tc.dialOpts if tls { + if opts == nil { + opts = &websocket.DialOptions{} + } opts.HTTPClient = s.Client() } - c, _, err := websocket.Dial(ctx, wsURL, opts) + c, resp, err := websocket.Dial(ctx, wsURL, opts) if err != nil { t.Fatal(err) } defer c.Close(websocket.StatusInternalError, "") - err = tc.client(ctx, c) - if err != nil { - t.Fatalf("client failed: %+v", err) + if tc.response != nil { + err = tc.response(resp) + if err != nil { + t.Fatalf("response asserter failed: %+v", err) + } } + + if tc.client != nil { + err = tc.client(ctx, c) + if err != nil { + t.Fatalf("client failed: %+v", err) + } + } + + c.Close(websocket.StatusNormalClosure, "") }) } } @@ -1174,7 +917,7 @@ func testServer(tb testing.TB, fn func(w http.ResponseWriter, r *http.Request) e atomic.AddInt64(&conns, 1) defer atomic.AddInt64(&conns, -1) - ctx, cancel := context.WithTimeout(r.Context(), time.Second*30) + ctx, cancel := context.WithTimeout(r.Context(), time.Second*10) defer cancel() r = r.WithContext(ctx) @@ -1598,3 +1341,67 @@ func BenchmarkConn(b *testing.B) { } }) } + +func assertCloseStatus(err error, code websocket.StatusCode) error { + var cerr websocket.CloseError + if !xerrors.As(err, &cerr) { + return xerrors.Errorf("no websocket close error in error chain: %+v", err) + } + return assertEqualf(code, cerr.Code, "unexpected status code") +} + +func assertJSONRead(ctx context.Context, c *websocket.Conn, exp interface{}) (err error) { + var act interface{} + err = wsjson.Read(ctx, c, &act) + if err != nil { + return err + } + + return assertEqualf(exp, act, "unexpected JSON") +} + +func assertProtobufRead(ctx context.Context, c *websocket.Conn, exp interface{}) error { + expType := reflect.TypeOf(exp) + actv := reflect.New(expType.Elem()) + act := actv.Interface().(proto.Message) + err := wspb.Read(ctx, c, act) + if err != nil { + return err + } + + return assertEqualf(exp, act, "unexpected protobuf") +} + +func assertSubprotocol(c *websocket.Conn, exp string) error { + return assertEqualf(exp, c.Subprotocol(), "unexpected subprotocol") +} + +func assertEqualf(exp, act interface{}, f string, v ...interface{}) error { + if diff := cmpDiff(exp, act); diff != "" { + return xerrors.Errorf(f+": %v", append(v, diff)) + } + return nil +} + +func assertNetConnRead(r io.Reader, exp string) error { + act := make([]byte, len(exp)) + _, err := r.Read(act) + if err != nil { + return err + } + return assertEqualf(exp, string(act), "unexpected net conn read") +} + +func assertErrorContains(err error, exp string) error { + if err == nil || !strings.Contains(err.Error(), exp) { + return xerrors.Errorf("expected error that contains %q but got: %+v", exp, err) + } + return nil +} + +func assertErrorIs(exp, act error) error { + if !xerrors.Is(act, exp) { + return xerrors.Errorf("expected error %+v to be in %+v", exp, act) + } + return nil +} From b02bae9d73cd74b42bfd1f5e3589102ce75e821b Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 31 Aug 2019 00:50:27 -0500 Subject: [PATCH 092/519] Remove -bench from ./ci/test.sh Testing should not run benchmarks by default, it takes too long. --- ci/test.sh | 8 +++----- docs/CONTRIBUTING.md | 10 ++++------ 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/ci/test.sh b/ci/test.sh index 1d4a8b07..3c476d93 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -10,18 +10,16 @@ argv=( "--junitfile=ci/out/websocket/testReport.xml" "--format=short-verbose" -- - -race "-vet=off" - "-bench=." ) -# Interactive usage probably does not want to enable benchmarks, race detection -# turn off vet or use gotestsum by default. +# Interactive usage does not want to turn off vet or use gotestsum by default. if [[ $# -gt 0 ]]; then argv=(go test "$@") fi -# We always want coverage. +# We always want coverage and race detection. argv+=( + -race "-coverprofile=ci/out/coverage.prof" "-coverpkg=./..." ) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index f003e743..74a25540 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -43,12 +43,10 @@ For coverage details locally, please see `ci/out/coverage.html` after running `c See [ci/image/Dockerfile](ci/image/Dockerfile) for the installation of the CI dependencies on Ubuntu. -You can also run tests normally with `go test`. -`ci/test.sh` just passes a default set of flags to `go test` to collect coverage, -enable the race detector, run benchmarks and also prettifies the output. +You can also run tests normally with `go test`. `ci/test.sh` just passes a default set of flags to +`go test` to collect coverage, enable the race detector and also prettifies the output. -If you pass flags to `ci/test.sh`, it will pass those flags directly to `go test` but will also -collect coverage for you. This is nice for when you don't want to wait for benchmarks -or the race detector but want to have coverage. +You can pass flags to `ci/test.sh` if you want to run a specific test or otherwise +control the behaviour of `go test`. Coverage percentage from codecov and the CI scripts will be different because they are calculated differently. From a819b572f273d2e83aa717d78fc0d8aa4c5194e2 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 31 Aug 2019 18:53:11 -0500 Subject: [PATCH 093/519] Fully translate autobahn tests to Go On top of the new Go tests designed to maximize coverage. Just so I can get rid of the autobahn-testsuite with confidence. --- export_test.go | 89 ++++- go.mod | 21 +- go.sum | 52 ++- websocket.go | 10 +- websocket_autobahn_python_test.go | 239 ++++++++++++ websocket_test.go | 579 +++++++++++++++++------------- 6 files changed, 707 insertions(+), 283 deletions(-) create mode 100644 websocket_autobahn_python_test.go diff --git a/export_test.go b/export_test.go index fc885bff..fb3cf81f 100644 --- a/export_test.go +++ b/export_test.go @@ -1,27 +1,98 @@ package websocket import ( + "bufio" "context" + + "golang.org/x/xerrors" ) -type Addr = websocketAddr +type ( + Addr = websocketAddr + OpCode int +) -const OPClose = opClose -const OPBinary = opBinary -const OPPing = opPing -const OPContinuation = opContinuation +const ( + OpClose = OpCode(opClose) + OpBinary = OpCode(opBinary) + OpPing = OpCode(opPing) + OpPong = OpCode(opPong) + OpContinuation = OpCode(opContinuation) +) + +func (c *Conn) ReadFrame(ctx context.Context) (OpCode, []byte, error) { + h, err := c.readFrameHeader(ctx) + if err != nil { + return 0, nil, err + } + b := make([]byte, h.payloadLength) + _, err = c.readFramePayload(ctx, b) + if err != nil { + return 0, nil, err + } + if h.masked { + fastXOR(h.maskKey, 0, b) + } + return OpCode(h.opcode), b, nil +} + +func (c *Conn) WriteFrame(ctx context.Context, fin bool, opc OpCode, p []byte) (int, error) { + return c.writeFrame(ctx, fin, opcode(opc), p) +} + +func (c *Conn) WriteHeader(ctx context.Context, fin bool, opc OpCode, lenp int64) error { + headerBytes := writeHeader(c.writeHeaderBuf, header{ + fin: fin, + opcode: opcode(opc), + payloadLength: lenp, + masked: c.client, + }) + _, err := c.bw.Write(headerBytes) + if err != nil { + return xerrors.Errorf("failed to write header: %w", err) + } + return nil +} -func (c *Conn) WriteFrame(ctx context.Context, fin bool, opcode opcode, p []byte) (int, error) { - return c.writeFrame(ctx, fin, opcode, p) +func (c *Conn) PingWithPayload(ctx context.Context, p string) error { + return c.ping(ctx, p) } func (c *Conn) WriteHalfFrame(ctx context.Context) (int, error) { return c.realWriteFrame(ctx, header{ + fin: true, opcode: opBinary, - payloadLength: 5, - }, make([]byte, 10)) + payloadLength: 10, + }, make([]byte, 5)) +} + +func (c *Conn) CloseUnderlyingConn() { + c.closer.Close() } func (c *Conn) Flush() error { return c.bw.Flush() } + +func (c CloseError) Bytes() ([]byte, error) { + return c.bytes() +} + +func (c *Conn) BW() *bufio.Writer { + return c.bw +} + +func (c *Conn) WriteClose(ctx context.Context, code StatusCode, reason string) ([]byte, error) { + b, err := CloseError{ + Code: code, + Reason: reason, + }.Bytes() + if err != nil { + return nil, err + } + _, err = c.WriteFrame(ctx, true, OpClose, b) + if err != nil { + return nil, err + } + return b, nil +} diff --git a/go.mod b/go.mod index c9cc6fc4..70fe1d4c 100644 --- a/go.mod +++ b/go.mod @@ -3,18 +3,29 @@ module nhooyr.io/websocket go 1.12 require ( - github.com/golang/protobuf v1.3.1 - github.com/google/go-cmp v0.2.0 + github.com/fatih/color v1.7.0 // indirect + github.com/golang/protobuf v1.3.2 + github.com/google/go-cmp v0.3.1 + github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect github.com/kr/pretty v0.1.0 // indirect + github.com/mattn/go-colorable v0.1.2 // indirect + github.com/mattn/go-isatty v0.0.9 // indirect + github.com/pkg/errors v0.8.1 // indirect + github.com/sirupsen/logrus v1.4.2 // indirect + github.com/spf13/pflag v1.0.3 // indirect + github.com/stretchr/testify v1.4.0 // indirect go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16 go.uber.org/atomic v1.4.0 // indirect go.uber.org/multierr v1.1.0 golang.org/x/lint v0.0.0-20190409202823-959b441ac422 - golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6 + golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 + golang.org/x/sys v0.0.0-20190830142957-1e83adbbebd0 // indirect golang.org/x/text v0.3.2 // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 - golang.org/x/tools v0.0.0-20190429184909-35c670923e21 - golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 + golang.org/x/tools v0.0.0-20190830223141-573d9926052a + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect + gotest.tools v2.2.0+incompatible // indirect gotest.tools/gotestsum v0.3.6-0.20190825182939-fc6cb5870c52 mvdan.cc/sh v2.6.4+incompatible ) diff --git a/go.sum b/go.sum index 187a2285..906f9c38 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,27 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.6.0 h1:66qjqZk8kalYAvDRtM1AdAJQI0tj4Wrue3Eq3B3pmFU= github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -20,8 +29,13 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -29,15 +43,25 @@ github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.0.5 h1:8c8b5uO0zS4X6RPl/sd1ENwSkIc0/H2PaHxE3udaE8I= github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16 h1:3gGa1bM0nG7Ruhu5b7wKnoOOwAD/fJ8iyyAcpOzDG3A= go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw= go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= @@ -53,14 +77,20 @@ golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6 h1:FP8hkuE6yUEaJnK7O2eTuejKWwW+Rhfj80dQ2JcKxCU= -golang.org/x/net v0.0.0-20190424112056-4829fb13d2c6/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM= +golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190830142957-1e83adbbebd0 h1:7z820YPX9pxWR59qM7BE5+fglp4D/mKqAwCvGt11b+8= +golang.org/x/sys v0.0.0-20190830142957-1e83adbbebd0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -69,14 +99,16 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZe golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190429184909-35c670923e21 h1:Kjcw+D2LTzLmxOHrMK9uvYP/NigJ0EdwMgzt6EU+Ghs= -golang.org/x/tools v0.0.0-20190429184909-35c670923e21/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522 h1:bhOzK9QyoD0ogCnFro1m2mz41+Ib0oOhfJnBp5MR4K4= -golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/tools v0.0.0-20190830223141-573d9926052a h1:XAHT1kdPpnU8Hk+FPi42KZFhtNFEk4vBg1U4OmIeHTU= +golang.org/x/tools v0.0.0-20190830223141-573d9926052a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0= @@ -85,8 +117,12 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkep gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools v2.1.0+incompatible h1:5USw7CrJBYKqjg9R7QlA6jzqZKEAtvW82aNmsxxGPxw= gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= +gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= +gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= gotest.tools/gotestsum v0.3.6-0.20190825182939-fc6cb5870c52 h1:Qr31uPFyjpOhAgRfKV4ATUnknnLT2X7HFjqwkstdbbE= gotest.tools/gotestsum v0.3.6-0.20190825182939-fc6cb5870c52/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY= mvdan.cc/sh v2.6.4+incompatible h1:eD6tDeh0pw+/TOTI1BBEryZ02rD2nMcFsgcvde7jffM= diff --git a/websocket.go b/websocket.go index 6f28a4bf..7dabfa25 100644 --- a/websocket.go +++ b/websocket.go @@ -886,17 +886,17 @@ func init() { // // TCP Keepalives should suffice for most use cases. func (c *Conn) Ping(ctx context.Context) error { - err := c.ping(ctx) + id := rand.Uint64() + p := strconv.FormatUint(id, 10) + + err := c.ping(ctx, p) if err != nil { return xerrors.Errorf("failed to ping: %w", err) } return nil } -func (c *Conn) ping(ctx context.Context) error { - id := rand.Uint64() - p := strconv.FormatUint(id, 10) - +func (c *Conn) ping(ctx context.Context, p string) error { pong := make(chan struct{}) c.activePingsMu.Lock() diff --git a/websocket_autobahn_python_test.go b/websocket_autobahn_python_test.go new file mode 100644 index 00000000..32ee1f5c --- /dev/null +++ b/websocket_autobahn_python_test.go @@ -0,0 +1,239 @@ +// +build autobahn-python + +package websocket_test + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "strconv" + "strings" + "testing" + "time" + + "nhooyr.io/websocket" +) + +// https://github.com/crossbario/autobahn-python/tree/master/wstest +func TestPythonAutobahnServer(t *testing.T) { + t.Parallel() + + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + Subprotocols: []string{"echo"}, + }) + if err != nil { + t.Logf("server handshake failed: %+v", err) + return + } + echoLoop(r.Context(), c) + })) + defer s.Close() + + spec := map[string]interface{}{ + "outdir": "ci/out/wstestServerReports", + "servers": []interface{}{ + map[string]interface{}{ + "agent": "main", + "url": strings.Replace(s.URL, "http", "ws", 1), + }, + }, + "cases": []string{"*"}, + // We skip the UTF-8 handling tests as there isn't any reason to reject invalid UTF-8, just + // more performance overhead. 7.5.1 is the same. + // 12.* and 13.* as we do not support compression. + "exclude-cases": []string{"6.*", "7.5.1", "12.*", "13.*"}, + } + specFile, err := ioutil.TempFile("", "websocketFuzzingClient.json") + if err != nil { + t.Fatalf("failed to create temp file for fuzzingclient.json: %v", err) + } + defer specFile.Close() + + e := json.NewEncoder(specFile) + e.SetIndent("", "\t") + err = e.Encode(spec) + if err != nil { + t.Fatalf("failed to write spec: %v", err) + } + + err = specFile.Close() + if err != nil { + t.Fatalf("failed to close file: %v", err) + } + + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, time.Minute*10) + defer cancel() + + args := []string{"--mode", "fuzzingclient", "--spec", specFile.Name()} + wstest := exec.CommandContext(ctx, "wstest", args...) + out, err := wstest.CombinedOutput() + if err != nil { + t.Fatalf("failed to run wstest: %v\nout:\n%s", err, out) + } + + checkWSTestIndex(t, "./ci/out/wstestServerReports/index.json") +} + +func unusedListenAddr() (string, error) { + l, err := net.Listen("tcp", "localhost:0") + if err != nil { + return "", err + } + l.Close() + return l.Addr().String(), nil +} + +// https://github.com/crossbario/autobahn-python/blob/master/wstest/testee_client_aio.py +func TestPythonAutobahnClientOld(t *testing.T) { + t.Parallel() + + serverAddr, err := unusedListenAddr() + if err != nil { + t.Fatalf("failed to get unused listen addr for wstest: %v", err) + } + + wsServerURL := "ws://" + serverAddr + + spec := map[string]interface{}{ + "url": wsServerURL, + "outdir": "ci/out/wstestClientReports", + "cases": []string{"*"}, + // See TestAutobahnServer for the reasons why we exclude these. + "exclude-cases": []string{"6.*", "7.5.1", "12.*", "13.*"}, + } + specFile, err := ioutil.TempFile("", "websocketFuzzingServer.json") + if err != nil { + t.Fatalf("failed to create temp file for fuzzingserver.json: %v", err) + } + defer specFile.Close() + + e := json.NewEncoder(specFile) + e.SetIndent("", "\t") + err = e.Encode(spec) + if err != nil { + t.Fatalf("failed to write spec: %v", err) + } + + err = specFile.Close() + if err != nil { + t.Fatalf("failed to close file: %v", err) + } + + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, time.Minute*10) + defer cancel() + + args := []string{"--mode", "fuzzingserver", "--spec", specFile.Name(), + // Disables some server that runs as part of fuzzingserver mode. + // See https://github.com/crossbario/autobahn-testsuite/blob/058db3a36b7c3a1edf68c282307c6b899ca4857f/autobahntestsuite/autobahntestsuite/wstest.py#L124 + "--webport=0", + } + wstest := exec.CommandContext(ctx, "wstest", args...) + err = wstest.Start() + if err != nil { + t.Fatal(err) + } + defer func() { + err := wstest.Process.Kill() + if err != nil { + t.Error(err) + } + }() + + // Let it come up. + time.Sleep(time.Second * 5) + + var cases int + func() { + c, _, err := websocket.Dial(ctx, wsServerURL+"/getCaseCount", nil) + if err != nil { + t.Fatal(err) + } + defer c.Close(websocket.StatusInternalError, "") + + _, r, err := c.Reader(ctx) + if err != nil { + t.Fatal(err) + } + b, err := ioutil.ReadAll(r) + if err != nil { + t.Fatal(err) + } + cases, err = strconv.Atoi(string(b)) + if err != nil { + t.Fatal(err) + } + + c.Close(websocket.StatusNormalClosure, "") + }() + + for i := 1; i <= cases; i++ { + func() { + ctx, cancel := context.WithTimeout(ctx, time.Second*45) + defer cancel() + + c, _, err := websocket.Dial(ctx, fmt.Sprintf(wsServerURL+"/runCase?case=%v&agent=main", i), nil) + if err != nil { + t.Fatal(err) + } + echoLoop(ctx, c) + }() + } + + c, _, err := websocket.Dial(ctx, fmt.Sprintf(wsServerURL+"/updateReports?agent=main"), nil) + if err != nil { + t.Fatal(err) + } + c.Close(websocket.StatusNormalClosure, "") + + checkWSTestIndex(t, "./ci/out/wstestClientReports/index.json") +} + +func checkWSTestIndex(t *testing.T, path string) { + wstestOut, err := ioutil.ReadFile(path) + if err != nil { + t.Fatalf("failed to read index.json: %v", err) + } + + var indexJSON map[string]map[string]struct { + Behavior string `json:"behavior"` + BehaviorClose string `json:"behaviorClose"` + } + err = json.Unmarshal(wstestOut, &indexJSON) + if err != nil { + t.Fatalf("failed to unmarshal index.json: %v", err) + } + + var failed bool + for _, tests := range indexJSON { + for test, result := range tests { + switch result.Behavior { + case "OK", "NON-STRICT", "INFORMATIONAL": + default: + failed = true + t.Errorf("test %v failed", test) + } + switch result.BehaviorClose { + case "OK", "INFORMATIONAL": + default: + failed = true + t.Errorf("bad close behaviour for test %v", test) + } + } + } + + if failed { + path = strings.Replace(path, ".json", ".html", 1) + if os.Getenv("CI") == "" { + t.Errorf("wstest found failure, please see %q (output as an artifact in CI)", path) + } + } +} diff --git a/websocket_test.go b/websocket_test.go index e6529f3b..906014ca 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -1,19 +1,17 @@ package websocket_test import ( + "bytes" "context" "encoding/json" "fmt" "io" "io/ioutil" "math/rand" - "net" "net/http" "net/http/cookiejar" "net/http/httptest" "net/url" - "os" - "os/exec" "reflect" "strconv" "strings" @@ -175,7 +173,7 @@ func TestHandshake(t *testing.T) { wsURL := strings.Replace(s.URL, "http", "ws", 1) - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() err := tc.client(ctx, wsURL) @@ -598,7 +596,7 @@ func TestConn(t *testing.T) { { name: "largeControlFrame", server: func(ctx context.Context, c *websocket.Conn) error { - _, err := c.WriteFrame(ctx, true, websocket.OPClose, []byte(strings.Repeat("x", 4096))) + _, err := c.WriteFrame(ctx, true, websocket.OpClose, []byte(strings.Repeat("x", 4096))) if err != nil { return err } @@ -613,7 +611,7 @@ func TestConn(t *testing.T) { { name: "fragmentedControlFrame", server: func(ctx context.Context, c *websocket.Conn) error { - _, err := c.WriteFrame(ctx, false, websocket.OPPing, []byte(strings.Repeat("x", 32))) + _, err := c.WriteFrame(ctx, false, websocket.OpPing, []byte(strings.Repeat("x", 32))) if err != nil { return err } @@ -632,7 +630,7 @@ func TestConn(t *testing.T) { { name: "invalidClosePayload", server: func(ctx context.Context, c *websocket.Conn) error { - _, err := c.WriteFrame(ctx, true, websocket.OPClose, []byte{0x17, 0x70}) + _, err := c.WriteFrame(ctx, true, websocket.OpClose, []byte{0x17, 0x70}) if err != nil { return err } @@ -736,7 +734,7 @@ func TestConn(t *testing.T) { if err != nil { return xerrors.Errorf("failed to flush: %w", err) } - _, err = c.WriteFrame(ctx, true, websocket.OPBinary, []byte(strings.Repeat("x", 10))) + _, err = c.WriteFrame(ctx, true, websocket.OpBinary, []byte(strings.Repeat("x", 10))) if err != nil { return xerrors.Errorf("expected non nil error") } @@ -751,7 +749,7 @@ func TestConn(t *testing.T) { return assertErrorContains(err, "received continuation frame not after data") }, client: func(ctx context.Context, c *websocket.Conn) error { - _, err := c.WriteFrame(ctx, false, websocket.OPContinuation, []byte(strings.Repeat("x", 10))) + _, err := c.WriteFrame(ctx, false, websocket.OpContinuation, []byte(strings.Repeat("x", 10))) return err }, }, @@ -810,7 +808,7 @@ func TestConn(t *testing.T) { if err != nil { return xerrors.Errorf("failed to flush: %w", err) } - _, err = c.WriteFrame(ctx, true, websocket.OPBinary, []byte(strings.Repeat("x", 10))) + _, err = c.WriteFrame(ctx, true, websocket.OpBinary, []byte(strings.Repeat("x", 10))) if err != nil { return xerrors.Errorf("expected non nil error") } @@ -844,7 +842,11 @@ func TestConn(t *testing.T) { }, client: func(ctx context.Context, c *websocket.Conn) error { _, err := c.WriteHalfFrame(ctx) - return err + if err != nil { + return err + } + c.CloseUnderlyingConn() + return nil }, }, } @@ -871,7 +873,7 @@ func TestConn(t *testing.T) { wsURL := strings.Replace(s.URL, "http", "ws", 1) - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() opts := tc.dialOpts @@ -946,69 +948,287 @@ func testServer(tb testing.TB, fn func(w http.ResponseWriter, r *http.Request) e } } -// https://github.com/crossbario/autobahn-python/tree/master/wstest -func TestAutobahnServer(t *testing.T) { +func TestAutobahn(t *testing.T) { t.Parallel() - if os.Getenv("AUTOBAHN") == "" { - t.Skip("Set $AUTOBAHN to run the autobahn test suite.") - } - s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ - Subprotocols: []string{"echo"}, - }) - if err != nil { - t.Logf("server handshake failed: %+v", err) - return + run := func(t *testing.T, name string, fn func(ctx context.Context, c *websocket.Conn) error) { + run2 := func(t *testing.T, testingClient bool) { + t.Parallel() + + // Run random tests over TLS. + tls := rand.Intn(2) == 1 + + s, closeFn := testServer(t, func(w http.ResponseWriter, r *http.Request) error { + c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + Subprotocols: []string{"echo"}, + }) + if err != nil { + return err + } + defer c.Close(websocket.StatusInternalError, "") + c.SetReadLimit(1 << 40) + + ctx := r.Context() + if testingClient { + echoLoop(r.Context(), c) + return nil + } + + err = fn(ctx, c) + if err != nil { + return err + } + c.Close(websocket.StatusNormalClosure, "") + return nil + }, tls) + defer closeFn() + + wsURL := strings.Replace(s.URL, "http", "ws", 1) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + opts := &websocket.DialOptions{ + Subprotocols: []string{"echo"}, + } + if tls { + opts.HTTPClient = s.Client() + } + + c, _, err := websocket.Dial(ctx, wsURL, opts) + if err != nil { + t.Fatal(err) + } + defer c.Close(websocket.StatusInternalError, "") + c.SetReadLimit(1 << 40) + + if testingClient { + err = fn(ctx, c) + if err != nil { + t.Fatalf("client failed: %+v", err) + } + c.Close(websocket.StatusNormalClosure, "") + return + } + + echoLoop(ctx, c) } - echoLoop(r.Context(), c) - })) - defer s.Close() + t.Run(name, func(t *testing.T) { + t.Parallel() - spec := map[string]interface{}{ - "outdir": "ci/out/wstestServerReports", - "servers": []interface{}{ - map[string]interface{}{ - "agent": "main", - "url": strings.Replace(s.URL, "http", "ws", 1), - }, - }, - "cases": []string{"*"}, - // We skip the UTF-8 handling tests as there isn't any reason to reject invalid UTF-8, just - // more performance overhead. 7.5.1 is the same. - // 12.* and 13.* as we do not support compression. - "exclude-cases": []string{"6.*", "7.5.1", "12.*", "13.*"}, - } - specFile, err := ioutil.TempFile("", "websocketFuzzingClient.json") - if err != nil { - t.Fatalf("failed to create temp file for fuzzingclient.json: %v", err) + t.Run("server", func(t *testing.T) { + run2(t, false) + }) + t.Run("client", func(t *testing.T) { + run2(t, true) + }) + }) } - defer specFile.Close() - e := json.NewEncoder(specFile) - e.SetIndent("", "\t") - err = e.Encode(spec) - if err != nil { - t.Fatalf("failed to write spec: %v", err) - } + // Section 1. + t.Run("echo", func(t *testing.T) { + t.Parallel() + + lengths := []int{ + 0, + 125, + 126, + 127, + 128, + 65535, + 65536, + 65536, + } + run := func(typ websocket.MessageType) { + for i, l := range lengths { + l := l + run(t, fmt.Sprintf("%v/%v", typ, l), func(ctx context.Context, c *websocket.Conn) error { + p := make([]byte, l) + rand.Read(p) + if i == len(lengths)-1 { + w, err := c.Writer(ctx, typ) + if err != nil { + return err + } + for i := 0; i < l; { + j := i + 997 + if j > l { + j = l + } + _, err = w.Write(p[i:j]) + if err != nil { + return err + } + + i = j + } + + err = w.Close() + if err != nil { + return err + } + } else { + err := c.Write(ctx, typ, p) + if err != nil { + return err + } + } + actTyp, p2, err := c.Read(ctx) + if err != nil { + return err + } + err = assertEqualf(typ, actTyp, "unexpected message type") + if err != nil { + return err + } + return assertEqualf(p, p2, "unexpected message") + }) + } + } - err = specFile.Close() - if err != nil { - t.Fatalf("failed to close file: %v", err) - } + run(websocket.MessageText) + run(websocket.MessageBinary) + }) - ctx := context.Background() - ctx, cancel := context.WithTimeout(ctx, time.Minute*10) - defer cancel() + // Section 2. + t.Run("pingPong", func(t *testing.T) { + run(t, "emptyPayload", func(ctx context.Context, c *websocket.Conn) error { + ctx = c.CloseRead(ctx) + return c.PingWithPayload(ctx, "") + }) + run(t, "smallTextPayload", func(ctx context.Context, c *websocket.Conn) error { + ctx = c.CloseRead(ctx) + return c.PingWithPayload(ctx, "hi") + }) + run(t, "smallBinaryPayload", func(ctx context.Context, c *websocket.Conn) error { + ctx = c.CloseRead(ctx) + p := bytes.Repeat([]byte{0xFE}, 16) + return c.PingWithPayload(ctx, string(p)) + }) + run(t, "largeBinaryPayload", func(ctx context.Context, c *websocket.Conn) error { + ctx = c.CloseRead(ctx) + p := bytes.Repeat([]byte{0xFE}, 125) + return c.PingWithPayload(ctx, string(p)) + }) + run(t, "tooLargeBinaryPayload", func(ctx context.Context, c *websocket.Conn) error { + c.CloseRead(ctx) + p := bytes.Repeat([]byte{0xFE}, 126) + err := c.PingWithPayload(ctx, string(p)) + return assertCloseStatus(err, websocket.StatusProtocolError) + }) + run(t, "streamPingPayload", func(ctx context.Context, c *websocket.Conn) error { + err := streamPing(ctx, c, 125) + if err != nil { + return err + } + return assertCloseHandshake(ctx, c, websocket.StatusNormalClosure, "") + }) + t.Run("unsolicitedPong", func(t *testing.T) { + t.Parallel() - args := []string{"--mode", "fuzzingclient", "--spec", specFile.Name()} - wstest := exec.CommandContext(ctx, "wstest", args...) - out, err := wstest.CombinedOutput() - if err != nil { - t.Fatalf("failed to run wstest: %v\nout:\n%s", err, out) + var testCases = []struct { + name string + pongPayload string + ping bool + }{ + { + name: "noPayload", + pongPayload: "", + }, + { + name: "payload", + pongPayload: "hi", + }, + { + name: "pongThenPing", + pongPayload: "hi", + ping: true, + }, + } + for _, tc := range testCases { + tc := tc + run(t, tc.name, func(ctx context.Context, c *websocket.Conn) error { + _, err := c.WriteFrame(ctx, true, websocket.OpPong, []byte(tc.pongPayload)) + if err != nil { + return err + } + if tc.ping { + _, err := c.WriteFrame(ctx, true, websocket.OpPing, []byte("meow")) + if err != nil { + return err + } + err = assertReadFrame(ctx, c, websocket.OpPong, []byte("meow")) + if err != nil { + return err + } + } + return assertCloseHandshake(ctx, c, websocket.StatusNormalClosure, "") + }) + } + }) + run(t, "tenPings", func(ctx context.Context, c *websocket.Conn) error { + ctx = c.CloseRead(ctx) + + for i := 0; i < 10; i++ { + err := c.Ping(ctx) + if err != nil { + return err + } + } + + _, err := c.WriteClose(ctx, websocket.StatusNormalClosure, "") + if err != nil { + return err + } + <-ctx.Done() + + err = c.Ping(context.Background()) + return assertCloseStatus(err, websocket.StatusNormalClosure) + }) + run(t, "tenStreamedPings", func(ctx context.Context, c *websocket.Conn) error { + for i := 0; i < 10; i++ { + err := streamPing(ctx, c, 125) + if err != nil { + return err + } + } + + return assertCloseHandshake(ctx, c, websocket.StatusNormalClosure, "") + }) + }) + +} + +func BenchmarkConn(b *testing.B) { + sizes := []int{ + 2, + 16, + 32, + 512, + 4096, + 16384, } - checkWSTestIndex(t, "./ci/out/wstestServerReports/index.json") + b.Run("write", func(b *testing.B) { + for _, size := range sizes { + b.Run(strconv.Itoa(size), func(b *testing.B) { + b.Run("stream", func(b *testing.B) { + benchConn(b, false, true, size) + }) + b.Run("buffer", func(b *testing.B) { + benchConn(b, false, false, size) + }) + }) + } + }) + + b.Run("echo", func(b *testing.B) { + for _, size := range sizes { + b.Run(strconv.Itoa(size), func(b *testing.B) { + benchConn(b, true, true, size) + }) + } + }) } func echoLoop(ctx context.Context, c *websocket.Conn) { @@ -1080,165 +1300,6 @@ func discardLoop(ctx context.Context, c *websocket.Conn) { } } -func unusedListenAddr() (string, error) { - l, err := net.Listen("tcp", "localhost:0") - if err != nil { - return "", err - } - l.Close() - return l.Addr().String(), nil -} - -// https://github.com/crossbario/autobahn-python/blob/master/wstest/testee_client_aio.py -func TestAutobahnClient(t *testing.T) { - t.Parallel() - if os.Getenv("AUTOBAHN") == "" { - t.Skip("Set $AUTOBAHN to run the autobahn test suite.") - } - - serverAddr, err := unusedListenAddr() - if err != nil { - t.Fatalf("failed to get unused listen addr for wstest: %v", err) - } - - wsServerURL := "ws://" + serverAddr - - spec := map[string]interface{}{ - "url": wsServerURL, - "outdir": "ci/out/wstestClientReports", - "cases": []string{"*"}, - // See TestAutobahnServer for the reasons why we exclude these. - "exclude-cases": []string{"6.*", "7.5.1", "12.*", "13.*"}, - } - specFile, err := ioutil.TempFile("", "websocketFuzzingServer.json") - if err != nil { - t.Fatalf("failed to create temp file for fuzzingserver.json: %v", err) - } - defer specFile.Close() - - e := json.NewEncoder(specFile) - e.SetIndent("", "\t") - err = e.Encode(spec) - if err != nil { - t.Fatalf("failed to write spec: %v", err) - } - - err = specFile.Close() - if err != nil { - t.Fatalf("failed to close file: %v", err) - } - - ctx := context.Background() - ctx, cancel := context.WithTimeout(ctx, time.Minute*10) - defer cancel() - - args := []string{"--mode", "fuzzingserver", "--spec", specFile.Name(), - // Disables some server that runs as part of fuzzingserver mode. - // See https://github.com/crossbario/autobahn-testsuite/blob/058db3a36b7c3a1edf68c282307c6b899ca4857f/autobahntestsuite/autobahntestsuite/wstest.py#L124 - "--webport=0", - } - wstest := exec.CommandContext(ctx, "wstest", args...) - err = wstest.Start() - if err != nil { - t.Fatal(err) - } - defer func() { - err := wstest.Process.Kill() - if err != nil { - t.Error(err) - } - }() - - // Let it come up. - time.Sleep(time.Second * 5) - - var cases int - func() { - c, _, err := websocket.Dial(ctx, wsServerURL+"/getCaseCount", nil) - if err != nil { - t.Fatal(err) - } - defer c.Close(websocket.StatusInternalError, "") - - _, r, err := c.Reader(ctx) - if err != nil { - t.Fatal(err) - } - b, err := ioutil.ReadAll(r) - if err != nil { - t.Fatal(err) - } - cases, err = strconv.Atoi(string(b)) - if err != nil { - t.Fatal(err) - } - - c.Close(websocket.StatusNormalClosure, "") - }() - - for i := 1; i <= cases; i++ { - func() { - ctx, cancel := context.WithTimeout(ctx, time.Second*45) - defer cancel() - - c, _, err := websocket.Dial(ctx, fmt.Sprintf(wsServerURL+"/runCase?case=%v&agent=main", i), nil) - if err != nil { - t.Fatal(err) - } - echoLoop(ctx, c) - }() - } - - c, _, err := websocket.Dial(ctx, fmt.Sprintf(wsServerURL+"/updateReports?agent=main"), nil) - if err != nil { - t.Fatal(err) - } - c.Close(websocket.StatusNormalClosure, "") - - checkWSTestIndex(t, "./ci/out/wstestClientReports/index.json") -} - -func checkWSTestIndex(t *testing.T, path string) { - wstestOut, err := ioutil.ReadFile(path) - if err != nil { - t.Fatalf("failed to read index.json: %v", err) - } - - var indexJSON map[string]map[string]struct { - Behavior string `json:"behavior"` - BehaviorClose string `json:"behaviorClose"` - } - err = json.Unmarshal(wstestOut, &indexJSON) - if err != nil { - t.Fatalf("failed to unmarshal index.json: %v", err) - } - - var failed bool - for _, tests := range indexJSON { - for test, result := range tests { - switch result.Behavior { - case "OK", "NON-STRICT", "INFORMATIONAL": - default: - failed = true - t.Errorf("test %v failed", test) - } - switch result.BehaviorClose { - case "OK", "INFORMATIONAL": - default: - failed = true - t.Errorf("bad close behaviour for test %v", test) - } - } - } - - if failed { - path = strings.Replace(path, ".json", ".html", 1) - if os.Getenv("CI") == "" { - t.Errorf("wstest found failure, please see %q (output as an artifact in CI)", path) - } - } -} - func benchConn(b *testing.B, echo, stream bool, size int) { s, closeFn := testServer(b, func(w http.ResponseWriter, r *http.Request) error { c, err := websocket.Accept(w, r, nil) @@ -1310,38 +1371,6 @@ func benchConn(b *testing.B, echo, stream bool, size int) { c.Close(websocket.StatusNormalClosure, "") } -func BenchmarkConn(b *testing.B) { - sizes := []int{ - 2, - 16, - 32, - 512, - 4096, - 16384, - } - - b.Run("write", func(b *testing.B) { - for _, size := range sizes { - b.Run(strconv.Itoa(size), func(b *testing.B) { - b.Run("stream", func(b *testing.B) { - benchConn(b, false, true, size) - }) - b.Run("buffer", func(b *testing.B) { - benchConn(b, false, false, size) - }) - }) - } - }) - - b.Run("echo", func(b *testing.B) { - for _, size := range sizes { - b.Run(strconv.Itoa(size), func(b *testing.B) { - benchConn(b, true, true, size) - }) - } - }) -} - func assertCloseStatus(err error, code websocket.StatusCode) error { var cerr websocket.CloseError if !xerrors.As(err, &cerr) { @@ -1378,7 +1407,7 @@ func assertSubprotocol(c *websocket.Conn, exp string) error { func assertEqualf(exp, act interface{}, f string, v ...interface{}) error { if diff := cmpDiff(exp, act); diff != "" { - return xerrors.Errorf(f+": %v", append(v, diff)) + return xerrors.Errorf(f+": %v", append(v, diff)...) } return nil } @@ -1405,3 +1434,41 @@ func assertErrorIs(exp, act error) error { } return nil } + +func assertReadFrame(ctx context.Context, c *websocket.Conn, opcode websocket.OpCode, p []byte) error { + actOpcode, actP, err := c.ReadFrame(ctx) + if err != nil { + return err + } + err = assertEqualf(opcode, actOpcode, "unexpected frame opcode with payload %q", p) + if err != nil { + return err + } + return assertEqualf(p, actP, "unexpected frame %v payload", opcode) +} + +func assertCloseHandshake(ctx context.Context, c *websocket.Conn, code websocket.StatusCode, reason string) error { + p, err := c.WriteClose(ctx, code, reason) + if err != nil { + return err + } + return assertReadFrame(ctx, c, websocket.OpClose, p) +} + +func streamPing(ctx context.Context, c *websocket.Conn, l int) error { + err := c.WriteHeader(ctx, true, websocket.OpPing, int64(l)) + if err != nil { + return err + } + for i := 0; i < l; i++ { + err = c.BW().WriteByte(0xFE) + if err != nil { + return err + } + err = c.BW().Flush() + if err != nil { + return err + } + } + return assertReadFrame(ctx, c, websocket.OpPong, bytes.Repeat([]byte{0xFE}, l)) +} From a681f25e4f7f8d447358e6bb2da33e56dd8b943c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 31 Aug 2019 19:05:24 -0500 Subject: [PATCH 094/519] Move benchmarks into separate file --- websocket_bench_test.go | 146 ++++++++++++++++++++++++++++++++++++++++ websocket_test.go | 138 +++---------------------------------- 2 files changed, 155 insertions(+), 129 deletions(-) create mode 100644 websocket_bench_test.go diff --git a/websocket_bench_test.go b/websocket_bench_test.go new file mode 100644 index 00000000..4ad8646c --- /dev/null +++ b/websocket_bench_test.go @@ -0,0 +1,146 @@ +package websocket_test + +import ( + "context" + "io" + "io/ioutil" + "net/http" + "nhooyr.io/websocket" + "strconv" + "strings" + "testing" + "time" +) + + +func BenchmarkConn(b *testing.B) { + sizes := []int{ + 2, + 16, + 32, + 512, + 4096, + 16384, + } + + b.Run("write", func(b *testing.B) { + for _, size := range sizes { + b.Run(strconv.Itoa(size), func(b *testing.B) { + b.Run("stream", func(b *testing.B) { + benchConn(b, false, true, size) + }) + b.Run("buffer", func(b *testing.B) { + benchConn(b, false, false, size) + }) + }) + } + }) + + b.Run("echo", func(b *testing.B) { + for _, size := range sizes { + b.Run(strconv.Itoa(size), func(b *testing.B) { + benchConn(b, true, true, size) + }) + } + }) +} + +func benchConn(b *testing.B, echo, stream bool, size int) { + s, closeFn := testServer(b, func(w http.ResponseWriter, r *http.Request) error { + c, err := websocket.Accept(w, r, nil) + if err != nil { + return err + } + if echo { + echoLoop(r.Context(), c) + } else { + discardLoop(r.Context(), c) + } + return nil + }, false) + defer closeFn() + + wsURL := strings.Replace(s.URL, "http", "ws", 1) + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) + defer cancel() + + c, _, err := websocket.Dial(ctx, wsURL, nil) + if err != nil { + b.Fatal(err) + } + defer c.Close(websocket.StatusInternalError, "") + + msg := []byte(strings.Repeat("2", size)) + readBuf := make([]byte, len(msg)) + b.SetBytes(int64(len(msg))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + if stream { + w, err := c.Writer(ctx, websocket.MessageText) + if err != nil { + b.Fatal(err) + } + + _, err = w.Write(msg) + if err != nil { + b.Fatal(err) + } + + err = w.Close() + if err != nil { + b.Fatal(err) + } + } else { + err = c.Write(ctx, websocket.MessageText, msg) + if err != nil { + b.Fatal(err) + } + } + + if echo { + _, r, err := c.Reader(ctx) + if err != nil { + b.Fatal(err) + } + + _, err = io.ReadFull(r, readBuf) + if err != nil { + b.Fatal(err) + } + } + } + b.StopTimer() + + c.Close(websocket.StatusNormalClosure, "") +} + + +func discardLoop(ctx context.Context, c *websocket.Conn) { + defer c.Close(websocket.StatusInternalError, "") + + ctx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + + b := make([]byte, 32768) + echo := func() error { + _, r, err := c.Reader(ctx) + if err != nil { + return err + } + + _, err = io.CopyBuffer(ioutil.Discard, r, b) + if err != nil { + return err + } + return nil + } + + for { + err := echo() + if err != nil { + return + } + } +} diff --git a/websocket_test.go b/websocket_test.go index 906014ca..732fc94c 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -1092,6 +1092,8 @@ func TestAutobahn(t *testing.T) { // Section 2. t.Run("pingPong", func(t *testing.T) { + t.Parallel() + run(t, "emptyPayload", func(ctx context.Context, c *websocket.Conn) error { ctx = c.CloseRead(ctx) return c.PingWithPayload(ctx, "") @@ -1197,40 +1199,17 @@ func TestAutobahn(t *testing.T) { }) }) -} - -func BenchmarkConn(b *testing.B) { - sizes := []int{ - 2, - 16, - 32, - 512, - 4096, - 16384, - } - - b.Run("write", func(b *testing.B) { - for _, size := range sizes { - b.Run(strconv.Itoa(size), func(b *testing.B) { - b.Run("stream", func(b *testing.B) { - benchConn(b, false, true, size) - }) - b.Run("buffer", func(b *testing.B) { - benchConn(b, false, false, size) - }) - }) - } - }) + // Section 3. + t.Run("reserved", func(t *testing.T) { + t.Parallel() - b.Run("echo", func(b *testing.B) { - for _, size := range sizes { - b.Run(strconv.Itoa(size), func(b *testing.B) { - benchConn(b, true, true, size) - }) - } + run(t, "rsv1", func(ctx context.Context, c *websocket.Conn) error { + c.WriteFrame() + }) }) } + func echoLoop(ctx context.Context, c *websocket.Conn) { defer c.Close(websocket.StatusInternalError, "") @@ -1272,105 +1251,6 @@ func echoLoop(ctx context.Context, c *websocket.Conn) { } } -func discardLoop(ctx context.Context, c *websocket.Conn) { - defer c.Close(websocket.StatusInternalError, "") - - ctx, cancel := context.WithTimeout(ctx, time.Minute) - defer cancel() - - b := make([]byte, 32768) - echo := func() error { - _, r, err := c.Reader(ctx) - if err != nil { - return err - } - - _, err = io.CopyBuffer(ioutil.Discard, r, b) - if err != nil { - return err - } - return nil - } - - for { - err := echo() - if err != nil { - return - } - } -} - -func benchConn(b *testing.B, echo, stream bool, size int) { - s, closeFn := testServer(b, func(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, nil) - if err != nil { - return err - } - if echo { - echoLoop(r.Context(), c) - } else { - discardLoop(r.Context(), c) - } - return nil - }, false) - defer closeFn() - - wsURL := strings.Replace(s.URL, "http", "ws", 1) - - ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) - defer cancel() - - c, _, err := websocket.Dial(ctx, wsURL, nil) - if err != nil { - b.Fatal(err) - } - defer c.Close(websocket.StatusInternalError, "") - - msg := []byte(strings.Repeat("2", size)) - readBuf := make([]byte, len(msg)) - b.SetBytes(int64(len(msg))) - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - if stream { - w, err := c.Writer(ctx, websocket.MessageText) - if err != nil { - b.Fatal(err) - } - - _, err = w.Write(msg) - if err != nil { - b.Fatal(err) - } - - err = w.Close() - if err != nil { - b.Fatal(err) - } - } else { - err = c.Write(ctx, websocket.MessageText, msg) - if err != nil { - b.Fatal(err) - } - } - - if echo { - _, r, err := c.Reader(ctx) - if err != nil { - b.Fatal(err) - } - - _, err = io.ReadFull(r, readBuf) - if err != nil { - b.Fatal(err) - } - } - } - b.StopTimer() - - c.Close(websocket.StatusNormalClosure, "") -} - func assertCloseStatus(err error, code websocket.StatusCode) error { var cerr websocket.CloseError if !xerrors.As(err, &cerr) { From 12d7f1acc28e859be846e7b6ed15066c1259df2b Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 31 Aug 2019 23:48:56 -0500 Subject: [PATCH 095/519] Translate the remaining useful Autobahn python tests --- ci/test.sh | 3 +- export_test.go | 34 +- websocket_bench_test.go | 5 +- websocket_test.go | 729 +++++++++++++++++++++++++++++++++++++++- 4 files changed, 748 insertions(+), 23 deletions(-) diff --git a/ci/test.sh b/ci/test.sh index 3c476d93..c8b8ec19 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -12,14 +12,13 @@ argv=( -- "-vet=off" ) -# Interactive usage does not want to turn off vet or use gotestsum by default. +# Interactive usage does not want to turn off vet or use gotestsum. if [[ $# -gt 0 ]]; then argv=(go test "$@") fi # We always want coverage and race detection. argv+=( - -race "-coverprofile=ci/out/coverage.prof" "-coverpkg=./..." ) diff --git a/export_test.go b/export_test.go index fb3cf81f..811bf800 100644 --- a/export_test.go +++ b/export_test.go @@ -15,6 +15,7 @@ type ( const ( OpClose = OpCode(opClose) OpBinary = OpCode(opBinary) + OpText = OpCode(opText) OpPing = OpCode(opPing) OpPong = OpCode(opPong) OpContinuation = OpCode(opContinuation) @@ -40,17 +41,38 @@ func (c *Conn) WriteFrame(ctx context.Context, fin bool, opc OpCode, p []byte) ( return c.writeFrame(ctx, fin, opcode(opc), p) } -func (c *Conn) WriteHeader(ctx context.Context, fin bool, opc OpCode, lenp int64) error { +// header represents a WebSocket frame header. +// See https://tools.ietf.org/html/rfc6455#section-5.2 +type Header struct { + Fin bool + Rsv1 bool + Rsv2 bool + Rsv3 bool + OpCode OpCode + + PayloadLength int64 +} + +func (c *Conn) WriteHeader(ctx context.Context, h Header) error { headerBytes := writeHeader(c.writeHeaderBuf, header{ - fin: fin, - opcode: opcode(opc), - payloadLength: lenp, + fin: h.Fin, + rsv1: h.Rsv1, + rsv2: h.Rsv2, + rsv3: h.Rsv3, + opcode: opcode(h.OpCode), + payloadLength: h.PayloadLength, masked: c.client, }) _, err := c.bw.Write(headerBytes) if err != nil { return xerrors.Errorf("failed to write header: %w", err) } + if h.Fin { + err = c.Flush() + if err != nil { + return err + } + } return nil } @@ -96,3 +118,7 @@ func (c *Conn) WriteClose(ctx context.Context, code StatusCode, reason string) ( } return b, nil } + +func ParseClosePayload(p []byte) (CloseError, error) { + return parseClosePayload(p) +} diff --git a/websocket_bench_test.go b/websocket_bench_test.go index 4ad8646c..6a54fab2 100644 --- a/websocket_bench_test.go +++ b/websocket_bench_test.go @@ -5,13 +5,13 @@ import ( "io" "io/ioutil" "net/http" - "nhooyr.io/websocket" "strconv" "strings" "testing" "time" -) + "nhooyr.io/websocket" +) func BenchmarkConn(b *testing.B) { sizes := []int{ @@ -116,7 +116,6 @@ func benchConn(b *testing.B, echo, stream bool, size int) { c.Close(websocket.StatusNormalClosure, "") } - func discardLoop(ctx context.Context, c *websocket.Conn) { defer c.Close(websocket.StatusInternalError, "") diff --git a/websocket_test.go b/websocket_test.go index 732fc94c..3482cbde 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -3,6 +3,7 @@ package websocket_test import ( "bytes" "context" + "encoding/binary" "encoding/json" "fmt" "io" @@ -919,7 +920,7 @@ func testServer(tb testing.TB, fn func(w http.ResponseWriter, r *http.Request) e atomic.AddInt64(&conns, 1) defer atomic.AddInt64(&conns, -1) - ctx, cancel := context.WithTimeout(r.Context(), time.Second*10) + ctx, cancel := context.WithTimeout(r.Context(), time.Minute) defer cancel() r = r.WithContext(ctx) @@ -953,8 +954,6 @@ func TestAutobahn(t *testing.T) { run := func(t *testing.T, name string, fn func(ctx context.Context, c *websocket.Conn) error) { run2 := func(t *testing.T, testingClient bool) { - t.Parallel() - // Run random tests over TLS. tls := rand.Intn(2) == 1 @@ -985,7 +984,7 @@ func TestAutobahn(t *testing.T) { wsURL := strings.Replace(s.URL, "http", "ws", 1) - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() opts := &websocket.DialOptions{ @@ -1017,9 +1016,11 @@ func TestAutobahn(t *testing.T) { t.Parallel() t.Run("server", func(t *testing.T) { + t.Parallel() run2(t, false) }) t.Run("client", func(t *testing.T) { + t.Parallel() run2(t, true) }) }) @@ -1043,8 +1044,7 @@ func TestAutobahn(t *testing.T) { for i, l := range lengths { l := l run(t, fmt.Sprintf("%v/%v", typ, l), func(ctx context.Context, c *websocket.Conn) error { - p := make([]byte, l) - rand.Read(p) + p := randBytes(l) if i == len(lengths)-1 { w, err := c.Writer(ctx, typ) if err != nil { @@ -1119,7 +1119,7 @@ func TestAutobahn(t *testing.T) { return assertCloseStatus(err, websocket.StatusProtocolError) }) run(t, "streamPingPayload", func(ctx context.Context, c *websocket.Conn) error { - err := streamPing(ctx, c, 125) + err := assertStreamPing(ctx, c, 125) if err != nil { return err } @@ -1189,7 +1189,7 @@ func TestAutobahn(t *testing.T) { }) run(t, "tenStreamedPings", func(ctx context.Context, c *websocket.Conn) error { for i := 0; i < 10; i++ { - err := streamPing(ctx, c, 125) + err := assertStreamPing(ctx, c, 125) if err != nil { return err } @@ -1200,15 +1200,659 @@ func TestAutobahn(t *testing.T) { }) // Section 3. + // We skip the per octet sending as it will add too much complexity. t.Run("reserved", func(t *testing.T) { t.Parallel() - run(t, "rsv1", func(ctx context.Context, c *websocket.Conn) error { - c.WriteFrame() + var testCases = []struct { + name string + header websocket.Header + }{ + { + name: "rsv1", + header: websocket.Header{ + Fin: true, + Rsv1: true, + OpCode: websocket.OpClose, + PayloadLength: 0, + }, + }, + { + name: "rsv2", + header: websocket.Header{ + Fin: true, + Rsv2: true, + OpCode: websocket.OpPong, + PayloadLength: 0, + }, + }, + { + name: "rsv3", + header: websocket.Header{ + Fin: true, + Rsv3: true, + OpCode: websocket.OpBinary, + PayloadLength: 0, + }, + }, + { + name: "rsvAll", + header: websocket.Header{ + Fin: true, + Rsv1: true, + Rsv2: true, + Rsv3: true, + OpCode: websocket.OpText, + PayloadLength: 0, + }, + }, + } + for _, tc := range testCases { + tc := tc + run(t, tc.name, func(ctx context.Context, c *websocket.Conn) error { + err := assertEcho(ctx, c, websocket.MessageText, 4096) + if err != nil { + return err + } + err = c.WriteHeader(ctx, tc.header) + if err != nil { + return err + } + err = c.Flush() + if err != nil { + return err + } + _, err = c.WriteFrame(ctx, true, websocket.OpPing, []byte("wtf")) + if err != nil { + return err + } + return assertReadCloseFrame(ctx, c, websocket.StatusProtocolError) + }) + } + }) + + // Section 4. + t.Run("opcodes", func(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + opcode websocket.OpCode + payload bool + echo bool + ping bool + }{ + // Section 1. + { + name: "3", + opcode: 3, + }, + { + name: "4", + opcode: 4, + payload: true, + }, + { + name: "5", + opcode: 5, + echo: true, + ping: true, + }, + { + name: "6", + opcode: 6, + payload: true, + echo: true, + ping: true, + }, + { + name: "7", + opcode: 7, + payload: true, + echo: true, + ping: true, + }, + + // Section 2. + { + name: "11", + opcode: 11, + }, + { + name: "12", + opcode: 12, + payload: true, + }, + { + name: "13", + opcode: 13, + payload: true, + echo: true, + ping: true, + }, + { + name: "14", + opcode: 14, + payload: true, + echo: true, + ping: true, + }, + { + name: "15", + opcode: 15, + payload: true, + echo: true, + ping: true, + }, + } + for _, tc := range testCases { + tc := tc + run(t, tc.name, func(ctx context.Context, c *websocket.Conn) error { + if tc.echo { + err := assertEcho(ctx, c, websocket.MessageText, 4096) + if err != nil { + return err + } + } + + p := []byte(nil) + if tc.payload { + p = randBytes(rand.Intn(4096) + 1) + } + _, err := c.WriteFrame(ctx, true, tc.opcode, p) + if err != nil { + return err + } + if tc.ping { + _, err = c.WriteFrame(ctx, true, websocket.OpPing, []byte("wtf")) + if err != nil { + return err + } + } + return assertReadCloseFrame(ctx, c, websocket.StatusProtocolError) + }) + } + }) + + // Section 5. + t.Run("fragmentation", func(t *testing.T) { + t.Parallel() + + // 5.1 to 5.8 + testCases := []struct { + name string + opcode websocket.OpCode + success bool + pingInBetween bool + }{ + { + name: "ping", + opcode: websocket.OpPing, + success: false, + }, + { + name: "pong", + opcode: websocket.OpPong, + success: false, + }, + { + name: "text", + opcode: websocket.OpText, + success: true, + }, + { + name: "textPing", + opcode: websocket.OpText, + success: true, + pingInBetween: true, + }, + } + for _, tc := range testCases { + tc := tc + run(t, tc.name, func(ctx context.Context, c *websocket.Conn) error { + p1 := randBytes(16) + _, err := c.WriteFrame(ctx, false, tc.opcode, p1) + if err != nil { + return err + } + err = c.BW().Flush() + if err != nil { + return err + } + if !tc.success { + _, _, err = c.Read(ctx) + return assertCloseStatus(err, websocket.StatusProtocolError) + } + + if tc.pingInBetween { + _, err = c.WriteFrame(ctx, true, websocket.OpPing, p1) + if err != nil { + return err + } + } + + p2 := randBytes(16) + _, err = c.WriteFrame(ctx, true, websocket.OpContinuation, p2) + if err != nil { + return err + } + + err = assertReadFrame(ctx, c, tc.opcode, p1) + if err != nil { + return err + } + + if tc.pingInBetween { + err = assertReadFrame(ctx, c, websocket.OpPong, p1) + if err != nil { + return err + } + } + + return assertReadFrame(ctx, c, websocket.OpContinuation, p2) + }) + } + + t.Run("unexpectedContinuation", func(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + fin bool + textFirst bool + }{ + { + name: "fin", + fin: true, + }, + { + name: "noFin", + fin: false, + }, + { + name: "echoFirst", + fin: false, + textFirst: true, + }, + // The rest of the tests in this section get complicated and do not inspire much confidence. + } + + for _, tc := range testCases { + tc := tc + run(t, tc.name, func(ctx context.Context, c *websocket.Conn) error { + if tc.textFirst { + w, err := c.Writer(ctx, websocket.MessageText) + if err != nil { + return err + } + p1 := randBytes(32) + _, err = w.Write(p1) + if err != nil { + return err + } + p2 := randBytes(32) + _, err = w.Write(p2) + if err != nil { + return err + } + err = w.Close() + if err != nil { + return err + } + err = assertReadFrame(ctx, c, websocket.OpText, p1) + if err != nil { + return err + } + err = assertReadFrame(ctx, c, websocket.OpContinuation, p2) + if err != nil { + return err + } + err = assertReadFrame(ctx, c, websocket.OpContinuation, []byte{}) + if err != nil { + return err + } + } + + _, err := c.WriteFrame(ctx, tc.fin, websocket.OpContinuation, randBytes(32)) + if err != nil { + return err + } + err = c.BW().Flush() + if err != nil { + return err + } + + return assertReadCloseFrame(ctx, c, websocket.StatusProtocolError) + }) + } + + run(t, "doubleText", func(ctx context.Context, c *websocket.Conn) error { + p1 := randBytes(32) + _, err := c.WriteFrame(ctx, false, websocket.OpText, p1) + if err != nil { + return err + } + _, err = c.WriteFrame(ctx, true, websocket.OpText, randBytes(32)) + if err != nil { + return err + } + err = assertReadFrame(ctx, c, websocket.OpText, p1) + if err != nil { + return err + } + return assertReadCloseFrame(ctx, c, websocket.StatusProtocolError) + }) + + run(t, "5.19", func(ctx context.Context, c *websocket.Conn) error { + p1 := randBytes(32) + p2 := randBytes(32) + p3 := randBytes(32) + p4 := randBytes(32) + p5 := randBytes(32) + + _, err := c.WriteFrame(ctx, false, websocket.OpText, p1) + if err != nil { + return err + } + _, err = c.WriteFrame(ctx, false, websocket.OpContinuation, p2) + if err != nil { + return err + } + + _, err = c.WriteFrame(ctx, true, websocket.OpPing, p1) + if err != nil { + return err + } + + time.Sleep(time.Second) + + _, err = c.WriteFrame(ctx, false, websocket.OpContinuation, p3) + if err != nil { + return err + } + _, err = c.WriteFrame(ctx, false, websocket.OpContinuation, p4) + if err != nil { + return err + } + + _, err = c.WriteFrame(ctx, true, websocket.OpPing, p1) + if err != nil { + return err + } + + _, err = c.WriteFrame(ctx, true, websocket.OpContinuation, p5) + if err != nil { + return err + } + + err = assertReadFrame(ctx, c, websocket.OpText, p1) + if err != nil { + return err + } + err = assertReadFrame(ctx, c, websocket.OpContinuation, p2) + if err != nil { + return err + } + err = assertReadFrame(ctx, c, websocket.OpPong, p1) + if err != nil { + return err + } + err = assertReadFrame(ctx, c, websocket.OpContinuation, p3) + if err != nil { + return err + } + err = assertReadFrame(ctx, c, websocket.OpContinuation, p4) + if err != nil { + return err + } + err = assertReadFrame(ctx, c, websocket.OpPong, p1) + if err != nil { + return err + } + err = assertReadFrame(ctx, c, websocket.OpContinuation, p5) + if err != nil { + return err + } + err = assertReadFrame(ctx, c, websocket.OpContinuation, []byte{}) + if err != nil { + return err + } + return assertCloseHandshake(ctx, c, websocket.StatusNormalClosure, "") + }) + }) + }) + + // Section 7 + t.Run("closeHandling", func(t *testing.T) { + t.Parallel() + + // 1.1 - 1.4 is useless. + run(t, "1.5", func(ctx context.Context, c *websocket.Conn) error { + p1 := randBytes(32) + _, err := c.WriteFrame(ctx, false, websocket.OpText, p1) + if err != nil { + return err + } + err = c.Flush() + if err != nil { + return err + } + _, err = c.WriteClose(ctx, websocket.StatusNormalClosure, "") + if err != nil { + return err + } + err = assertReadFrame(ctx, c, websocket.OpText, p1) + if err != nil { + return err + } + return assertReadCloseFrame(ctx, c, websocket.StatusNormalClosure) + }) + + run(t, "1.6", func(ctx context.Context, c *websocket.Conn) error { + // 262144 bytes. + p1 := randBytes(1 << 18) + err := c.Write(ctx, websocket.MessageText, p1) + if err != nil { + return err + } + _, err = c.WriteClose(ctx, websocket.StatusNormalClosure, "") + if err != nil { + return err + } + err = assertReadMessage(ctx, c, websocket.MessageText, p1) + if err != nil { + return err + } + return assertReadCloseFrame(ctx, c, websocket.StatusNormalClosure) + }) + + run(t, "emptyClose", func(ctx context.Context, c *websocket.Conn) error { + _, err := c.WriteFrame(ctx, true, websocket.OpClose, nil) + if err != nil { + return err + } + return assertReadFrame(ctx, c, websocket.OpClose, []byte{}) + }) + + run(t, "badClose", func(ctx context.Context, c *websocket.Conn) error { + _, err := c.WriteFrame(ctx, true, websocket.OpClose, []byte{1}) + if err != nil { + return err + } + return assertReadCloseFrame(ctx, c, websocket.StatusProtocolError) + }) + + run(t, "noReason", func(ctx context.Context, c *websocket.Conn) error { + return assertCloseHandshake(ctx, c, websocket.StatusNormalClosure, "") + }) + + run(t, "simpleReason", func(ctx context.Context, c *websocket.Conn) error { + return assertCloseHandshake(ctx, c, websocket.StatusNormalClosure, randString(16)) + }) + + run(t, "maxReason", func(ctx context.Context, c *websocket.Conn) error { + return assertCloseHandshake(ctx, c, websocket.StatusNormalClosure, randString(123)) + }) + + run(t, "tooBigReason", func(ctx context.Context, c *websocket.Conn) error { + _, err := c.WriteFrame(ctx, true, websocket.OpClose, + append([]byte{0x03, 0xE8}, randString(124)...), + ) + if err != nil { + return err + } + return assertReadCloseFrame(ctx, c, websocket.StatusProtocolError) + }) + + t.Run("validCloses", func(t *testing.T) { + t.Parallel() + + codes := [...]websocket.StatusCode{ + 1000, + 1001, + 1002, + 1003, + 1007, + 1008, + 1009, + 1010, + 1011, + 3000, + 3999, + 4000, + 4999, + } + for _, code := range codes { + run(t, strconv.Itoa(int(code)), func(ctx context.Context, c *websocket.Conn) error { + return assertCloseHandshake(ctx, c, code, randString(32)) + }) + } + }) + + t.Run("invalidCloseCodes", func(t *testing.T) { + t.Parallel() + + codes := []websocket.StatusCode{ + 0, + 999, + 1004, + 1005, + 1006, + 1016, + 1100, + 2000, + 2999, + 5000, + 65535, + } + for _, code := range codes { + run(t, strconv.Itoa(int(code)), func(ctx context.Context, c *websocket.Conn) error { + p := make([]byte, 2) + binary.BigEndian.PutUint16(p, uint16(code)) + p = append(p, randBytes(32)...) + _, err := c.WriteFrame(ctx, true, websocket.OpClose, p) + if err != nil { + return err + } + return assertReadCloseFrame(ctx, c, websocket.StatusProtocolError) + }) + } }) }) -} + // Section 9. + t.Run("limits", func(t *testing.T) { + t.Parallel() + + t.Run("unfragmentedEcho", func(t *testing.T) { + t.Parallel() + + lengths := []int{ + 1 << 16, // 65536 + 1 << 18, // 262144 + // Anything higher is completely unnecessary. + } + + for _, l := range lengths { + l := l + run(t, strconv.Itoa(l), func(ctx context.Context, c *websocket.Conn) error { + return assertEcho(ctx, c, websocket.MessageBinary, l) + }) + } + }) + + t.Run("fragmentedEcho", func(t *testing.T) { + t.Parallel() + + fragments := []int{ + 64, + 256, + 1 << 10, + 1 << 12, + 1 << 14, + 1 << 16, + 1 << 18, + } + + for _, l := range fragments { + fragmentLength := l + run(t, strconv.Itoa(fragmentLength), func(ctx context.Context, c *websocket.Conn) error { + w, err := c.Writer(ctx, websocket.MessageText) + if err != nil { + return err + } + b := randBytes(1 << 18) + for i := 0; i < len(b); { + j := i + fragmentLength + if j > len(b) { + j = len(b) + } + + _, err = w.Write(b[i:j]) + if err != nil { + return err + } + + i = j + } + err = w.Close() + if err != nil { + return err + } + + err = assertReadMessage(ctx, c, websocket.MessageText, b) + if err != nil { + return err + } + return assertCloseHandshake(ctx, c, websocket.StatusNormalClosure, "") + }) + } + }) + + t.Run("latencyEcho", func(t *testing.T) { + t.Parallel() + + lengths := []int{ + 0, + 16, + 64, + } + + for _, l := range lengths { + l := l + run(t, strconv.Itoa(l), func(ctx context.Context, c *websocket.Conn) error { + for i := 0; i < 1000; i++ { + err := assertEcho(ctx, c, websocket.MessageBinary, l) + if err != nil { + return err + } + } + return nil + }) + } + }) + }) +} func echoLoop(ctx context.Context, c *websocket.Conn) { defer c.Close(websocket.StatusInternalError, "") @@ -1269,6 +1913,31 @@ func assertJSONRead(ctx context.Context, c *websocket.Conn, exp interface{}) (er return assertEqualf(exp, act, "unexpected JSON") } +func randBytes(n int) []byte { + return make([]byte, n) +} + +func randString(n int) string { + return string(randBytes(n)) +} + +func assertEcho(ctx context.Context, c *websocket.Conn, typ websocket.MessageType, n int) error { + p := randBytes(n) + err := c.Write(ctx, typ, p) + if err != nil { + return err + } + typ2, p2, err := c.Read(ctx) + if err != nil { + return err + } + err = assertEqualf(typ, typ2, "unexpected data type") + if err != nil { + return err + } + return assertEqualf(p, p2, "unexpected payload") +} + func assertProtobufRead(ctx context.Context, c *websocket.Conn, exp interface{}) error { expType := reflect.TypeOf(exp) actv := reflect.New(expType.Elem()) @@ -1320,13 +1989,29 @@ func assertReadFrame(ctx context.Context, c *websocket.Conn, opcode websocket.Op if err != nil { return err } - err = assertEqualf(opcode, actOpcode, "unexpected frame opcode with payload %q", p) + err = assertEqualf(opcode, actOpcode, "unexpected frame opcode with payload %q", actP) if err != nil { return err } return assertEqualf(p, actP, "unexpected frame %v payload", opcode) } +func assertReadCloseFrame(ctx context.Context, c *websocket.Conn, code websocket.StatusCode) error { + actOpcode, actP, err := c.ReadFrame(ctx) + if err != nil { + return err + } + err = assertEqualf(websocket.OpClose, actOpcode, "unexpected frame opcode with payload %q", actP) + if err != nil { + return err + } + ce, err := websocket.ParseClosePayload(actP) + if err != nil { + return xerrors.Errorf("failed to parse close frame payload: %w", err) + } + return assertEqualf(ce.Code, code, "unexpected frame close frame code with payload %q", actP) +} + func assertCloseHandshake(ctx context.Context, c *websocket.Conn, code websocket.StatusCode, reason string) error { p, err := c.WriteClose(ctx, code, reason) if err != nil { @@ -1335,8 +2020,12 @@ func assertCloseHandshake(ctx context.Context, c *websocket.Conn, code websocket return assertReadFrame(ctx, c, websocket.OpClose, p) } -func streamPing(ctx context.Context, c *websocket.Conn, l int) error { - err := c.WriteHeader(ctx, true, websocket.OpPing, int64(l)) +func assertStreamPing(ctx context.Context, c *websocket.Conn, l int) error { + err := c.WriteHeader(ctx, websocket.Header{ + Fin: true, + OpCode: websocket.OpPing, + PayloadLength: int64(l), + }) if err != nil { return err } @@ -1352,3 +2041,15 @@ func streamPing(ctx context.Context, c *websocket.Conn, l int) error { } return assertReadFrame(ctx, c, websocket.OpPong, bytes.Repeat([]byte{0xFE}, l)) } + +func assertReadMessage(ctx context.Context, c *websocket.Conn, typ websocket.MessageType, p []byte) error { + actTyp, actP, err := c.Read(ctx) + if err != nil { + return err + } + err = assertEqualf(websocket.MessageText, actTyp, "unexpected frame opcode with payload %q", actP) + if err != nil { + return err + } + return assertEqualf(p, actP, "unexpected frame %v payload", actTyp) +} From 016b71680ad8e2824cf4a98a84bf3d6a60a833cc Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 1 Sep 2019 00:02:37 -0500 Subject: [PATCH 096/519] Improve test speed --- ci/test.sh | 1 + websocket_test.go | 8 +++----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/ci/test.sh b/ci/test.sh index c8b8ec19..7b611921 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -19,6 +19,7 @@ fi # We always want coverage and race detection. argv+=( + "-parallel=512" "-coverprofile=ci/out/coverage.prof" "-coverpkg=./..." ) diff --git a/websocket_test.go b/websocket_test.go index 3482cbde..27750bca 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -1768,8 +1768,8 @@ func TestAutobahn(t *testing.T) { t.Parallel() lengths := []int{ - 1 << 16, // 65536 - 1 << 18, // 262144 + 1 << 16, + 1 << 18, // Anything higher is completely unnecessary. } @@ -1791,7 +1791,6 @@ func TestAutobahn(t *testing.T) { 1 << 12, 1 << 14, 1 << 16, - 1 << 18, } for _, l := range fragments { @@ -1801,7 +1800,7 @@ func TestAutobahn(t *testing.T) { if err != nil { return err } - b := randBytes(1 << 18) + b := randBytes(1 << 16) for i := 0; i < len(b); { j := i + fragmentLength if j > len(b) { @@ -1835,7 +1834,6 @@ func TestAutobahn(t *testing.T) { lengths := []int{ 0, 16, - 64, } for _, l := range lengths { From 31b47c392a0537e99360fed8e59bf208ee539953 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 1 Sep 2019 00:43:51 -0500 Subject: [PATCH 097/519] Modify NetConn to take a context as the first argument Closes #125 Minor API change, will need to document clearly in release notes. --- netconn.go | 13 ++++++++----- websocket_test.go | 8 ++++---- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/netconn.go b/netconn.go index a6f902da..a7c9bf7f 100644 --- a/netconn.go +++ b/netconn.go @@ -21,8 +21,11 @@ import ( // Every Write to the net.Conn will correspond to a message write of // the given type on *websocket.Conn. // -// If a message is read that is not of the correct type, an error -// will be thrown. +// The passed ctx bounds the lifetime of the net.Conn. If cancelled, +// all reads and writes on the net.Conn will be cancelled. +// +// If a message is read that is not of the correct type, the connection +// will be closed with StatusUnsupportedData and an error will be returned. // // Close will close the *websocket.Conn with StatusNormalClosure. // @@ -35,20 +38,20 @@ import ( // // A received StatusNormalClosure or StatusGoingAway close frame will be translated to // io.EOF when reading. -func NetConn(c *Conn, msgType MessageType) net.Conn { +func NetConn(ctx context.Context, c *Conn, msgType MessageType) net.Conn { nc := &netConn{ c: c, msgType: msgType, } var cancel context.CancelFunc - nc.writeContext, cancel = context.WithCancel(context.Background()) + nc.writeContext, cancel = context.WithCancel(ctx) nc.writeTimer = time.AfterFunc(math.MaxInt64, cancel) if !nc.writeTimer.Stop() { <-nc.writeTimer.C } - nc.readContext, cancel = context.WithCancel(context.Background()) + nc.readContext, cancel = context.WithCancel(ctx) nc.readTimer = time.AfterFunc(math.MaxInt64, cancel) if !nc.readTimer.Stop() { <-nc.readTimer.C diff --git a/websocket_test.go b/websocket_test.go index 27750bca..979b092c 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -264,7 +264,7 @@ func TestConn(t *testing.T) { { name: "netConn", server: func(ctx context.Context, c *websocket.Conn) error { - nc := websocket.NetConn(c, websocket.MessageBinary) + nc := websocket.NetConn(ctx, c, websocket.MessageBinary) defer nc.Close() nc.SetWriteDeadline(time.Time{}) @@ -290,7 +290,7 @@ func TestConn(t *testing.T) { return nil }, client: func(ctx context.Context, c *websocket.Conn) error { - nc := websocket.NetConn(c, websocket.MessageBinary) + nc := websocket.NetConn(ctx, c, websocket.MessageBinary) nc.SetReadDeadline(time.Time{}) time.Sleep(1) @@ -317,7 +317,7 @@ func TestConn(t *testing.T) { { name: "netConn/badReadMsgType", server: func(ctx context.Context, c *websocket.Conn) error { - nc := websocket.NetConn(c, websocket.MessageBinary) + nc := websocket.NetConn(ctx, c, websocket.MessageBinary) nc.SetDeadline(time.Now().Add(time.Second * 15)) @@ -337,7 +337,7 @@ func TestConn(t *testing.T) { { name: "netConn/badRead", server: func(ctx context.Context, c *websocket.Conn) error { - nc := websocket.NetConn(c, websocket.MessageBinary) + nc := websocket.NetConn(ctx, c, websocket.MessageBinary) defer nc.Close() nc.SetDeadline(time.Now().Add(time.Second * 15)) From 3c34db4c5ef03d4c27f3f2d188ef18d0396d3d2a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 1 Sep 2019 01:06:07 -0500 Subject: [PATCH 098/519] Small fixes --- websocket_autobahn_python_test.go | 3 +++ websocket_test.go | 15 +++++---------- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/websocket_autobahn_python_test.go b/websocket_autobahn_python_test.go index 32ee1f5c..0512c85a 100644 --- a/websocket_autobahn_python_test.go +++ b/websocket_autobahn_python_test.go @@ -1,3 +1,6 @@ +// This file contains the old autobahn test suite tests that use the +// python binary. The approach is very clunky and slow so new tests +// have been written in pure Go in websocket_test.go. // +build autobahn-python package websocket_test diff --git a/websocket_test.go b/websocket_test.go index 979b092c..ef6ae926 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -47,10 +47,7 @@ func TestHandshake(t *testing.T) { c.Close(websocket.StatusInternalError, "") return xerrors.New("expected error regarding bad origin") } - if !strings.Contains(err.Error(), "not authorized") { - return xerrors.Errorf("expected error regarding bad origin: %+v", err) - } - return nil + return assertErrorContains(err, "not authorized") }, client: func(ctx context.Context, u string) error { h := http.Header{} @@ -62,10 +59,7 @@ func TestHandshake(t *testing.T) { c.Close(websocket.StatusInternalError, "") return xerrors.New("expected handshake failure") } - if !strings.Contains(err.Error(), "403") { - return xerrors.Errorf("expected handshake failure: %+v", err) - } - return nil + return assertErrorContains(err, "403") }, }, { @@ -123,8 +117,9 @@ func TestHandshake(t *testing.T) { if err != nil { return xerrors.Errorf("request is missing mycookie: %w", err) } - if cookie.Value != "myvalue" { - return xerrors.Errorf("expected %q but got %q", "myvalue", cookie.Value) + err = assertEqualf("myvalue", cookie.Value, "unexpected cookie value") + if err != nil { + return err } c, err := websocket.Accept(w, r, nil) if err != nil { From 522ff139316e6143d18a4e1babfdb488987b6321 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 1 Sep 2019 12:17:36 -0500 Subject: [PATCH 099/519] Add CI status to README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f7c9fe4f..212aaddc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # websocket [![GoDoc](https://godoc.org/nhooyr.io/websocket?status.svg)](https://godoc.org/nhooyr.io/websocket) +[![CI](https://img.shields.io/circleci/build/github/nhooyr/websocket)](https://github.com/nhooyr/websocket/commits/master) [![Codecov](https://img.shields.io/codecov/c/github/nhooyr/websocket.svg?color=brightgreen)](https://codecov.io/gh/nhooyr/websocket) websocket is a minimal and idiomatic WebSocket library for Go. From fbc3f18753b7d64c8aa576a9a0cefbb41c163c63 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 1 Sep 2019 12:18:56 -0500 Subject: [PATCH 100/519] Improve CI badge --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 212aaddc..f7d37445 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # websocket [![GoDoc](https://godoc.org/nhooyr.io/websocket?status.svg)](https://godoc.org/nhooyr.io/websocket) -[![CI](https://img.shields.io/circleci/build/github/nhooyr/websocket)](https://github.com/nhooyr/websocket/commits/master) +[![CI](https://img.shields.io/circleci/build/github/nhooyr/websocket?label=ci)](https://github.com/nhooyr/websocket/commits/master) [![Codecov](https://img.shields.io/codecov/c/github/nhooyr/websocket.svg?color=brightgreen)](https://codecov.io/gh/nhooyr/websocket) websocket is a minimal and idiomatic WebSocket library for Go. From a443b78ac7fe25cae27cf870e4981e6fb914b855 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 1 Sep 2019 12:22:12 -0500 Subject: [PATCH 101/519] Specify canonical import path for godoc --- doc.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc.go b/doc.go index 6ee4166a..d715a052 100644 --- a/doc.go +++ b/doc.go @@ -14,4 +14,4 @@ // comparison with existing implementations. // // Please be sure to use the https://golang.org/x/xerrors package when inspecting returned errors. -package websocket +package websocket // import "nhooyr.io/websocket" From 40cae1b143b1c2a9c51d123d59428894cbe5bbfd Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 1 Sep 2019 12:24:05 -0500 Subject: [PATCH 102/519] Specify canonical import path for subpackages --- wsjson/wsjson.go | 2 +- wspb/wspb.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/wsjson/wsjson.go b/wsjson/wsjson.go index b72d562f..fda429ac 100644 --- a/wsjson/wsjson.go +++ b/wsjson/wsjson.go @@ -1,5 +1,5 @@ // Package wsjson provides websocket helpers for JSON messages. -package wsjson +package wsjson // import "nhooyr.io/websocket/wsjson" import ( "context" diff --git a/wspb/wspb.go b/wspb/wspb.go index 56b14ee8..dfc407cc 100644 --- a/wspb/wspb.go +++ b/wspb/wspb.go @@ -1,5 +1,5 @@ // Package wspb provides websocket helpers for protobuf messages. -package wspb +package wspb // import "nhooyr.io/websocket/wspb" import ( "bytes" From 8fb446659253544f115fbe21db0358e2544c3698 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 2 Sep 2019 00:15:44 -0500 Subject: [PATCH 103/519] Update xerrors link in docs --- statuscode.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/statuscode.go b/statuscode.go index 498437d0..558610d4 100644 --- a/statuscode.go +++ b/statuscode.go @@ -43,7 +43,7 @@ const ( // CloseError represents a WebSocket close frame. // It is returned by Conn's methods when a WebSocket close frame is received from // the peer. -// You will need to use https://golang.org/x/xerrors to check for this error. +// You will need to use https://godoc.org/golang.org/x/xerrors#As to check for this error. type CloseError struct { Code StatusCode Reason string From 1f053d98782d3407c72abd87e6eeeb45a21388fc Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 2 Sep 2019 13:55:28 -0500 Subject: [PATCH 104/519] Bump version in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f7d37445..1767c3aa 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ websocket is a minimal and idiomatic WebSocket library for Go. ## Install ```bash -go get nhooyr.io/websocket@v1.4.0 +go get nhooyr.io/websocket@v1.5.0 ``` ## Features From 1d635994126a3467be117acba9c36205e1cfc4ad Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 2 Sep 2019 14:14:45 -0500 Subject: [PATCH 105/519] Fix grammar in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1767c3aa..f2fbddd6 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,7 @@ As mentioned above, nhooyr/websocket also supports concurrent writers. The only performance con to nhooyr/websocket is that uses one extra goroutine to support cancellation with context.Context. This costs 2 KB of memory which is cheap compared to -simplicity benefits. +the benefits. ### x/net/websocket From 2d41f39727d868d6d577a8705432f4563fb00a41 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 2 Sep 2019 17:28:02 -0500 Subject: [PATCH 106/519] Add a CloseError example Closes #128 --- README.md | 2 ++ example_test.go | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/README.md b/README.md index f2fbddd6..9924fb54 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,8 @@ go get nhooyr.io/websocket@v1.5.0 For a production quality example that shows off the full API, see the [echo example on the godoc](https://godoc.org/nhooyr.io/websocket#example-package--Echo). On github, the example is at [example_echo_test.go](./example_echo_test.go). +Please use the [golang.org/x/xerrors](https://godoc.org/golang.org/x/xerrors#As) package to check for [websocket.CloseError](https://godoc.org/nhooyr.io/websocket#CloseError). See the [CloseError godoc example](https://godoc.org/nhooyr.io/websocket#example-CloseError). + ### Server ```go diff --git a/example_test.go b/example_test.go index 22c31202..797658b4 100644 --- a/example_test.go +++ b/example_test.go @@ -6,6 +6,8 @@ import ( "net/http" "time" + "golang.org/x/xerrors" + "nhooyr.io/websocket" "nhooyr.io/websocket/wsjson" ) @@ -60,6 +62,26 @@ func ExampleDial() { c.Close(websocket.StatusNormalClosure, "") } +// This example dials a server and then expects to be disconnected with status code +// websocket.StatusNormalClosure. +func ExampleCloseError() { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + c, _, err := websocket.Dial(ctx, "ws://localhost:8080", nil) + if err != nil { + log.Fatal(err) + } + defer c.Close(websocket.StatusInternalError, "the sky is falling") + + _, _, err = c.Reader(ctx) + var cerr websocket.CloseError + if !xerrors.As(err, &cerr) || cerr.Code != websocket.StatusNormalClosure { + log.Fatalf("expected to be disconnected with StatusNormalClosure but got: %+v", err) + return + } +} + // This example shows how to correctly handle a WebSocket connection // on which you will only write and do not expect to read data messages. func Example_writeOnly() { From 793cb866ad7a58aafb447346b5b2f786529694d3 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 2 Sep 2019 17:48:00 -0500 Subject: [PATCH 107/519] Cleanup link in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9924fb54..4ee0a30e 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ go get nhooyr.io/websocket@v1.5.0 For a production quality example that shows off the full API, see the [echo example on the godoc](https://godoc.org/nhooyr.io/websocket#example-package--Echo). On github, the example is at [example_echo_test.go](./example_echo_test.go). -Please use the [golang.org/x/xerrors](https://godoc.org/golang.org/x/xerrors#As) package to check for [websocket.CloseError](https://godoc.org/nhooyr.io/websocket#CloseError). See the [CloseError godoc example](https://godoc.org/nhooyr.io/websocket#example-CloseError). +Please use the [golang.org/x/xerrors.As](https://godoc.org/golang.org/x/xerrors#As) package to check for [websocket.CloseError](https://godoc.org/nhooyr.io/websocket#CloseError). See the [CloseError godoc example](https://godoc.org/nhooyr.io/websocket#example-CloseError). ### Server From 6d3f05d6259457bdb2a32453fc8b53ed23228d70 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Sep 2019 17:15:00 -0500 Subject: [PATCH 108/519] Remove httpguts dependency Closes #134 --- README.md | 8 ++++---- accept.go | 25 ++++++++++++++++++++++--- go.mod | 2 -- go.sum | 5 ----- 4 files changed, 26 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 4ee0a30e..94a569db 100644 --- a/README.md +++ b/README.md @@ -16,10 +16,10 @@ go get nhooyr.io/websocket@v1.5.0 - Minimal and idiomatic API - Tiny codebase at 1700 lines -- First class context.Context support +- First class [context.Context](https://blog.golang.org/context) support - Thorough tests, fully passes the [autobahn-testsuite](https://github.com/crossbario/autobahn-testsuite) -- Zero dependencies outside of the stdlib for the core library -- JSON and ProtoBuf helpers in the wsjson and wspb subpackages +- [Zero dependencies](https://godoc.org/nhooyr.io/websocket?imports) +- JSON and ProtoBuf helpers in the [wsjson](https://godoc.org/nhooyr.io/websocket/wsjson) and [wspb](https://godoc.org/nhooyr.io/websocket/wspb) subpackages - Highly optimized by default - Concurrent writes out of the box @@ -172,4 +172,4 @@ This is a list of companies or projects that use this library. - [Coder](https://github.com/cdr) -If your company or project is using this library, please feel free to open a PR to amend the list. +If your company or project is using this library, please feel free to open an issue or PR to amend the list. diff --git a/accept.go b/accept.go index afad1be2..fc211d7e 100644 --- a/accept.go +++ b/accept.go @@ -10,7 +10,6 @@ import ( "net/url" "strings" - "golang.org/x/net/http/httpguts" "golang.org/x/xerrors" ) @@ -151,9 +150,29 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, return c, nil } -func headerValuesContainsToken(h http.Header, key, val string) bool { +func headerValuesContainsToken(h http.Header, key, token string) bool { key = textproto.CanonicalMIMEHeaderKey(key) - return httpguts.HeaderValuesContainsToken(h[key], val) + + for _, val2 := range h[key] { + if headerValueContainsToken(val2, token) { + return true + } + } + + return false +} + +func headerValueContainsToken(val2, token string) bool { + val2 = strings.TrimSpace(val2) + + for _, val2 := range strings.Split(val2, ",") { + val2 = strings.TrimSpace(val2) + if strings.EqualFold(val2, token) { + return true + } + } + + return false } func selectSubprotocol(r *http.Request, subprotocols []string) string { diff --git a/go.mod b/go.mod index 70fe1d4c..9e8e33b7 100644 --- a/go.mod +++ b/go.mod @@ -18,9 +18,7 @@ require ( go.uber.org/atomic v1.4.0 // indirect go.uber.org/multierr v1.1.0 golang.org/x/lint v0.0.0-20190409202823-959b441ac422 - golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 golang.org/x/sys v0.0.0-20190830142957-1e83adbbebd0 // indirect - golang.org/x/text v0.3.2 // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 golang.org/x/tools v0.0.0-20190830223141-573d9926052a golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 diff --git a/go.sum b/go.sum index 906f9c38..18b3dd70 100644 --- a/go.sum +++ b/go.sum @@ -78,8 +78,6 @@ golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297 h1:k7pJ2yAPLPgbskkFdhRCsA77k2fySZ1zf2zCjvQCiIM= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -93,11 +91,8 @@ golang.org/x/sys v0.0.0-20190830142957-1e83adbbebd0 h1:7z820YPX9pxWR59qM7BE5+fgl golang.org/x/sys v0.0.0-20190830142957-1e83adbbebd0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190830223141-573d9926052a h1:XAHT1kdPpnU8Hk+FPi42KZFhtNFEk4vBg1U4OmIeHTU= golang.org/x/tools v0.0.0-20190830223141-573d9926052a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= From 7235d8b0704cce01acd8a7b4826adf346072dc2f Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Sep 2019 19:28:00 -0500 Subject: [PATCH 109/519] Remove xerrors dependency Closes #134 --- README.md | 2 +- accept.go | 24 +++++++-------- dial.go | 23 +++++++------- example_echo_test.go | 7 ++--- example_test.go | 5 ++-- export_test.go | 5 ++-- go.mod | 3 +- header.go | 4 +-- netconn.go | 5 ++-- statuscode.go | 13 ++++---- websocket.go | 71 ++++++++++++++++++++++---------------------- websocket_test.go | 54 ++++++++++++++++----------------- wsjson/wsjson.go | 13 ++++---- wspb/wspb.go | 12 ++++---- 14 files changed, 115 insertions(+), 126 deletions(-) diff --git a/README.md b/README.md index 94a569db..5f3f7d9d 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ go get nhooyr.io/websocket@v1.5.0 For a production quality example that shows off the full API, see the [echo example on the godoc](https://godoc.org/nhooyr.io/websocket#example-package--Echo). On github, the example is at [example_echo_test.go](./example_echo_test.go). -Please use the [golang.org/x/xerrors.As](https://godoc.org/golang.org/x/xerrors#As) package to check for [websocket.CloseError](https://godoc.org/nhooyr.io/websocket#CloseError). See the [CloseError godoc example](https://godoc.org/nhooyr.io/websocket#example-CloseError). +Please use the [errors.As](https://golang.org/pkg/errors/#As) function [new in Go 1.13](https://golang.org/doc/go1.13#error_wrapping) to check for [websocket.CloseError](https://godoc.org/nhooyr.io/websocket#CloseError). See the [CloseError godoc example](https://godoc.org/nhooyr.io/websocket#example-CloseError). ### Server diff --git a/accept.go b/accept.go index fc211d7e..7ef72ad2 100644 --- a/accept.go +++ b/accept.go @@ -4,13 +4,13 @@ import ( "bytes" "crypto/sha1" "encoding/base64" + "errors" + "fmt" "io" "net/http" "net/textproto" "net/url" "strings" - - "golang.org/x/xerrors" ) // AcceptOptions represents the options available to pass to Accept. @@ -42,31 +42,31 @@ type AcceptOptions struct { func verifyClientRequest(w http.ResponseWriter, r *http.Request) error { if !headerValuesContainsToken(r.Header, "Connection", "Upgrade") { - err := xerrors.Errorf("websocket protocol violation: Connection header %q does not contain Upgrade", r.Header.Get("Connection")) + err := fmt.Errorf("websocket protocol violation: Connection header %q does not contain Upgrade", r.Header.Get("Connection")) http.Error(w, err.Error(), http.StatusBadRequest) return err } if !headerValuesContainsToken(r.Header, "Upgrade", "WebSocket") { - err := xerrors.Errorf("websocket protocol violation: Upgrade header %q does not contain websocket", r.Header.Get("Upgrade")) + err := fmt.Errorf("websocket protocol violation: Upgrade header %q does not contain websocket", r.Header.Get("Upgrade")) http.Error(w, err.Error(), http.StatusBadRequest) return err } if r.Method != "GET" { - err := xerrors.Errorf("websocket protocol violation: handshake request method is not GET but %q", r.Method) + err := fmt.Errorf("websocket protocol violation: handshake request method is not GET but %q", r.Method) http.Error(w, err.Error(), http.StatusBadRequest) return err } if r.Header.Get("Sec-WebSocket-Version") != "13" { - err := xerrors.Errorf("unsupported websocket protocol version (only 13 is supported): %q", r.Header.Get("Sec-WebSocket-Version")) + err := fmt.Errorf("unsupported websocket protocol version (only 13 is supported): %q", r.Header.Get("Sec-WebSocket-Version")) http.Error(w, err.Error(), http.StatusBadRequest) return err } if r.Header.Get("Sec-WebSocket-Key") == "" { - err := xerrors.New("websocket protocol violation: missing Sec-WebSocket-Key") + err := errors.New("websocket protocol violation: missing Sec-WebSocket-Key") http.Error(w, err.Error(), http.StatusBadRequest) return err } @@ -86,7 +86,7 @@ func verifyClientRequest(w http.ResponseWriter, r *http.Request) error { func Accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, error) { c, err := accept(w, r, opts) if err != nil { - return nil, xerrors.Errorf("failed to accept websocket connection: %w", err) + return nil, fmt.Errorf("failed to accept websocket connection: %w", err) } return c, nil } @@ -111,7 +111,7 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, hj, ok := w.(http.Hijacker) if !ok { - err = xerrors.New("passed ResponseWriter does not implement http.Hijacker") + err = errors.New("passed ResponseWriter does not implement http.Hijacker") http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented) return nil, err } @@ -130,7 +130,7 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, netConn, brw, err := hj.Hijack() if err != nil { - err = xerrors.Errorf("failed to hijack connection: %w", err) + err = fmt.Errorf("failed to hijack connection: %w", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return nil, err } @@ -206,10 +206,10 @@ func authenticateOrigin(r *http.Request) error { } u, err := url.Parse(origin) if err != nil { - return xerrors.Errorf("failed to parse Origin header %q: %w", origin, err) + return fmt.Errorf("failed to parse Origin header %q: %w", origin, err) } if strings.EqualFold(u.Host, r.Host) { return nil } - return xerrors.Errorf("request Origin %q is not authorized for Host %q", origin, r.Host) + return fmt.Errorf("request Origin %q is not authorized for Host %q", origin, r.Host) } diff --git a/dial.go b/dial.go index 461817f6..2ed836bd 100644 --- a/dial.go +++ b/dial.go @@ -5,6 +5,7 @@ import ( "bytes" "context" "encoding/base64" + "fmt" "io" "io/ioutil" "math/rand" @@ -12,8 +13,6 @@ import ( "net/url" "strings" "sync" - - "golang.org/x/xerrors" ) // DialOptions represents the options available to pass to Dial. @@ -44,7 +43,7 @@ type DialOptions struct { func Dial(ctx context.Context, u string, opts *DialOptions) (*Conn, *http.Response, error) { c, r, err := dial(ctx, u, opts) if err != nil { - return nil, r, xerrors.Errorf("failed to websocket dial: %w", err) + return nil, r, fmt.Errorf("failed to websocket dial: %w", err) } return c, r, nil } @@ -62,7 +61,7 @@ func dial(ctx context.Context, u string, opts *DialOptions) (_ *Conn, _ *http.Re opts.HTTPClient = http.DefaultClient } if opts.HTTPClient.Timeout > 0 { - return nil, nil, xerrors.Errorf("please use context for cancellation instead of http.Client.Timeout; see https://github.com/nhooyr/websocket/issues/67") + return nil, nil, fmt.Errorf("please use context for cancellation instead of http.Client.Timeout; see https://github.com/nhooyr/websocket/issues/67") } if opts.HTTPHeader == nil { opts.HTTPHeader = http.Header{} @@ -70,7 +69,7 @@ func dial(ctx context.Context, u string, opts *DialOptions) (_ *Conn, _ *http.Re parsedURL, err := url.Parse(u) if err != nil { - return nil, nil, xerrors.Errorf("failed to parse url: %w", err) + return nil, nil, fmt.Errorf("failed to parse url: %w", err) } switch parsedURL.Scheme { @@ -79,7 +78,7 @@ func dial(ctx context.Context, u string, opts *DialOptions) (_ *Conn, _ *http.Re case "wss": parsedURL.Scheme = "https" default: - return nil, nil, xerrors.Errorf("unexpected url scheme: %q", parsedURL.Scheme) + return nil, nil, fmt.Errorf("unexpected url scheme: %q", parsedURL.Scheme) } req, _ := http.NewRequest("GET", parsedURL.String(), nil) @@ -95,7 +94,7 @@ func dial(ctx context.Context, u string, opts *DialOptions) (_ *Conn, _ *http.Re resp, err := opts.HTTPClient.Do(req) if err != nil { - return nil, nil, xerrors.Errorf("failed to send handshake request: %w", err) + return nil, nil, fmt.Errorf("failed to send handshake request: %w", err) } defer func() { if err != nil { @@ -114,7 +113,7 @@ func dial(ctx context.Context, u string, opts *DialOptions) (_ *Conn, _ *http.Re rwc, ok := resp.Body.(io.ReadWriteCloser) if !ok { - return nil, resp, xerrors.Errorf("response body is not a io.ReadWriteCloser: %T", rwc) + return nil, resp, fmt.Errorf("response body is not a io.ReadWriteCloser: %T", rwc) } c := &Conn{ @@ -132,19 +131,19 @@ func dial(ctx context.Context, u string, opts *DialOptions) (_ *Conn, _ *http.Re func verifyServerResponse(r *http.Request, resp *http.Response) error { if resp.StatusCode != http.StatusSwitchingProtocols { - return xerrors.Errorf("expected handshake response status code %v but got %v", http.StatusSwitchingProtocols, resp.StatusCode) + return fmt.Errorf("expected handshake response status code %v but got %v", http.StatusSwitchingProtocols, resp.StatusCode) } if !headerValuesContainsToken(resp.Header, "Connection", "Upgrade") { - return xerrors.Errorf("websocket protocol violation: Connection header %q does not contain Upgrade", resp.Header.Get("Connection")) + return fmt.Errorf("websocket protocol violation: Connection header %q does not contain Upgrade", resp.Header.Get("Connection")) } if !headerValuesContainsToken(resp.Header, "Upgrade", "WebSocket") { - return xerrors.Errorf("websocket protocol violation: Upgrade header %q does not contain websocket", resp.Header.Get("Upgrade")) + return fmt.Errorf("websocket protocol violation: Upgrade header %q does not contain websocket", resp.Header.Get("Upgrade")) } if resp.Header.Get("Sec-WebSocket-Accept") != secWebSocketAccept(r.Header.Get("Sec-WebSocket-Key")) { - return xerrors.Errorf("websocket protocol violation: invalid Sec-WebSocket-Accept %q, key %q", + return fmt.Errorf("websocket protocol violation: invalid Sec-WebSocket-Accept %q, key %q", resp.Header.Get("Sec-WebSocket-Accept"), r.Header.Get("Sec-WebSocket-Key"), ) diff --git a/example_echo_test.go b/example_echo_test.go index 3e7e7f9d..aad32675 100644 --- a/example_echo_test.go +++ b/example_echo_test.go @@ -10,7 +10,6 @@ import ( "time" "golang.org/x/time/rate" - "golang.org/x/xerrors" "nhooyr.io/websocket" "nhooyr.io/websocket/wsjson" @@ -78,14 +77,14 @@ func echoServer(w http.ResponseWriter, r *http.Request) error { if c.Subprotocol() != "echo" { c.Close(websocket.StatusPolicyViolation, "client must speak the echo subprotocol") - return xerrors.Errorf("client does not speak echo sub protocol") + return fmt.Errorf("client does not speak echo sub protocol") } l := rate.NewLimiter(rate.Every(time.Millisecond*100), 10) for { err = echo(r.Context(), c, l) if err != nil { - return xerrors.Errorf("failed to echo with %v: %w", r.RemoteAddr, err) + return fmt.Errorf("failed to echo with %v: %w", r.RemoteAddr, err) } } } @@ -114,7 +113,7 @@ func echo(ctx context.Context, c *websocket.Conn, l *rate.Limiter) error { _, err = io.Copy(w, r) if err != nil { - return xerrors.Errorf("failed to io.Copy: %w", err) + return fmt.Errorf("failed to io.Copy: %w", err) } err = w.Close() diff --git a/example_test.go b/example_test.go index 797658b4..36cab2bd 100644 --- a/example_test.go +++ b/example_test.go @@ -2,12 +2,11 @@ package websocket_test import ( "context" + "errors" "log" "net/http" "time" - "golang.org/x/xerrors" - "nhooyr.io/websocket" "nhooyr.io/websocket/wsjson" ) @@ -76,7 +75,7 @@ func ExampleCloseError() { _, _, err = c.Reader(ctx) var cerr websocket.CloseError - if !xerrors.As(err, &cerr) || cerr.Code != websocket.StatusNormalClosure { + if !errors.As(err, &cerr) || cerr.Code != websocket.StatusNormalClosure { log.Fatalf("expected to be disconnected with StatusNormalClosure but got: %+v", err) return } diff --git a/export_test.go b/export_test.go index 811bf800..5a0d1c32 100644 --- a/export_test.go +++ b/export_test.go @@ -3,8 +3,7 @@ package websocket import ( "bufio" "context" - - "golang.org/x/xerrors" + "fmt" ) type ( @@ -65,7 +64,7 @@ func (c *Conn) WriteHeader(ctx context.Context, h Header) error { }) _, err := c.bw.Write(headerBytes) if err != nil { - return xerrors.Errorf("failed to write header: %w", err) + return fmt.Errorf("failed to write header: %w", err) } if h.Fin { err = c.Flush() diff --git a/go.mod b/go.mod index 9e8e33b7..b59397c1 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module nhooyr.io/websocket -go 1.12 +go 1.13 require ( github.com/fatih/color v1.7.0 // indirect @@ -21,7 +21,6 @@ require ( golang.org/x/sys v0.0.0-20190830142957-1e83adbbebd0 // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 golang.org/x/tools v0.0.0-20190830223141-573d9926052a - golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gotest.tools v2.2.0+incompatible // indirect gotest.tools/gotestsum v0.3.6-0.20190825182939-fc6cb5870c52 diff --git a/header.go b/header.go index 16ab6474..6eb8610f 100644 --- a/header.go +++ b/header.go @@ -5,8 +5,6 @@ import ( "fmt" "io" "math" - - "golang.org/x/xerrors" ) // First byte contains fin, rsv1, rsv2, rsv3. @@ -145,7 +143,7 @@ func readHeader(b []byte, r io.Reader) (header, error) { case payloadLength == 127: h.payloadLength = int64(binary.BigEndian.Uint64(b)) if h.payloadLength < 0 { - return header{}, xerrors.Errorf("header with negative payload length: %v", h.payloadLength) + return header{}, fmt.Errorf("header with negative payload length: %v", h.payloadLength) } b = b[8:] } diff --git a/netconn.go b/netconn.go index a7c9bf7f..20b99c2a 100644 --- a/netconn.go +++ b/netconn.go @@ -2,13 +2,12 @@ package websocket import ( "context" + "errors" "fmt" "io" "math" "net" "time" - - "golang.org/x/xerrors" ) // NetConn converts a *websocket.Conn into a net.Conn. @@ -97,7 +96,7 @@ func (c *netConn) Read(p []byte) (int, error) { typ, r, err := c.c.Reader(c.readContext) if err != nil { var ce CloseError - if xerrors.As(err, &ce) && (ce.Code == StatusNormalClosure) || (ce.Code == StatusGoingAway) { + if errors.As(err, &ce) && (ce.Code == StatusNormalClosure) || (ce.Code == StatusGoingAway) { c.eofed = true return 0, io.EOF } diff --git a/statuscode.go b/statuscode.go index 558610d4..d2a64d62 100644 --- a/statuscode.go +++ b/statuscode.go @@ -3,8 +3,6 @@ package websocket import ( "encoding/binary" "fmt" - - "golang.org/x/xerrors" ) // StatusCode represents a WebSocket status code. @@ -43,7 +41,8 @@ const ( // CloseError represents a WebSocket close frame. // It is returned by Conn's methods when a WebSocket close frame is received from // the peer. -// You will need to use https://godoc.org/golang.org/x/xerrors#As to check for this error. +// You will need to use the https://golang.org/pkg/errors/#As function, new in Go 1.13, +// to check for this error. See the CloseError example. type CloseError struct { Code StatusCode Reason string @@ -61,7 +60,7 @@ func parseClosePayload(p []byte) (CloseError, error) { } if len(p) < 2 { - return CloseError{}, xerrors.Errorf("close payload %q too small, cannot even contain the 2 byte status code", p) + return CloseError{}, fmt.Errorf("close payload %q too small, cannot even contain the 2 byte status code", p) } ce := CloseError{ @@ -70,7 +69,7 @@ func parseClosePayload(p []byte) (CloseError, error) { } if !validWireCloseCode(ce.Code) { - return CloseError{}, xerrors.Errorf("invalid status code %v", ce.Code) + return CloseError{}, fmt.Errorf("invalid status code %v", ce.Code) } return ce, nil @@ -98,10 +97,10 @@ const maxControlFramePayload = 125 func (ce CloseError) bytes() ([]byte, error) { if len(ce.Reason) > maxControlFramePayload-2 { - return nil, xerrors.Errorf("reason string max is %v but got %q with length %v", maxControlFramePayload-2, ce.Reason, len(ce.Reason)) + return nil, fmt.Errorf("reason string max is %v but got %q with length %v", maxControlFramePayload-2, ce.Reason, len(ce.Reason)) } if !validWireCloseCode(ce.Code) { - return nil, xerrors.Errorf("status code %v cannot be set", ce.Code) + return nil, fmt.Errorf("status code %v cannot be set", ce.Code) } buf := make([]byte, 2+len(ce.Reason)) diff --git a/websocket.go b/websocket.go index 7dabfa25..56aca705 100644 --- a/websocket.go +++ b/websocket.go @@ -4,6 +4,7 @@ import ( "bufio" "context" cryptorand "crypto/rand" + "errors" "fmt" "io" "io/ioutil" @@ -14,8 +15,6 @@ import ( "sync" "sync/atomic" "time" - - "golang.org/x/xerrors" ) // Conn represents a WebSocket connection. @@ -107,7 +106,7 @@ func (c *Conn) init() { c.controlPayloadBuf = make([]byte, maxControlFramePayload) runtime.SetFinalizer(c, func(c *Conn) { - c.close(xerrors.New("connection garbage collected")) + c.close(errors.New("connection garbage collected")) }) go c.timeoutLoop() @@ -121,7 +120,7 @@ func (c *Conn) Subprotocol() string { func (c *Conn) setCloseErr(err error) { c.closeErrOnce.Do(func() { - c.closeErr = xerrors.Errorf("websocket closed: %w", err) + c.closeErr = fmt.Errorf("websocket closed: %w", err) }) } @@ -166,9 +165,9 @@ func (c *Conn) timeoutLoop() { case readCtx = <-c.setReadTimeout: case <-readCtx.Done(): - c.close(xerrors.Errorf("read timed out: %w", readCtx.Err())) + c.close(fmt.Errorf("read timed out: %w", readCtx.Err())) case <-writeCtx.Done(): - c.close(xerrors.Errorf("write timed out: %w", writeCtx.Err())) + c.close(fmt.Errorf("write timed out: %w", writeCtx.Err())) } } } @@ -179,9 +178,9 @@ func (c *Conn) acquireLock(ctx context.Context, lock chan struct{}) error { var err error switch lock { case c.writeFrameLock, c.writeMsgLock: - err = xerrors.Errorf("could not acquire write lock: %v", ctx.Err()) + err = fmt.Errorf("could not acquire write lock: %v", ctx.Err()) case c.readFrameLock: - err = xerrors.Errorf("could not acquire read lock: %v", ctx.Err()) + err = fmt.Errorf("could not acquire read lock: %v", ctx.Err()) default: panic(fmt.Sprintf("websocket: failed to acquire unknown lock: %v", ctx.Err())) } @@ -217,7 +216,7 @@ func (c *Conn) readTillMsg(ctx context.Context) (header, error) { if h.opcode.controlOp() { err = c.handleControl(ctx, h) if err != nil { - return header{}, xerrors.Errorf("failed to handle control frame: %w", err) + return header{}, fmt.Errorf("failed to handle control frame: %w", err) } continue } @@ -254,7 +253,7 @@ func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { err = ctx.Err() default: } - err := xerrors.Errorf("failed to read header: %w", err) + err := fmt.Errorf("failed to read header: %w", err) c.releaseLock(c.readFrameLock) c.close(err) return header{}, err @@ -307,14 +306,14 @@ func (c *Conn) handleControl(ctx context.Context, h header) error { case opClose: ce, err := parseClosePayload(b) if err != nil { - err = xerrors.Errorf("received invalid close payload: %w", err) + err = fmt.Errorf("received invalid close payload: %w", err) c.Close(StatusProtocolError, err.Error()) return c.closeErr } // This ensures the closeErr of the Conn is always the received CloseError // in case the echo close frame write fails. // See https://github.com/nhooyr/websocket/issues/109 - c.setCloseErr(xerrors.Errorf("received close frame: %w", ce)) + c.setCloseErr(fmt.Errorf("received close frame: %w", ce)) c.writeClose(b, nil) return c.closeErr default: @@ -347,12 +346,12 @@ func (c *Conn) handleControl(ctx context.Context, h header) error { // Most users should not need this. func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) { if atomic.LoadInt64(&c.readClosed) == 1 { - return 0, nil, xerrors.Errorf("websocket connection read closed") + return 0, nil, fmt.Errorf("websocket connection read closed") } typ, r, err := c.reader(ctx) if err != nil { - return 0, nil, xerrors.Errorf("failed to get reader: %w", err) + return 0, nil, fmt.Errorf("failed to get reader: %w", err) } return typ, r, nil } @@ -363,7 +362,7 @@ func (c *Conn) reader(ctx context.Context) (MessageType, io.Reader, error) { // if there is an active frame not yet fully read. // Otherwise, a user may have read the last byte but not the EOF if the EOF // is in the next frame so we check for that below. - return 0, nil, xerrors.Errorf("previous message not read to completion") + return 0, nil, fmt.Errorf("previous message not read to completion") } h, err := c.readTillMsg(ctx) @@ -378,7 +377,7 @@ func (c *Conn) reader(ctx context.Context) (MessageType, io.Reader, error) { } if !h.fin || h.payloadLength > 0 { - return 0, nil, xerrors.Errorf("previous message not read to completion") + return 0, nil, fmt.Errorf("previous message not read to completion") } c.activeReader = nil @@ -441,17 +440,17 @@ func (r *messageReader) Read(p []byte) (int, error) { if err != nil { // Have to return io.EOF directly for now, we cannot wrap as xerrors // isn't used in stdlib. - if xerrors.Is(err, io.EOF) { + if errors.Is(err, io.EOF) { return n, io.EOF } - return n, xerrors.Errorf("failed to read: %w", err) + return n, fmt.Errorf("failed to read: %w", err) } return n, nil } func (r *messageReader) read(p []byte) (int, error) { if r.eof() { - return 0, xerrors.Errorf("cannot use EOFed reader") + return 0, fmt.Errorf("cannot use EOFed reader") } if r.c.readMsgLeft <= 0 { @@ -531,7 +530,7 @@ func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { err = ctx.Err() default: } - err = xerrors.Errorf("failed to read frame payload: %w", err) + err = fmt.Errorf("failed to read frame payload: %w", err) c.releaseLock(c.readFrameLock) c.close(err) return n, err @@ -580,7 +579,7 @@ func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { func (c *Conn) Writer(ctx context.Context, typ MessageType) (io.WriteCloser, error) { wc, err := c.writer(ctx, typ) if err != nil { - return nil, xerrors.Errorf("failed to get writer: %w", err) + return nil, fmt.Errorf("failed to get writer: %w", err) } return wc, nil } @@ -605,7 +604,7 @@ func (c *Conn) writer(ctx context.Context, typ MessageType) (io.WriteCloser, err func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { _, err := c.write(ctx, typ, p) if err != nil { - return xerrors.Errorf("failed to write msg: %w", err) + return fmt.Errorf("failed to write msg: %w", err) } return nil } @@ -634,18 +633,18 @@ func (w *messageWriter) closed() bool { func (w *messageWriter) Write(p []byte) (int, error) { n, err := w.write(p) if err != nil { - return n, xerrors.Errorf("failed to write: %w", err) + return n, fmt.Errorf("failed to write: %w", err) } return n, nil } func (w *messageWriter) write(p []byte) (int, error) { if w.closed() { - return 0, xerrors.Errorf("cannot use closed writer") + return 0, fmt.Errorf("cannot use closed writer") } n, err := w.c.writeFrame(w.c.writeMsgCtx, false, w.c.writeMsgOpcode, p) if err != nil { - return n, xerrors.Errorf("failed to write data frame: %w", err) + return n, fmt.Errorf("failed to write data frame: %w", err) } w.c.writeMsgOpcode = opContinuation return n, nil @@ -656,20 +655,20 @@ func (w *messageWriter) write(p []byte) (int, error) { func (w *messageWriter) Close() error { err := w.close() if err != nil { - return xerrors.Errorf("failed to close writer: %w", err) + return fmt.Errorf("failed to close writer: %w", err) } return nil } func (w *messageWriter) close() error { if w.closed() { - return xerrors.Errorf("cannot use closed writer") + return fmt.Errorf("cannot use closed writer") } w.c.activeWriter = nil _, err := w.c.writeFrame(w.c.writeMsgCtx, true, w.c.writeMsgOpcode, nil) if err != nil { - return xerrors.Errorf("failed to write fin frame: %w", err) + return fmt.Errorf("failed to write fin frame: %w", err) } w.c.releaseLock(w.c.writeMsgLock) @@ -679,7 +678,7 @@ func (w *messageWriter) close() error { func (c *Conn) writeControl(ctx context.Context, opcode opcode, p []byte) error { _, err := c.writeFrame(ctx, true, opcode, p) if err != nil { - return xerrors.Errorf("failed to write control frame: %w", err) + return fmt.Errorf("failed to write control frame: %w", err) } return nil } @@ -706,7 +705,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, opcode opcode, p []byte if c.client { _, err := io.ReadFull(cryptorand.Reader, c.writeHeader.maskKey[:]) if err != nil { - return 0, xerrors.Errorf("failed to generate masking key: %w", err) + return 0, fmt.Errorf("failed to generate masking key: %w", err) } } @@ -737,7 +736,7 @@ func (c *Conn) realWriteFrame(ctx context.Context, h header, p []byte) (n int, e default: } - err = xerrors.Errorf("failed to write %v frame: %w", h.opcode, err) + err = fmt.Errorf("failed to write %v frame: %w", h.opcode, err) // We need to release the lock first before closing the connection to ensure // the lock can be acquired inside close to ensure no one can access c.bw. c.releaseLock(c.writeFrameLock) @@ -821,7 +820,7 @@ func (c *Conn) writePong(p []byte) error { func (c *Conn) Close(code StatusCode, reason string) error { err := c.exportedClose(code, reason) if err != nil { - return xerrors.Errorf("failed to close websocket connection: %w", err) + return fmt.Errorf("failed to close websocket connection: %w", err) } return nil } @@ -846,13 +845,13 @@ func (c *Conn) exportedClose(code StatusCode, reason string) error { // CloseErrors sent are made opaque to prevent applications from thinking // they received a given status. - sentErr := xerrors.Errorf("sent close frame: %v", ce) + sentErr := fmt.Errorf("sent close frame: %v", ce) err = c.writeClose(p, sentErr) if err != nil { return err } - if !xerrors.Is(c.closeErr, sentErr) { + if !errors.Is(c.closeErr, sentErr) { return c.closeErr } @@ -891,7 +890,7 @@ func (c *Conn) Ping(ctx context.Context) error { err := c.ping(ctx, p) if err != nil { - return xerrors.Errorf("failed to ping: %w", err) + return fmt.Errorf("failed to ping: %w", err) } return nil } @@ -918,7 +917,7 @@ func (c *Conn) ping(ctx context.Context, p string) error { case <-c.closed: return c.closeErr case <-ctx.Done(): - err := xerrors.Errorf("failed to wait for pong: %w", ctx.Err()) + err := fmt.Errorf("failed to wait for pong: %w", ctx.Err()) c.close(err) return err case <-pong: diff --git a/websocket_test.go b/websocket_test.go index ef6ae926..1aa8b201 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -5,6 +5,7 @@ import ( "context" "encoding/binary" "encoding/json" + "errors" "fmt" "io" "io/ioutil" @@ -24,7 +25,6 @@ import ( "github.com/golang/protobuf/ptypes" "github.com/golang/protobuf/ptypes/timestamp" "go.uber.org/multierr" - "golang.org/x/xerrors" "nhooyr.io/websocket" "nhooyr.io/websocket/wsjson" @@ -45,7 +45,7 @@ func TestHandshake(t *testing.T) { c, err := websocket.Accept(w, r, nil) if err == nil { c.Close(websocket.StatusInternalError, "") - return xerrors.New("expected error regarding bad origin") + return errors.New("expected error regarding bad origin") } return assertErrorContains(err, "not authorized") }, @@ -57,7 +57,7 @@ func TestHandshake(t *testing.T) { }) if err == nil { c.Close(websocket.StatusInternalError, "") - return xerrors.New("expected handshake failure") + return errors.New("expected handshake failure") } return assertErrorContains(err, "403") }, @@ -115,7 +115,7 @@ func TestHandshake(t *testing.T) { server: func(w http.ResponseWriter, r *http.Request) error { cookie, err := r.Cookie("mycookie") if err != nil { - return xerrors.Errorf("request is missing mycookie: %w", err) + return fmt.Errorf("request is missing mycookie: %w", err) } err = assertEqualf("myvalue", cookie.Value, "unexpected cookie value") if err != nil { @@ -131,11 +131,11 @@ func TestHandshake(t *testing.T) { client: func(ctx context.Context, u string) error { jar, err := cookiejar.New(nil) if err != nil { - return xerrors.Errorf("failed to create cookie jar: %w", err) + return fmt.Errorf("failed to create cookie jar: %w", err) } parsedURL, err := url.Parse(u) if err != nil { - return xerrors.Errorf("failed to parse url: %w", err) + return fmt.Errorf("failed to parse url: %w", err) } parsedURL.Scheme = "http" jar.SetCookies(parsedURL, []*http.Cookie{ @@ -410,7 +410,7 @@ func TestConn(t *testing.T) { case err = <-pingErrc: return err case <-ctx2.Done(): - return xerrors.Errorf("failed to wait for pong: %w", ctx2.Err()) + return fmt.Errorf("failed to wait for pong: %w", ctx2.Err()) } }, }, @@ -576,15 +576,15 @@ func TestConn(t *testing.T) { } _, _, err = c.Read(ctx) cerr := &websocket.CloseError{} - if !xerrors.As(err, cerr) || cerr.Code != websocket.StatusProtocolError { - return xerrors.Errorf("expected close error with StatusProtocolError: %+v", err) + if !errors.As(err, cerr) || cerr.Code != websocket.StatusProtocolError { + return fmt.Errorf("expected close error with StatusProtocolError: %+v", err) } return nil }, client: func(ctx context.Context, c *websocket.Conn) error { _, _, err := c.Read(ctx) if err == nil || !strings.Contains(err.Error(), "rsv") { - return xerrors.Errorf("expected error that contains rsv: %+v", err) + return fmt.Errorf("expected error that contains rsv: %+v", err) } return nil }, @@ -684,19 +684,19 @@ func TestConn(t *testing.T) { } _, err = w.Write([]byte(strings.Repeat("x", 10))) if err != nil { - return xerrors.Errorf("expected non nil error") + return fmt.Errorf("expected non nil error") } err = c.Flush() if err != nil { - return xerrors.Errorf("failed to flush: %w", err) + return fmt.Errorf("failed to flush: %w", err) } _, err = w.Write([]byte(strings.Repeat("x", 10))) if err != nil { - return xerrors.Errorf("expected non nil error") + return fmt.Errorf("expected non nil error") } err = c.Flush() if err != nil { - return xerrors.Errorf("failed to flush: %w", err) + return fmt.Errorf("failed to flush: %w", err) } _, _, err = c.Read(ctx) return assertCloseStatus(err, websocket.StatusInternalError) @@ -724,15 +724,15 @@ func TestConn(t *testing.T) { } _, err = w.Write([]byte(strings.Repeat("x", 10))) if err != nil { - return xerrors.Errorf("expected non nil error") + return fmt.Errorf("expected non nil error") } err = c.Flush() if err != nil { - return xerrors.Errorf("failed to flush: %w", err) + return fmt.Errorf("failed to flush: %w", err) } _, err = c.WriteFrame(ctx, true, websocket.OpBinary, []byte(strings.Repeat("x", 10))) if err != nil { - return xerrors.Errorf("expected non nil error") + return fmt.Errorf("expected non nil error") } _, _, err = c.Read(ctx) return assertErrorContains(err, "received new data message without finishing") @@ -798,15 +798,15 @@ func TestConn(t *testing.T) { } _, err = w.Write([]byte(strings.Repeat("x", 10))) if err != nil { - return xerrors.Errorf("expected non nil error") + return fmt.Errorf("expected non nil error") } err = c.Flush() if err != nil { - return xerrors.Errorf("failed to flush: %w", err) + return fmt.Errorf("failed to flush: %w", err) } _, err = c.WriteFrame(ctx, true, websocket.OpBinary, []byte(strings.Repeat("x", 10))) if err != nil { - return xerrors.Errorf("expected non nil error") + return fmt.Errorf("expected non nil error") } _, _, err = c.Read(ctx) return assertCloseStatus(err, websocket.StatusProtocolError) @@ -1890,8 +1890,8 @@ func echoLoop(ctx context.Context, c *websocket.Conn) { func assertCloseStatus(err error, code websocket.StatusCode) error { var cerr websocket.CloseError - if !xerrors.As(err, &cerr) { - return xerrors.Errorf("no websocket close error in error chain: %+v", err) + if !errors.As(err, &cerr) { + return fmt.Errorf("no websocket close error in error chain: %+v", err) } return assertEqualf(code, cerr.Code, "unexpected status code") } @@ -1949,7 +1949,7 @@ func assertSubprotocol(c *websocket.Conn, exp string) error { func assertEqualf(exp, act interface{}, f string, v ...interface{}) error { if diff := cmpDiff(exp, act); diff != "" { - return xerrors.Errorf(f+": %v", append(v, diff)...) + return fmt.Errorf(f+": %v", append(v, diff)...) } return nil } @@ -1965,14 +1965,14 @@ func assertNetConnRead(r io.Reader, exp string) error { func assertErrorContains(err error, exp string) error { if err == nil || !strings.Contains(err.Error(), exp) { - return xerrors.Errorf("expected error that contains %q but got: %+v", exp, err) + return fmt.Errorf("expected error that contains %q but got: %+v", exp, err) } return nil } func assertErrorIs(exp, act error) error { - if !xerrors.Is(act, exp) { - return xerrors.Errorf("expected error %+v to be in %+v", exp, act) + if !errors.Is(act, exp) { + return fmt.Errorf("expected error %+v to be in %+v", exp, act) } return nil } @@ -2000,7 +2000,7 @@ func assertReadCloseFrame(ctx context.Context, c *websocket.Conn, code websocket } ce, err := websocket.ParseClosePayload(actP) if err != nil { - return xerrors.Errorf("failed to parse close frame payload: %w", err) + return fmt.Errorf("failed to parse close frame payload: %w", err) } return assertEqualf(ce.Code, code, "unexpected frame close frame code with payload %q", actP) } diff --git a/wsjson/wsjson.go b/wsjson/wsjson.go index fda429ac..1e63f940 100644 --- a/wsjson/wsjson.go +++ b/wsjson/wsjson.go @@ -4,8 +4,7 @@ package wsjson // import "nhooyr.io/websocket/wsjson" import ( "context" "encoding/json" - - "golang.org/x/xerrors" + "fmt" "nhooyr.io/websocket" "nhooyr.io/websocket/internal/bpool" @@ -16,7 +15,7 @@ import ( func Read(ctx context.Context, c *websocket.Conn, v interface{}) error { err := read(ctx, c, v) if err != nil { - return xerrors.Errorf("failed to read json: %w", err) + return fmt.Errorf("failed to read json: %w", err) } return nil } @@ -29,7 +28,7 @@ func read(ctx context.Context, c *websocket.Conn, v interface{}) error { if typ != websocket.MessageText { c.Close(websocket.StatusUnsupportedData, "can only accept text messages") - return xerrors.Errorf("unexpected frame type for json (expected %v): %v", websocket.MessageText, typ) + return fmt.Errorf("unexpected frame type for json (expected %v): %v", websocket.MessageText, typ) } b := bpool.Get() @@ -45,7 +44,7 @@ func read(ctx context.Context, c *websocket.Conn, v interface{}) error { err = json.Unmarshal(b.Bytes(), v) if err != nil { c.Close(websocket.StatusInvalidFramePayloadData, "failed to unmarshal JSON") - return xerrors.Errorf("failed to unmarshal json: %w", err) + return fmt.Errorf("failed to unmarshal json: %w", err) } return nil @@ -56,7 +55,7 @@ func read(ctx context.Context, c *websocket.Conn, v interface{}) error { func Write(ctx context.Context, c *websocket.Conn, v interface{}) error { err := write(ctx, c, v) if err != nil { - return xerrors.Errorf("failed to write json: %w", err) + return fmt.Errorf("failed to write json: %w", err) } return nil } @@ -72,7 +71,7 @@ func write(ctx context.Context, c *websocket.Conn, v interface{}) error { e := json.NewEncoder(w) err = e.Encode(v) if err != nil { - return xerrors.Errorf("failed to encode json: %w", err) + return fmt.Errorf("failed to encode json: %w", err) } err = w.Close() diff --git a/wspb/wspb.go b/wspb/wspb.go index dfc407cc..8613a080 100644 --- a/wspb/wspb.go +++ b/wspb/wspb.go @@ -4,10 +4,10 @@ package wspb // import "nhooyr.io/websocket/wspb" import ( "bytes" "context" + "fmt" "sync" "github.com/golang/protobuf/proto" - "golang.org/x/xerrors" "nhooyr.io/websocket" "nhooyr.io/websocket/internal/bpool" @@ -18,7 +18,7 @@ import ( func Read(ctx context.Context, c *websocket.Conn, v proto.Message) error { err := read(ctx, c, v) if err != nil { - return xerrors.Errorf("failed to read protobuf: %w", err) + return fmt.Errorf("failed to read protobuf: %w", err) } return nil } @@ -31,7 +31,7 @@ func read(ctx context.Context, c *websocket.Conn, v proto.Message) error { if typ != websocket.MessageBinary { c.Close(websocket.StatusUnsupportedData, "can only accept binary messages") - return xerrors.Errorf("unexpected frame type for protobuf (expected %v): %v", websocket.MessageBinary, typ) + return fmt.Errorf("unexpected frame type for protobuf (expected %v): %v", websocket.MessageBinary, typ) } b := bpool.Get() @@ -47,7 +47,7 @@ func read(ctx context.Context, c *websocket.Conn, v proto.Message) error { err = proto.Unmarshal(b.Bytes(), v) if err != nil { c.Close(websocket.StatusInvalidFramePayloadData, "failed to unmarshal protobuf") - return xerrors.Errorf("failed to unmarshal protobuf: %w", err) + return fmt.Errorf("failed to unmarshal protobuf: %w", err) } return nil @@ -58,7 +58,7 @@ func read(ctx context.Context, c *websocket.Conn, v proto.Message) error { func Write(ctx context.Context, c *websocket.Conn, v proto.Message) error { err := write(ctx, c, v) if err != nil { - return xerrors.Errorf("failed to write protobuf: %w", err) + return fmt.Errorf("failed to write protobuf: %w", err) } return nil } @@ -74,7 +74,7 @@ func write(ctx context.Context, c *websocket.Conn, v proto.Message) error { err := pb.Marshal(v) if err != nil { - return xerrors.Errorf("failed to marshal protobuf: %w", err) + return fmt.Errorf("failed to marshal protobuf: %w", err) } return c.Write(ctx, websocket.MessageBinary, pb.Bytes()) From 31280ea678d3b8e4f2194c1868aad905bb352e36 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Sep 2019 19:29:29 -0500 Subject: [PATCH 110/519] Bump version in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5f3f7d9d..4091736d 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ websocket is a minimal and idiomatic WebSocket library for Go. ## Install ```bash -go get nhooyr.io/websocket@v1.5.0 +go get nhooyr.io/websocket@v1.5.1 ``` ## Features From 1ea51d3b40d89c2e65e9c44bfe40b5f14c380b8b Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Sep 2019 19:30:37 -0500 Subject: [PATCH 111/519] Update CI image version to Go 1.13 --- .circleci/config.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 196ec671..ec061964 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2 jobs: fmt: docker: - - image: nhooyr/websocket-ci@sha256:371ca985ce2548840aeb0f8434a551708cdfe0628be722c361958e65cdded945 + - image: nhooyr/websocket-ci@sha256:77e37211ded3c528e947439e294fbfc03b4fb9f9537c4e5198d5b304fd1df435 steps: - checkout - restore_cache: @@ -19,7 +19,7 @@ jobs: lint: docker: - - image: nhooyr/websocket-ci@sha256:371ca985ce2548840aeb0f8434a551708cdfe0628be722c361958e65cdded945 + - image: nhooyr/websocket-ci@sha256:77e37211ded3c528e947439e294fbfc03b4fb9f9537c4e5198d5b304fd1df435 steps: - checkout - restore_cache: @@ -36,7 +36,7 @@ jobs: test: docker: - - image: nhooyr/websocket-ci@sha256:371ca985ce2548840aeb0f8434a551708cdfe0628be722c361958e65cdded945 + - image: nhooyr/websocket-ci@sha256:77e37211ded3c528e947439e294fbfc03b4fb9f9537c4e5198d5b304fd1df435 steps: - checkout - restore_cache: From 103b1365631ba839127a24fee152cbd775cbec23 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Sep 2019 19:47:43 -0500 Subject: [PATCH 112/519] Fix codecov thresholds --- ci/.codecov.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/.codecov.yml b/ci/.codecov.yml index f7eec9f8..5ede9ea1 100644 --- a/ci/.codecov.yml +++ b/ci/.codecov.yml @@ -4,7 +4,7 @@ coverage: # Prevent small changes in coverage from failing CI. project: default: - threshold: 5 + threshold: 10 patch: default: - threshold: 5 + threshold: 50 From 307e991006a695b1811d1d2869a5b9abd90236c7 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Sep 2019 20:00:11 -0500 Subject: [PATCH 113/519] Add release badge instead of in install command Go will automatically pull the latest semver anyway. --- README.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4091736d..36cf81eb 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,16 @@ # websocket [![GoDoc](https://godoc.org/nhooyr.io/websocket?status.svg)](https://godoc.org/nhooyr.io/websocket) +[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/nhooyr/websocket?color=critical&sort=semver)](https://github.com/nhooyr/websocket/releases) +[![Codecov](https://img.shields.io/codecov/c/github/nhooyr/websocket.svg?color=success)](https://codecov.io/gh/nhooyr/websocket) [![CI](https://img.shields.io/circleci/build/github/nhooyr/websocket?label=ci)](https://github.com/nhooyr/websocket/commits/master) -[![Codecov](https://img.shields.io/codecov/c/github/nhooyr/websocket.svg?color=brightgreen)](https://codecov.io/gh/nhooyr/websocket) websocket is a minimal and idiomatic WebSocket library for Go. ## Install ```bash -go get nhooyr.io/websocket@v1.5.1 +go get nhooyr.io/websocket ``` ## Features From b59dddcec7a9f5c0f0bc520b1981a520ca9fd987 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Tue, 3 Sep 2019 21:01:16 -0500 Subject: [PATCH 114/519] Add Tatsu to companies section in README --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 36cf81eb..fac186f6 100644 --- a/README.md +++ b/README.md @@ -173,4 +173,6 @@ This is a list of companies or projects that use this library. - [Coder](https://github.com/cdr) +- [Tatsu Works](https://github.com/tatsuworks) - Ingresses 20TB/month of websocket data on their Discord bot. + If your company or project is using this library, please feel free to open an issue or PR to amend the list. From 93d751f3b3fdfa806f4ae3f49fa1bf4e31cee04c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Sep 2019 23:29:54 -0500 Subject: [PATCH 115/519] Simplify README.md --- README.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/README.md b/README.md index fac186f6..244315ef 100644 --- a/README.md +++ b/README.md @@ -169,10 +169,7 @@ Please see [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md). ## Users -This is a list of companies or projects that use this library. - - [Coder](https://github.com/cdr) - -- [Tatsu Works](https://github.com/tatsuworks) - Ingresses 20TB/month of websocket data on their Discord bot. +- [Tatsu Works](https://github.com/tatsuworks) - Ingresses 20 TB in websocket data every month on their Discord bot. If your company or project is using this library, please feel free to open an issue or PR to amend the list. From 067b40e227d0d6af9e399c1efe0dd80efae1b79f Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Sep 2019 20:05:40 -0500 Subject: [PATCH 116/519] Simplify root of repository Will be cleaner in GitHub as the README.md is the first thing you will see instead of a massive directory listing. --- .circleci/config.yml | 28 ++++++------- README.md | 4 +- docs/CONTRIBUTING.md | 21 +++++----- go.mod | 28 ------------- accept.go => mod/accept.go | 0 accept_test.go => mod/accept_test.go | 0 {ci => mod/ci}/.codecov.yml | 0 {ci => mod/ci}/.gitignore | 0 {ci => mod/ci}/fmt.sh | 10 ++--- {ci => mod/ci}/image/Dockerfile | 0 {ci => mod/ci}/image/push.sh | 0 {ci => mod/ci}/lint.sh | 4 +- {ci => mod/ci}/run.sh | 2 +- {ci => mod/ci}/test.sh | 2 +- cmp_test.go => mod/cmp_test.go | 0 dial.go => mod/dial.go | 0 dial_test.go => mod/dial_test.go | 0 doc.go => mod/doc.go | 0 .../example_echo_test.go | 0 example_test.go => mod/example_test.go | 0 export_test.go => mod/export_test.go | 0 mod/go.mod | 20 ++++++++++ go.sum => mod/go.sum | 39 +++---------------- header.go => mod/header.go | 0 header_test.go => mod/header_test.go | 0 {internal => mod/internal}/bpool/bpool.go | 0 .../internal}/bpool/bpool_test.go | 0 messagetype.go => mod/messagetype.go | 0 .../messagetype_string.go | 0 netconn.go => mod/netconn.go | 0 opcode.go => mod/opcode.go | 0 opcode_string.go => mod/opcode_string.go | 0 statuscode.go => mod/statuscode.go | 0 .../statuscode_string.go | 0 statuscode_test.go => mod/statuscode_test.go | 0 tools.go => mod/tools.go | 2 +- websocket.go => mod/websocket.go | 0 .../websocket_autobahn_python_test.go | 0 .../websocket_bench_test.go | 0 websocket_test.go => mod/websocket_test.go | 0 {wsjson => mod/wsjson}/wsjson.go | 0 {wspb => mod/wspb}/wspb.go | 0 xor.go => mod/xor.go | 0 xor_test.go => mod/xor_test.go | 0 44 files changed, 63 insertions(+), 97 deletions(-) delete mode 100644 go.mod rename accept.go => mod/accept.go (100%) rename accept_test.go => mod/accept_test.go (100%) rename {ci => mod/ci}/.codecov.yml (100%) rename {ci => mod/ci}/.gitignore (100%) rename {ci => mod/ci}/fmt.sh (77%) rename {ci => mod/ci}/image/Dockerfile (100%) rename {ci => mod/ci}/image/push.sh (100%) rename {ci => mod/ci}/lint.sh (65%) rename {ci => mod/ci}/run.sh (79%) rename {ci => mod/ci}/test.sh (95%) rename cmp_test.go => mod/cmp_test.go (100%) rename dial.go => mod/dial.go (100%) rename dial_test.go => mod/dial_test.go (100%) rename doc.go => mod/doc.go (100%) rename example_echo_test.go => mod/example_echo_test.go (100%) rename example_test.go => mod/example_test.go (100%) rename export_test.go => mod/export_test.go (100%) create mode 100644 mod/go.mod rename go.sum => mod/go.sum (72%) rename header.go => mod/header.go (100%) rename header_test.go => mod/header_test.go (100%) rename {internal => mod/internal}/bpool/bpool.go (100%) rename {internal => mod/internal}/bpool/bpool_test.go (100%) rename messagetype.go => mod/messagetype.go (100%) rename messagetype_string.go => mod/messagetype_string.go (100%) rename netconn.go => mod/netconn.go (100%) rename opcode.go => mod/opcode.go (100%) rename opcode_string.go => mod/opcode_string.go (100%) rename statuscode.go => mod/statuscode.go (100%) rename statuscode_string.go => mod/statuscode_string.go (100%) rename statuscode_test.go => mod/statuscode_test.go (100%) rename tools.go => mod/tools.go (93%) rename websocket.go => mod/websocket.go (100%) rename websocket_autobahn_python_test.go => mod/websocket_autobahn_python_test.go (100%) rename websocket_bench_test.go => mod/websocket_bench_test.go (100%) rename websocket_test.go => mod/websocket_test.go (100%) rename {wsjson => mod/wsjson}/wsjson.go (100%) rename {wspb => mod/wspb}/wspb.go (100%) rename xor.go => mod/xor.go (100%) rename xor_test.go => mod/xor_test.go (100%) diff --git a/.circleci/config.yml b/.circleci/config.yml index ec061964..6fee0320 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,15 +7,15 @@ jobs: - checkout - restore_cache: keys: - - go-v3-{{ checksum "go.sum" }} + - go-v4-{{ checksum "mod/go.sum" }} # Fallback to using the latest cache if no exact match is found. - - go-v3- - - run: ./ci/fmt.sh + - go-v4- + - run: ./mod/ci/fmt.sh - save_cache: paths: - /root/gopath - /root/.cache/go-build - key: go-v3-{{ checksum "go.sum" }} + key: go-v4-{{ checksum "mod/go.sum" }} lint: docker: @@ -24,15 +24,15 @@ jobs: - checkout - restore_cache: keys: - - go-v3-{{ checksum "go.sum" }} + - go-v4-{{ checksum "mod/go.sum" }} # Fallback to using the latest cache if no exact match is found. - - go-v3- - - run: ./ci/lint.sh + - go-v4- + - run: ./mod/ci/lint.sh - save_cache: paths: - /root/gopath - /root/.cache/go-build - key: go-v3-{{ checksum "go.sum" }} + key: go-v4-{{ checksum "mod/go.sum" }} test: docker: @@ -41,20 +41,20 @@ jobs: - checkout - restore_cache: keys: - - go-v3-{{ checksum "go.sum" }} + - go-v4-{{ checksum "mod/go.sum" }} # Fallback to using the latest cache if no exact match is found. - - go-v3- - - run: ./ci/test.sh + - go-v4- + - run: ./mod/ci/test.sh - store_artifacts: - path: ci/out + path: mod/ci/out destination: out - save_cache: paths: - /root/gopath - /root/.cache/go-build - key: go-v3-{{ checksum "go.sum" }} + key: go-v4-{{ checksum "mod/go.sum" }} - store_test_results: - path: ci/out + path: mod/ci/out workflows: version: 2 diff --git a/README.md b/README.md index 244315ef..b3313aa6 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ go get nhooyr.io/websocket ## Examples -For a production quality example that shows off the full API, see the [echo example on the godoc](https://godoc.org/nhooyr.io/websocket#example-package--Echo). On github, the example is at [example_echo_test.go](./example_echo_test.go). +For a production quality example that shows off the full API, see the [echo example on the godoc](https://godoc.org/nhooyr.io/websocket#example-package--Echo). On github, the example is at [./mod/example_echo_test.go](./mod/example_echo_test.go). Please use the [errors.As](https://golang.org/pkg/errors/#As) function [new in Go 1.13](https://golang.org/doc/go1.13#error_wrapping) to check for [websocket.CloseError](https://godoc.org/nhooyr.io/websocket#CloseError). See the [CloseError godoc example](https://godoc.org/nhooyr.io/websocket#example-CloseError). @@ -62,7 +62,7 @@ http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) { ### Client -The client side of this library requires at minimum Go 1.12 as it uses a [new feature +The client side of this library requires at least Go 1.12 as it uses a [new feature in net/http](https://github.com/golang/go/issues/26937#issuecomment-415855861) to perform WebSocket handshakes. ```go diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 74a25540..846c96f9 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -23,6 +23,9 @@ Be sure to [correctly link](https://help.github.com/en/articles/closing-issues-u to an existing issue if one exists. In general, create an issue before a PR to get some discussion going and to make sure you do not spend time on a PR that may be rejected. +The Go module where development occurs is in the [./mod](./mod) subpath so that the README.md +is immediately visible on the main page. + CI must pass on your changes for them to be merged. ### CI @@ -32,21 +35,21 @@ It will collect coverage and report it to [codecov](https://codecov.io/gh/nhooyr and also upload a `out/coverage.html` artifact that you can click on to interactively browse coverage. -You can run CI locally. The various steps are located in `ci/*.sh`. +You can run CI locally. The various steps are located in `mod/ci/*.sh`. -1. `ci/fmt.sh` which requires node (specifically prettier). -1. `ci/lint.sh` which requires [shellcheck](https://github.com/koalaman/shellcheck#installing). -1. `ci/test.sh` -1. `ci/run.sh` which runs the above scripts in order. +1. `mod/ci/fmt.sh` which requires node (specifically prettier). +1. `mod/ci/lint.sh` which requires [shellcheck](https://github.com/koalaman/shellcheck#installing). +1. `mod/ci/test.sh` +1. `mod/ci/run.sh` which runs the above scripts in order. -For coverage details locally, please see `ci/out/coverage.html` after running `ci/test.sh`. +For coverage details locally, please see `mod/ci/out/coverage.html` after running `mod/ci/test.sh`. -See [ci/image/Dockerfile](ci/image/Dockerfile) for the installation of the CI dependencies on Ubuntu. +See [mod/ci/image/Dockerfile](mod/ci/image/Dockerfile) for the installation of the CI dependencies on Ubuntu. -You can also run tests normally with `go test`. `ci/test.sh` just passes a default set of flags to +You can also run tests normally with `go test`. `mod/ci/test.sh` just passes a default set of flags to `go test` to collect coverage, enable the race detector and also prettifies the output. -You can pass flags to `ci/test.sh` if you want to run a specific test or otherwise +You can pass flags to `mod/ci/test.sh` if you want to run a specific test or otherwise control the behaviour of `go test`. Coverage percentage from codecov and the CI scripts will be different because they are calculated differently. diff --git a/go.mod b/go.mod deleted file mode 100644 index b59397c1..00000000 --- a/go.mod +++ /dev/null @@ -1,28 +0,0 @@ -module nhooyr.io/websocket - -go 1.13 - -require ( - github.com/fatih/color v1.7.0 // indirect - github.com/golang/protobuf v1.3.2 - github.com/google/go-cmp v0.3.1 - github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect - github.com/kr/pretty v0.1.0 // indirect - github.com/mattn/go-colorable v0.1.2 // indirect - github.com/mattn/go-isatty v0.0.9 // indirect - github.com/pkg/errors v0.8.1 // indirect - github.com/sirupsen/logrus v1.4.2 // indirect - github.com/spf13/pflag v1.0.3 // indirect - github.com/stretchr/testify v1.4.0 // indirect - go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16 - go.uber.org/atomic v1.4.0 // indirect - go.uber.org/multierr v1.1.0 - golang.org/x/lint v0.0.0-20190409202823-959b441ac422 - golang.org/x/sys v0.0.0-20190830142957-1e83adbbebd0 // indirect - golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 - golang.org/x/tools v0.0.0-20190830223141-573d9926052a - gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect - gotest.tools v2.2.0+incompatible // indirect - gotest.tools/gotestsum v0.3.6-0.20190825182939-fc6cb5870c52 - mvdan.cc/sh v2.6.4+incompatible -) diff --git a/accept.go b/mod/accept.go similarity index 100% rename from accept.go rename to mod/accept.go diff --git a/accept_test.go b/mod/accept_test.go similarity index 100% rename from accept_test.go rename to mod/accept_test.go diff --git a/ci/.codecov.yml b/mod/ci/.codecov.yml similarity index 100% rename from ci/.codecov.yml rename to mod/ci/.codecov.yml diff --git a/ci/.gitignore b/mod/ci/.gitignore similarity index 100% rename from ci/.gitignore rename to mod/ci/.gitignore diff --git a/ci/fmt.sh b/mod/ci/fmt.sh similarity index 77% rename from ci/fmt.sh rename to mod/ci/fmt.sh index a4e7ff02..52a194e9 100755 --- a/ci/fmt.sh +++ b/mod/ci/fmt.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash -set -euo pipefail +set -euox pipefail cd "$(dirname "${0}")" -cd "$(git rev-parse --show-toplevel)" +cd "$(git rev-parse --show-toplevel)/mod" gen() { # Unfortunately, this is the only way to ensure go.mod and go.sum are correct. @@ -16,7 +16,7 @@ gen() { fmt() { gofmt -w -s . go run go.coder.com/go-tools/cmd/goimports -w "-local=$(go list -m)" . - go run mvdan.cc/sh/cmd/shfmt -i 2 -w -s -sr . + go run mvdan.cc/sh/cmd/shfmt -i 2 -w -s -sr .. # shellcheck disable=SC2046 npx prettier \ --write \ @@ -24,11 +24,11 @@ fmt() { --no-semi \ --trailing-comma all \ --loglevel silent \ - $(git ls-files "*.yaml" "*.yml" "*.md") + $(git ls-files "../*.yaml" "../*.yml" "../*.md") } unstaged_files() { - git ls-files --other --modified --exclude-standard + git ls-files --other --modified --exclude-standard .. } check() { diff --git a/ci/image/Dockerfile b/mod/ci/image/Dockerfile similarity index 100% rename from ci/image/Dockerfile rename to mod/ci/image/Dockerfile diff --git a/ci/image/push.sh b/mod/ci/image/push.sh similarity index 100% rename from ci/image/push.sh rename to mod/ci/image/push.sh diff --git a/ci/lint.sh b/mod/ci/lint.sh similarity index 65% rename from ci/lint.sh rename to mod/ci/lint.sh index b7268c55..744783ff 100755 --- a/ci/lint.sh +++ b/mod/ci/lint.sh @@ -2,9 +2,9 @@ set -euo pipefail cd "$(dirname "${0}")" -cd "$(git rev-parse --show-toplevel)" +cd "$(git rev-parse --show-toplevel)/mod" # shellcheck disable=SC2046 -shellcheck -x $(git ls-files "*.sh") +shellcheck -x $(git ls-files "../*.sh") go vet ./... go run golang.org/x/lint/golint -set_exit_status ./... diff --git a/ci/run.sh b/mod/ci/run.sh similarity index 79% rename from ci/run.sh rename to mod/ci/run.sh index 56da2d93..f2eec485 100755 --- a/ci/run.sh +++ b/mod/ci/run.sh @@ -4,7 +4,7 @@ set -euo pipefail cd "$(dirname "${0}")" -cd "$(git rev-parse --show-toplevel)" +cd "$(git rev-parse --show-toplevel)/mod" ./ci/fmt.sh ./ci/lint.sh diff --git a/ci/test.sh b/mod/ci/test.sh similarity index 95% rename from ci/test.sh rename to mod/ci/test.sh index 7b611921..55b06810 100755 --- a/ci/test.sh +++ b/mod/ci/test.sh @@ -2,7 +2,7 @@ set -euo pipefail cd "$(dirname "${0}")" -cd "$(git rev-parse --show-toplevel)" +cd "$(git rev-parse --show-toplevel)/mod" argv=( go run gotest.tools/gotestsum diff --git a/cmp_test.go b/mod/cmp_test.go similarity index 100% rename from cmp_test.go rename to mod/cmp_test.go diff --git a/dial.go b/mod/dial.go similarity index 100% rename from dial.go rename to mod/dial.go diff --git a/dial_test.go b/mod/dial_test.go similarity index 100% rename from dial_test.go rename to mod/dial_test.go diff --git a/doc.go b/mod/doc.go similarity index 100% rename from doc.go rename to mod/doc.go diff --git a/example_echo_test.go b/mod/example_echo_test.go similarity index 100% rename from example_echo_test.go rename to mod/example_echo_test.go diff --git a/example_test.go b/mod/example_test.go similarity index 100% rename from example_test.go rename to mod/example_test.go diff --git a/export_test.go b/mod/export_test.go similarity index 100% rename from export_test.go rename to mod/export_test.go diff --git a/mod/go.mod b/mod/go.mod new file mode 100644 index 00000000..7d0c2e24 --- /dev/null +++ b/mod/go.mod @@ -0,0 +1,20 @@ +module nhooyr.io/websocket + +go 1.13 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/golang/protobuf v1.3.2 + github.com/google/go-cmp v0.3.1 + github.com/kr/pretty v0.1.0 // indirect + github.com/stretchr/testify v1.4.0 // indirect + go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16 + go.uber.org/atomic v1.4.0 // indirect + go.uber.org/multierr v1.1.0 + golang.org/x/lint v0.0.0-20190409202823-959b441ac422 + golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 + golang.org/x/tools v0.0.0-20190903163617-be0da057c5e3 + gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect + gotest.tools/gotestsum v0.3.5 + mvdan.cc/sh v2.6.4+incompatible +) diff --git a/go.sum b/mod/go.sum similarity index 72% rename from go.sum rename to mod/go.sum index 18b3dd70..00db27ea 100644 --- a/go.sum +++ b/mod/go.sum @@ -4,14 +4,11 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.6.0 h1:66qjqZk8kalYAvDRtM1AdAJQI0tj4Wrue3Eq3B3pmFU= github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/google/go-cmp v0.2.0 h1:+dTQ8DZQJz0Mb/HjFlkptS1FeQ4cWSnN941F8aEG4SQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -19,9 +16,6 @@ github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -29,13 +23,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= -github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -43,22 +32,13 @@ github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.0.5 h1:8c8b5uO0zS4X6RPl/sd1ENwSkIc0/H2PaHxE3udaE8I= github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= -github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -75,8 +55,8 @@ golang.org/x/lint v0.0.0-20190409202823-959b441ac422 h1:QzoH/1pFpZguR8NrRHLcO6jK golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= @@ -84,19 +64,13 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190830142957-1e83adbbebd0 h1:7z820YPX9pxWR59qM7BE5+fglp4D/mKqAwCvGt11b+8= -golang.org/x/sys v0.0.0-20190830142957-1e83adbbebd0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190830223141-573d9926052a h1:XAHT1kdPpnU8Hk+FPi42KZFhtNFEk4vBg1U4OmIeHTU= -golang.org/x/tools v0.0.0-20190830223141-573d9926052a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= +golang.org/x/tools v0.0.0-20190903163617-be0da057c5e3 h1:1cLrGl9PL64Mzl9NATDCqFE57dVYwWOkoPXvppEnjO4= +golang.org/x/tools v0.0.0-20190903163617-be0da057c5e3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= @@ -110,15 +84,12 @@ gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNj gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gotest.tools v2.1.0+incompatible h1:5USw7CrJBYKqjg9R7QlA6jzqZKEAtvW82aNmsxxGPxw= gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= -gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -gotest.tools/gotestsum v0.3.6-0.20190825182939-fc6cb5870c52 h1:Qr31uPFyjpOhAgRfKV4ATUnknnLT2X7HFjqwkstdbbE= -gotest.tools/gotestsum v0.3.6-0.20190825182939-fc6cb5870c52/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY= +gotest.tools/gotestsum v0.3.5 h1:VePOWRsuWFYpfp/G8mbmOZKxO5T3501SEGQRUdvq7h0= +gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY= mvdan.cc/sh v2.6.4+incompatible h1:eD6tDeh0pw+/TOTI1BBEryZ02rD2nMcFsgcvde7jffM= mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8= diff --git a/header.go b/mod/header.go similarity index 100% rename from header.go rename to mod/header.go diff --git a/header_test.go b/mod/header_test.go similarity index 100% rename from header_test.go rename to mod/header_test.go diff --git a/internal/bpool/bpool.go b/mod/internal/bpool/bpool.go similarity index 100% rename from internal/bpool/bpool.go rename to mod/internal/bpool/bpool.go diff --git a/internal/bpool/bpool_test.go b/mod/internal/bpool/bpool_test.go similarity index 100% rename from internal/bpool/bpool_test.go rename to mod/internal/bpool/bpool_test.go diff --git a/messagetype.go b/mod/messagetype.go similarity index 100% rename from messagetype.go rename to mod/messagetype.go diff --git a/messagetype_string.go b/mod/messagetype_string.go similarity index 100% rename from messagetype_string.go rename to mod/messagetype_string.go diff --git a/netconn.go b/mod/netconn.go similarity index 100% rename from netconn.go rename to mod/netconn.go diff --git a/opcode.go b/mod/opcode.go similarity index 100% rename from opcode.go rename to mod/opcode.go diff --git a/opcode_string.go b/mod/opcode_string.go similarity index 100% rename from opcode_string.go rename to mod/opcode_string.go diff --git a/statuscode.go b/mod/statuscode.go similarity index 100% rename from statuscode.go rename to mod/statuscode.go diff --git a/statuscode_string.go b/mod/statuscode_string.go similarity index 100% rename from statuscode_string.go rename to mod/statuscode_string.go diff --git a/statuscode_test.go b/mod/statuscode_test.go similarity index 100% rename from statuscode_test.go rename to mod/statuscode_test.go diff --git a/tools.go b/mod/tools.go similarity index 93% rename from tools.go rename to mod/tools.go index a6f0268e..0ba3f40c 100644 --- a/tools.go +++ b/mod/tools.go @@ -1,6 +1,6 @@ // +build tools -package tools +package websocket // See https://github.com/go-modules-by-example/index/blob/master/010_tools/README.md import ( diff --git a/websocket.go b/mod/websocket.go similarity index 100% rename from websocket.go rename to mod/websocket.go diff --git a/websocket_autobahn_python_test.go b/mod/websocket_autobahn_python_test.go similarity index 100% rename from websocket_autobahn_python_test.go rename to mod/websocket_autobahn_python_test.go diff --git a/websocket_bench_test.go b/mod/websocket_bench_test.go similarity index 100% rename from websocket_bench_test.go rename to mod/websocket_bench_test.go diff --git a/websocket_test.go b/mod/websocket_test.go similarity index 100% rename from websocket_test.go rename to mod/websocket_test.go diff --git a/wsjson/wsjson.go b/mod/wsjson/wsjson.go similarity index 100% rename from wsjson/wsjson.go rename to mod/wsjson/wsjson.go diff --git a/wspb/wspb.go b/mod/wspb/wspb.go similarity index 100% rename from wspb/wspb.go rename to mod/wspb/wspb.go diff --git a/xor.go b/mod/xor.go similarity index 100% rename from xor.go rename to mod/xor.go diff --git a/xor_test.go b/mod/xor_test.go similarity index 100% rename from xor_test.go rename to mod/xor_test.go From b18866394a198734d57d5e10bccfcc341ff150ea Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Sep 2019 23:42:27 -0500 Subject: [PATCH 117/519] Simplify README Users section further --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b3313aa6..5792671a 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ Please see [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md). ## Users +If your company or project is using this library, please feel free to open an issue or PR to amend the list. + - [Coder](https://github.com/cdr) - [Tatsu Works](https://github.com/tatsuworks) - Ingresses 20 TB in websocket data every month on their Discord bot. - -If your company or project is using this library, please feel free to open an issue or PR to amend the list. From 3e84a7c6b35a2cf8406082a48ba92f23a8e2b032 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 3 Sep 2019 23:58:52 -0500 Subject: [PATCH 118/519] Fix grammar in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5792671a..bfbbb226 100644 --- a/README.md +++ b/README.md @@ -169,7 +169,7 @@ Please see [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md). ## Users -If your company or project is using this library, please feel free to open an issue or PR to amend the list. +If your company or project is using this library, please feel free to open an issue or PR to amend this list. - [Coder](https://github.com/cdr) - [Tatsu Works](https://github.com/tatsuworks) - Ingresses 20 TB in websocket data every month on their Discord bot. From a926a7f4daa8b5b6bf4615de96720366d3b12c5d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 4 Sep 2019 00:10:43 -0500 Subject: [PATCH 119/519] Fix CI coverage --- mod/ci/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mod/ci/test.sh b/mod/ci/test.sh index 55b06810..53028dd7 100755 --- a/mod/ci/test.sh +++ b/mod/ci/test.sh @@ -33,5 +33,5 @@ mv ci/out/coverage2.prof ci/out/coverage.prof go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html if [[ ${CI:-} ]]; then - bash <(curl -s https://codecov.io/bash) -f ci/out/coverage.prof + bash <(curl -s https://codecov.io/bash) -R . -f ci/out/coverage.prof fi From 7f0bc4ca4edca0287d345dfcde7d028e20ae9451 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 4 Sep 2019 00:15:47 -0500 Subject: [PATCH 120/519] Move tools.go to ci dir --- mod/{ => ci}/tools.go | 2 +- mod/go.mod | 9 ++++++++- mod/go.sum | 24 ++++++++++++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) rename mod/{ => ci}/tools.go (93%) diff --git a/mod/tools.go b/mod/ci/tools.go similarity index 93% rename from mod/tools.go rename to mod/ci/tools.go index 0ba3f40c..5aebe7d4 100644 --- a/mod/tools.go +++ b/mod/ci/tools.go @@ -1,6 +1,6 @@ // +build tools -package websocket +package ci // See https://github.com/go-modules-by-example/index/blob/master/010_tools/README.md import ( diff --git a/mod/go.mod b/mod/go.mod index 7d0c2e24..d9c48955 100644 --- a/mod/go.mod +++ b/mod/go.mod @@ -3,15 +3,22 @@ module nhooyr.io/websocket go 1.13 require ( - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fatih/color v1.7.0 // indirect github.com/golang/protobuf v1.3.2 github.com/google/go-cmp v0.3.1 + github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect github.com/kr/pretty v0.1.0 // indirect + github.com/mattn/go-colorable v0.1.2 // indirect + github.com/mattn/go-isatty v0.0.9 // indirect + github.com/pkg/errors v0.8.1 // indirect + github.com/sirupsen/logrus v1.4.2 // indirect + github.com/spf13/pflag v1.0.3 // indirect github.com/stretchr/testify v1.4.0 // indirect go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16 go.uber.org/atomic v1.4.0 // indirect go.uber.org/multierr v1.1.0 golang.org/x/lint v0.0.0-20190409202823-959b441ac422 + golang.org/x/sys v0.0.0-20190904005037-43c01164e931 // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 golang.org/x/tools v0.0.0-20190903163617-be0da057c5e3 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect diff --git a/mod/go.sum b/mod/go.sum index 00db27ea..03310997 100644 --- a/mod/go.sum +++ b/mod/go.sum @@ -4,6 +4,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.6.0 h1:66qjqZk8kalYAvDRtM1AdAJQI0tj4Wrue3Eq3B3pmFU= github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -16,6 +18,10 @@ github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= +github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -23,8 +29,13 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= +github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= @@ -32,13 +43,21 @@ github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sirupsen/logrus v1.0.5 h1:8c8b5uO0zS4X6RPl/sd1ENwSkIc0/H2PaHxE3udaE8I= github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= +github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= +github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -64,6 +83,11 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190904005037-43c01164e931 h1:+WYfosiOJzB4BjsISl1Rv4ZLUy+VYcF+u+0Y9jcerv8= +golang.org/x/sys v0.0.0-20190904005037-43c01164e931/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= From a134edc0d8e463e5975d35f05894270dc6e242ea Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 4 Sep 2019 00:26:02 -0500 Subject: [PATCH 121/519] Fix godoc link in README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index bfbbb226..57e3eff6 100644 --- a/README.md +++ b/README.md @@ -110,7 +110,7 @@ https://github.com/gorilla/websocket This package is the community standard but it is 6 years old and over time has accumulated cruft. There are too many ways to do the same thing. Just compare the godoc of -[nhooyr/websocket](https://godoc.org/github.com/nhooyr/websocket) side by side with +[nhooyr/websocket](https://godoc.org/nhooyr.io/websocket) side by side with [gorilla/websocket](https://godoc.org/github.com/gorilla/websocket). The API for nhooyr/websocket has been designed such that there is only one way to do things From 727dd542eeecbcabd26f6dc0b421aec28fc86af3 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 4 Sep 2019 00:38:17 -0500 Subject: [PATCH 122/519] Cleanup CONTRIBUTING.md --- docs/CONTRIBUTING.md | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 846c96f9..3e5ef135 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -23,7 +23,7 @@ Be sure to [correctly link](https://help.github.com/en/articles/closing-issues-u to an existing issue if one exists. In general, create an issue before a PR to get some discussion going and to make sure you do not spend time on a PR that may be rejected. -The Go module where development occurs is in the [./mod](./mod) subpath so that the README.md +The Go module where development occurs is in the [mod](../mod) subpath so that the README.md is immediately visible on the main page. CI must pass on your changes for them to be merged. @@ -35,21 +35,21 @@ It will collect coverage and report it to [codecov](https://codecov.io/gh/nhooyr and also upload a `out/coverage.html` artifact that you can click on to interactively browse coverage. -You can run CI locally. The various steps are located in `mod/ci/*.sh`. +You can run CI locally. The various steps are located in [mod/ci/\*.sh](../mod/ci). -1. `mod/ci/fmt.sh` which requires node (specifically prettier). -1. `mod/ci/lint.sh` which requires [shellcheck](https://github.com/koalaman/shellcheck#installing). -1. `mod/ci/test.sh` -1. `mod/ci/run.sh` which runs the above scripts in order. +1. [fmt.sh](../mod/ci/fmt.sh) which requires node (specifically prettier). +1. [lint.sh](../mod/ci/lint.sh) which requires [shellcheck](https://github.com/koalaman/shellcheck#installing). +1. [test.sh](../mod/ci/test.sh) +1. [run.sh](../mod/ci/run.sh) which runs the above scripts in order. -For coverage details locally, please see `mod/ci/out/coverage.html` after running `mod/ci/test.sh`. +For coverage details locally, please see `mod/ci/out/coverage.html` after running [test.sh](../mod/ci/test.sh). -See [mod/ci/image/Dockerfile](mod/ci/image/Dockerfile) for the installation of the CI dependencies on Ubuntu. +See [mod/ci/image/Dockerfile](../mod/ci/image/Dockerfile) for the installation of the CI dependencies on Ubuntu. -You can also run tests normally with `go test`. `mod/ci/test.sh` just passes a default set of flags to -`go test` to collect coverage, enable the race detector and also prettifies the output. +You can also run tests normally with `go test`. [test.sh](../mod/ci/test.sh) just passes a default set of flags to +`go test` to collect coverage and also prettify the output. -You can pass flags to `mod/ci/test.sh` if you want to run a specific test or otherwise -control the behaviour of `go test`. +You can pass flags to [test.sh](mod/ci/test.sh) if you want to run a specific test or otherwise +control the behaviour of `go test` but also get coverage. Coverage percentage from codecov and the CI scripts will be different because they are calculated differently. From 8666e31b5cb39b967dd43399213a454e7f106e9c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 4 Sep 2019 01:16:29 -0500 Subject: [PATCH 123/519] Cleanup docs further --- README.md | 2 +- docs/CONTRIBUTING.md | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 57e3eff6..93600f61 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ go get nhooyr.io/websocket ## Examples -For a production quality example that shows off the full API, see the [echo example on the godoc](https://godoc.org/nhooyr.io/websocket#example-package--Echo). On github, the example is at [./mod/example_echo_test.go](./mod/example_echo_test.go). +For a production quality example that shows off the full API, see the [echo example on the godoc](https://godoc.org/nhooyr.io/websocket#example-package--Echo). On github, the example is at [mod/example_echo_test.go](./mod/example_echo_test.go). Please use the [errors.As](https://golang.org/pkg/errors/#As) function [new in Go 1.13](https://golang.org/doc/go1.13#error_wrapping) to check for [websocket.CloseError](https://godoc.org/nhooyr.io/websocket#CloseError). See the [CloseError godoc example](https://godoc.org/nhooyr.io/websocket#example-CloseError). diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 3e5ef135..df79622b 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -24,7 +24,9 @@ to an existing issue if one exists. In general, create an issue before a PR to g discussion going and to make sure you do not spend time on a PR that may be rejected. The Go module where development occurs is in the [mod](../mod) subpath so that the README.md -is immediately visible on the main page. +is immediately visible on the main page. I regularly clone the repo with mod as the root +to [nhooyr/websocket-mod](https://github.com/nhooyr/websocket-mod) so that all tooling works +normally. CI must pass on your changes for them to be merged. From 3ca22b55c84bb2c1bb4bc76f8a6aada9395de0c6 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 4 Sep 2019 10:46:12 -0500 Subject: [PATCH 124/519] Rename docs to .github --- {docs => .github}/CODEOWNERS.txt | 0 {docs => .github}/CONTRIBUTING.md | 0 {docs => .github}/ISSUE_TEMPLATE.md | 0 .github/PULL_REQUEST_TEMPLATE.md | 2 ++ README.md | 2 +- docs/PULL_REQUEST_TEMPLATE.md | 2 -- 6 files changed, 3 insertions(+), 3 deletions(-) rename {docs => .github}/CODEOWNERS.txt (100%) rename {docs => .github}/CONTRIBUTING.md (100%) rename {docs => .github}/ISSUE_TEMPLATE.md (100%) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md delete mode 100644 docs/PULL_REQUEST_TEMPLATE.md diff --git a/docs/CODEOWNERS.txt b/.github/CODEOWNERS.txt similarity index 100% rename from docs/CODEOWNERS.txt rename to .github/CODEOWNERS.txt diff --git a/docs/CONTRIBUTING.md b/.github/CONTRIBUTING.md similarity index 100% rename from docs/CONTRIBUTING.md rename to .github/CONTRIBUTING.md diff --git a/docs/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md similarity index 100% rename from docs/ISSUE_TEMPLATE.md rename to .github/ISSUE_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..12e6e7a0 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,2 @@ + + diff --git a/README.md b/README.md index 93600f61..69c32854 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,7 @@ as performant but much easier to use correctly and idiomatic. ## Contributing -Please see [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md). +Please see [.github/CONTRIBUTING.md](.github/CONTRIBUTING.md). ## Users diff --git a/docs/PULL_REQUEST_TEMPLATE.md b/docs/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 5f828acb..00000000 --- a/docs/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,2 +0,0 @@ - - From 3e4e409aa425c0164f3c70558170fc3a7ce94657 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 4 Sep 2019 11:11:04 -0500 Subject: [PATCH 125/519] Move everything back to the root The boys in the office hated it. Highly unfortunate but makes sense as it seems confusing. --- .circleci/config.yml | 22 +++++++++--------- .github/CONTRIBUTING.md | 23 ++++++++----------- README.md | 2 +- mod/accept.go => accept.go | 0 mod/accept_test.go => accept_test.go | 0 {mod/ci => ci}/.codecov.yml | 0 {mod/ci => ci}/.gitignore | 0 {mod/ci => ci}/fmt.sh | 8 +++---- {mod/ci => ci}/image/Dockerfile | 0 {mod/ci => ci}/image/push.sh | 0 {mod/ci => ci}/lint.sh | 4 ++-- {mod/ci => ci}/run.sh | 2 +- {mod/ci => ci}/test.sh | 2 +- {mod/ci => ci}/tools.go | 0 mod/cmp_test.go => cmp_test.go | 0 mod/dial.go => dial.go | 0 mod/dial_test.go => dial_test.go | 0 mod/doc.go => doc.go | 0 ...ample_echo_test.go => example_echo_test.go | 0 mod/example_test.go => example_test.go | 0 mod/export_test.go => export_test.go | 0 mod/go.mod => go.mod | 0 mod/go.sum => go.sum | 0 mod/header.go => header.go | 0 mod/header_test.go => header_test.go | 0 {mod/internal => internal}/bpool/bpool.go | 0 .../internal => internal}/bpool/bpool_test.go | 0 mod/messagetype.go => messagetype.go | 0 ...agetype_string.go => messagetype_string.go | 0 mod/netconn.go => netconn.go | 0 mod/opcode.go => opcode.go | 0 mod/opcode_string.go => opcode_string.go | 0 mod/statuscode.go => statuscode.go | 0 ...atuscode_string.go => statuscode_string.go | 0 mod/statuscode_test.go => statuscode_test.go | 0 mod/websocket.go => websocket.go | 0 ...st.go => websocket_autobahn_python_test.go | 16 ++++++------- ...t_bench_test.go => websocket_bench_test.go | 0 mod/websocket_test.go => websocket_test.go | 0 {mod/wsjson => wsjson}/wsjson.go | 0 {mod/wspb => wspb}/wspb.go | 0 mod/xor.go => xor.go | 0 mod/xor_test.go => xor_test.go | 0 43 files changed, 36 insertions(+), 43 deletions(-) rename mod/accept.go => accept.go (100%) rename mod/accept_test.go => accept_test.go (100%) rename {mod/ci => ci}/.codecov.yml (100%) rename {mod/ci => ci}/.gitignore (100%) rename {mod/ci => ci}/fmt.sh (82%) rename {mod/ci => ci}/image/Dockerfile (100%) rename {mod/ci => ci}/image/push.sh (100%) rename {mod/ci => ci}/lint.sh (65%) rename {mod/ci => ci}/run.sh (79%) rename {mod/ci => ci}/test.sh (95%) rename {mod/ci => ci}/tools.go (100%) rename mod/cmp_test.go => cmp_test.go (100%) rename mod/dial.go => dial.go (100%) rename mod/dial_test.go => dial_test.go (100%) rename mod/doc.go => doc.go (100%) rename mod/example_echo_test.go => example_echo_test.go (100%) rename mod/example_test.go => example_test.go (100%) rename mod/export_test.go => export_test.go (100%) rename mod/go.mod => go.mod (100%) rename mod/go.sum => go.sum (100%) rename mod/header.go => header.go (100%) rename mod/header_test.go => header_test.go (100%) rename {mod/internal => internal}/bpool/bpool.go (100%) rename {mod/internal => internal}/bpool/bpool_test.go (100%) rename mod/messagetype.go => messagetype.go (100%) rename mod/messagetype_string.go => messagetype_string.go (100%) rename mod/netconn.go => netconn.go (100%) rename mod/opcode.go => opcode.go (100%) rename mod/opcode_string.go => opcode_string.go (100%) rename mod/statuscode.go => statuscode.go (100%) rename mod/statuscode_string.go => statuscode_string.go (100%) rename mod/statuscode_test.go => statuscode_test.go (100%) rename mod/websocket.go => websocket.go (100%) rename mod/websocket_autobahn_python_test.go => websocket_autobahn_python_test.go (91%) rename mod/websocket_bench_test.go => websocket_bench_test.go (100%) rename mod/websocket_test.go => websocket_test.go (100%) rename {mod/wsjson => wsjson}/wsjson.go (100%) rename {mod/wspb => wspb}/wspb.go (100%) rename mod/xor.go => xor.go (100%) rename mod/xor_test.go => xor_test.go (100%) diff --git a/.circleci/config.yml b/.circleci/config.yml index 6fee0320..0209153a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -7,15 +7,15 @@ jobs: - checkout - restore_cache: keys: - - go-v4-{{ checksum "mod/go.sum" }} + - go-v4-{{ checksum "go.sum" }} # Fallback to using the latest cache if no exact match is found. - go-v4- - - run: ./mod/ci/fmt.sh + - run: ./ci/fmt.sh - save_cache: paths: - /root/gopath - /root/.cache/go-build - key: go-v4-{{ checksum "mod/go.sum" }} + key: go-v4-{{ checksum "go.sum" }} lint: docker: @@ -24,15 +24,15 @@ jobs: - checkout - restore_cache: keys: - - go-v4-{{ checksum "mod/go.sum" }} + - go-v4-{{ checksum "go.sum" }} # Fallback to using the latest cache if no exact match is found. - go-v4- - - run: ./mod/ci/lint.sh + - run: ./ci/lint.sh - save_cache: paths: - /root/gopath - /root/.cache/go-build - key: go-v4-{{ checksum "mod/go.sum" }} + key: go-v4-{{ checksum "go.sum" }} test: docker: @@ -41,20 +41,20 @@ jobs: - checkout - restore_cache: keys: - - go-v4-{{ checksum "mod/go.sum" }} + - go-v4-{{ checksum "go.sum" }} # Fallback to using the latest cache if no exact match is found. - go-v4- - - run: ./mod/ci/test.sh + - run: ./ci/test.sh - store_artifacts: - path: mod/ci/out + path: ci/out destination: out - save_cache: paths: - /root/gopath - /root/.cache/go-build - key: go-v4-{{ checksum "mod/go.sum" }} + key: go-v4-{{ checksum "go.sum" }} - store_test_results: - path: mod/ci/out + path: ci/out workflows: version: 2 diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index df79622b..3bc9f82b 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -23,11 +23,6 @@ Be sure to [correctly link](https://help.github.com/en/articles/closing-issues-u to an existing issue if one exists. In general, create an issue before a PR to get some discussion going and to make sure you do not spend time on a PR that may be rejected. -The Go module where development occurs is in the [mod](../mod) subpath so that the README.md -is immediately visible on the main page. I regularly clone the repo with mod as the root -to [nhooyr/websocket-mod](https://github.com/nhooyr/websocket-mod) so that all tooling works -normally. - CI must pass on your changes for them to be merged. ### CI @@ -37,21 +32,21 @@ It will collect coverage and report it to [codecov](https://codecov.io/gh/nhooyr and also upload a `out/coverage.html` artifact that you can click on to interactively browse coverage. -You can run CI locally. The various steps are located in [mod/ci/\*.sh](../mod/ci). +You can run CI locally. The various steps are located in [ci/\*.sh](../ci). -1. [fmt.sh](../mod/ci/fmt.sh) which requires node (specifically prettier). -1. [lint.sh](../mod/ci/lint.sh) which requires [shellcheck](https://github.com/koalaman/shellcheck#installing). -1. [test.sh](../mod/ci/test.sh) -1. [run.sh](../mod/ci/run.sh) which runs the above scripts in order. +1. [fmt.sh](../ci/fmt.sh) which requires node (specifically prettier). +1. [lint.sh](../ci/lint.sh) which requires [shellcheck](https://github.com/koalaman/shellcheck#installing). +1. [test.sh](../ci/test.sh) +1. [run.sh](../ci/run.sh) which runs the above scripts in order. -For coverage details locally, please see `mod/ci/out/coverage.html` after running [test.sh](../mod/ci/test.sh). +For coverage details locally, please see `ci/out/coverage.html` after running [test.sh](../ci/test.sh). -See [mod/ci/image/Dockerfile](../mod/ci/image/Dockerfile) for the installation of the CI dependencies on Ubuntu. +See [ci/image/Dockerfile](../ci/image/Dockerfile) for the installation of the CI dependencies on Ubuntu. -You can also run tests normally with `go test`. [test.sh](../mod/ci/test.sh) just passes a default set of flags to +You can also run tests normally with `go test`. [test.sh](../ci/test.sh) just passes a default set of flags to `go test` to collect coverage and also prettify the output. -You can pass flags to [test.sh](mod/ci/test.sh) if you want to run a specific test or otherwise +You can pass flags to [test.sh](ci/test.sh) if you want to run a specific test or otherwise control the behaviour of `go test` but also get coverage. Coverage percentage from codecov and the CI scripts will be different because they are calculated differently. diff --git a/README.md b/README.md index 69c32854..a16cf2ed 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ go get nhooyr.io/websocket ## Examples -For a production quality example that shows off the full API, see the [echo example on the godoc](https://godoc.org/nhooyr.io/websocket#example-package--Echo). On github, the example is at [mod/example_echo_test.go](./mod/example_echo_test.go). +For a production quality example that shows off the full API, see the [echo example on the godoc](https://godoc.org/nhooyr.io/websocket#example-package--Echo). On github, the example is at [example_echo_test.go](./example_echo_test.go). Please use the [errors.As](https://golang.org/pkg/errors/#As) function [new in Go 1.13](https://golang.org/doc/go1.13#error_wrapping) to check for [websocket.CloseError](https://godoc.org/nhooyr.io/websocket#CloseError). See the [CloseError godoc example](https://godoc.org/nhooyr.io/websocket#example-CloseError). diff --git a/mod/accept.go b/accept.go similarity index 100% rename from mod/accept.go rename to accept.go diff --git a/mod/accept_test.go b/accept_test.go similarity index 100% rename from mod/accept_test.go rename to accept_test.go diff --git a/mod/ci/.codecov.yml b/ci/.codecov.yml similarity index 100% rename from mod/ci/.codecov.yml rename to ci/.codecov.yml diff --git a/mod/ci/.gitignore b/ci/.gitignore similarity index 100% rename from mod/ci/.gitignore rename to ci/.gitignore diff --git a/mod/ci/fmt.sh b/ci/fmt.sh similarity index 82% rename from mod/ci/fmt.sh rename to ci/fmt.sh index 52a194e9..4eda00fc 100755 --- a/mod/ci/fmt.sh +++ b/ci/fmt.sh @@ -1,8 +1,8 @@ #!/usr/bin/env bash -set -euox pipefail +set -euo pipefail cd "$(dirname "${0}")" -cd "$(git rev-parse --show-toplevel)/mod" +cd "$(git rev-parse --show-toplevel)" gen() { # Unfortunately, this is the only way to ensure go.mod and go.sum are correct. @@ -24,11 +24,11 @@ fmt() { --no-semi \ --trailing-comma all \ --loglevel silent \ - $(git ls-files "../*.yaml" "../*.yml" "../*.md") + $(git ls-files "*.yaml" "*.yml" "*.md") } unstaged_files() { - git ls-files --other --modified --exclude-standard .. + git ls-files --other --modified --exclude-standard } check() { diff --git a/mod/ci/image/Dockerfile b/ci/image/Dockerfile similarity index 100% rename from mod/ci/image/Dockerfile rename to ci/image/Dockerfile diff --git a/mod/ci/image/push.sh b/ci/image/push.sh similarity index 100% rename from mod/ci/image/push.sh rename to ci/image/push.sh diff --git a/mod/ci/lint.sh b/ci/lint.sh similarity index 65% rename from mod/ci/lint.sh rename to ci/lint.sh index 744783ff..b7268c55 100755 --- a/mod/ci/lint.sh +++ b/ci/lint.sh @@ -2,9 +2,9 @@ set -euo pipefail cd "$(dirname "${0}")" -cd "$(git rev-parse --show-toplevel)/mod" +cd "$(git rev-parse --show-toplevel)" # shellcheck disable=SC2046 -shellcheck -x $(git ls-files "../*.sh") +shellcheck -x $(git ls-files "*.sh") go vet ./... go run golang.org/x/lint/golint -set_exit_status ./... diff --git a/mod/ci/run.sh b/ci/run.sh similarity index 79% rename from mod/ci/run.sh rename to ci/run.sh index f2eec485..56da2d93 100755 --- a/mod/ci/run.sh +++ b/ci/run.sh @@ -4,7 +4,7 @@ set -euo pipefail cd "$(dirname "${0}")" -cd "$(git rev-parse --show-toplevel)/mod" +cd "$(git rev-parse --show-toplevel)" ./ci/fmt.sh ./ci/lint.sh diff --git a/mod/ci/test.sh b/ci/test.sh similarity index 95% rename from mod/ci/test.sh rename to ci/test.sh index 53028dd7..fca42ff0 100755 --- a/mod/ci/test.sh +++ b/ci/test.sh @@ -2,7 +2,7 @@ set -euo pipefail cd "$(dirname "${0}")" -cd "$(git rev-parse --show-toplevel)/mod" +cd "$(git rev-parse --show-toplevel)" argv=( go run gotest.tools/gotestsum diff --git a/mod/ci/tools.go b/ci/tools.go similarity index 100% rename from mod/ci/tools.go rename to ci/tools.go diff --git a/mod/cmp_test.go b/cmp_test.go similarity index 100% rename from mod/cmp_test.go rename to cmp_test.go diff --git a/mod/dial.go b/dial.go similarity index 100% rename from mod/dial.go rename to dial.go diff --git a/mod/dial_test.go b/dial_test.go similarity index 100% rename from mod/dial_test.go rename to dial_test.go diff --git a/mod/doc.go b/doc.go similarity index 100% rename from mod/doc.go rename to doc.go diff --git a/mod/example_echo_test.go b/example_echo_test.go similarity index 100% rename from mod/example_echo_test.go rename to example_echo_test.go diff --git a/mod/example_test.go b/example_test.go similarity index 100% rename from mod/example_test.go rename to example_test.go diff --git a/mod/export_test.go b/export_test.go similarity index 100% rename from mod/export_test.go rename to export_test.go diff --git a/mod/go.mod b/go.mod similarity index 100% rename from mod/go.mod rename to go.mod diff --git a/mod/go.sum b/go.sum similarity index 100% rename from mod/go.sum rename to go.sum diff --git a/mod/header.go b/header.go similarity index 100% rename from mod/header.go rename to header.go diff --git a/mod/header_test.go b/header_test.go similarity index 100% rename from mod/header_test.go rename to header_test.go diff --git a/mod/internal/bpool/bpool.go b/internal/bpool/bpool.go similarity index 100% rename from mod/internal/bpool/bpool.go rename to internal/bpool/bpool.go diff --git a/mod/internal/bpool/bpool_test.go b/internal/bpool/bpool_test.go similarity index 100% rename from mod/internal/bpool/bpool_test.go rename to internal/bpool/bpool_test.go diff --git a/mod/messagetype.go b/messagetype.go similarity index 100% rename from mod/messagetype.go rename to messagetype.go diff --git a/mod/messagetype_string.go b/messagetype_string.go similarity index 100% rename from mod/messagetype_string.go rename to messagetype_string.go diff --git a/mod/netconn.go b/netconn.go similarity index 100% rename from mod/netconn.go rename to netconn.go diff --git a/mod/opcode.go b/opcode.go similarity index 100% rename from mod/opcode.go rename to opcode.go diff --git a/mod/opcode_string.go b/opcode_string.go similarity index 100% rename from mod/opcode_string.go rename to opcode_string.go diff --git a/mod/statuscode.go b/statuscode.go similarity index 100% rename from mod/statuscode.go rename to statuscode.go diff --git a/mod/statuscode_string.go b/statuscode_string.go similarity index 100% rename from mod/statuscode_string.go rename to statuscode_string.go diff --git a/mod/statuscode_test.go b/statuscode_test.go similarity index 100% rename from mod/statuscode_test.go rename to statuscode_test.go diff --git a/mod/websocket.go b/websocket.go similarity index 100% rename from mod/websocket.go rename to websocket.go diff --git a/mod/websocket_autobahn_python_test.go b/websocket_autobahn_python_test.go similarity index 91% rename from mod/websocket_autobahn_python_test.go rename to websocket_autobahn_python_test.go index 0512c85a..ae7e3022 100644 --- a/mod/websocket_autobahn_python_test.go +++ b/websocket_autobahn_python_test.go @@ -19,8 +19,6 @@ import ( "strings" "testing" "time" - - "nhooyr.io/websocket" ) // https://github.com/crossbario/autobahn-python/tree/master/wstest @@ -28,7 +26,7 @@ func TestPythonAutobahnServer(t *testing.T) { t.Parallel() s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + c, err := Accept(w, r, &AcceptOptions{ Subprotocols: []string{"echo"}, }) if err != nil { @@ -156,11 +154,11 @@ func TestPythonAutobahnClientOld(t *testing.T) { var cases int func() { - c, _, err := websocket.Dial(ctx, wsServerURL+"/getCaseCount", nil) + c, _, err := Dial(ctx, wsServerURL+"/getCaseCount", nil) if err != nil { t.Fatal(err) } - defer c.Close(websocket.StatusInternalError, "") + defer c.Close(StatusInternalError, "") _, r, err := c.Reader(ctx) if err != nil { @@ -175,7 +173,7 @@ func TestPythonAutobahnClientOld(t *testing.T) { t.Fatal(err) } - c.Close(websocket.StatusNormalClosure, "") + c.Close(StatusNormalClosure, "") }() for i := 1; i <= cases; i++ { @@ -183,7 +181,7 @@ func TestPythonAutobahnClientOld(t *testing.T) { ctx, cancel := context.WithTimeout(ctx, time.Second*45) defer cancel() - c, _, err := websocket.Dial(ctx, fmt.Sprintf(wsServerURL+"/runCase?case=%v&agent=main", i), nil) + c, _, err := Dial(ctx, fmt.Sprintf(wsServerURL+"/runCase?case=%v&agent=main", i), nil) if err != nil { t.Fatal(err) } @@ -191,11 +189,11 @@ func TestPythonAutobahnClientOld(t *testing.T) { }() } - c, _, err := websocket.Dial(ctx, fmt.Sprintf(wsServerURL+"/updateReports?agent=main"), nil) + c, _, err := Dial(ctx, fmt.Sprintf(wsServerURL+"/updateReports?agent=main"), nil) if err != nil { t.Fatal(err) } - c.Close(websocket.StatusNormalClosure, "") + c.Close(StatusNormalClosure, "") checkWSTestIndex(t, "./ci/out/wstestClientReports/index.json") } diff --git a/mod/websocket_bench_test.go b/websocket_bench_test.go similarity index 100% rename from mod/websocket_bench_test.go rename to websocket_bench_test.go diff --git a/mod/websocket_test.go b/websocket_test.go similarity index 100% rename from mod/websocket_test.go rename to websocket_test.go diff --git a/mod/wsjson/wsjson.go b/wsjson/wsjson.go similarity index 100% rename from mod/wsjson/wsjson.go rename to wsjson/wsjson.go diff --git a/mod/wspb/wspb.go b/wspb/wspb.go similarity index 100% rename from mod/wspb/wspb.go rename to wspb/wspb.go diff --git a/mod/xor.go b/xor.go similarity index 100% rename from mod/xor.go rename to xor.go diff --git a/mod/xor_test.go b/xor_test.go similarity index 100% rename from mod/xor_test.go rename to xor_test.go From f4f1a8c0a6730cf3e15f8116b1a0005d29bfe384 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 4 Sep 2019 14:34:59 -0500 Subject: [PATCH 126/519] Fix bad link in CONTRIBUTING.md --- .github/CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 3bc9f82b..f1c4671e 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -46,7 +46,7 @@ See [ci/image/Dockerfile](../ci/image/Dockerfile) for the installation of the CI You can also run tests normally with `go test`. [test.sh](../ci/test.sh) just passes a default set of flags to `go test` to collect coverage and also prettify the output. -You can pass flags to [test.sh](ci/test.sh) if you want to run a specific test or otherwise +You can pass flags to [test.sh](../ci/test.sh) if you want to run a specific test or otherwise control the behaviour of `go test` but also get coverage. Coverage percentage from codecov and the CI scripts will be different because they are calculated differently. From 1b874731eab56c69c8bb3ebf8a029020c7863fc9 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 5 Sep 2019 14:26:52 -0500 Subject: [PATCH 127/519] Simplify docs The docs were way too Canadian with all the pleases ^_^ --- .github/CONTRIBUTING.md | 4 ++-- README.md | 6 +++--- accept.go | 2 +- ci/fmt.sh | 2 +- dial.go | 2 +- doc.go | 7 ++++--- websocket.go | 2 +- websocket_autobahn_python_test.go | 2 +- 8 files changed, 14 insertions(+), 13 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index f1c4671e..7338093a 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -8,7 +8,7 @@ Reproducible examples are key to finding and fixing bugs. ## Pull requests -Good issues for first time contributors are marked as such. Please feel free to +Good issues for first time contributors are marked as such. Feel free to reach out for clarification on what needs to be done. Split up large changes into several small descriptive commits. @@ -39,7 +39,7 @@ You can run CI locally. The various steps are located in [ci/\*.sh](../ci). 1. [test.sh](../ci/test.sh) 1. [run.sh](../ci/run.sh) which runs the above scripts in order. -For coverage details locally, please see `ci/out/coverage.html` after running [test.sh](../ci/test.sh). +For coverage details locally, see `ci/out/coverage.html` after running [test.sh](../ci/test.sh). See [ci/image/Dockerfile](../ci/image/Dockerfile) for the installation of the CI dependencies on Ubuntu. diff --git a/README.md b/README.md index a16cf2ed..f799b070 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ go get nhooyr.io/websocket For a production quality example that shows off the full API, see the [echo example on the godoc](https://godoc.org/nhooyr.io/websocket#example-package--Echo). On github, the example is at [example_echo_test.go](./example_echo_test.go). -Please use the [errors.As](https://golang.org/pkg/errors/#As) function [new in Go 1.13](https://golang.org/doc/go1.13#error_wrapping) to check for [websocket.CloseError](https://godoc.org/nhooyr.io/websocket#CloseError). See the [CloseError godoc example](https://godoc.org/nhooyr.io/websocket#example-CloseError). +Use the [errors.As](https://golang.org/pkg/errors/#As) function [new in Go 1.13](https://golang.org/doc/go1.13#error_wrapping) to check for [websocket.CloseError](https://godoc.org/nhooyr.io/websocket#CloseError). See the [CloseError godoc example](https://godoc.org/nhooyr.io/websocket#example-CloseError). ### Server @@ -165,11 +165,11 @@ as performant but much easier to use correctly and idiomatic. ## Contributing -Please see [.github/CONTRIBUTING.md](.github/CONTRIBUTING.md). +See [.github/CONTRIBUTING.md](.github/CONTRIBUTING.md). ## Users -If your company or project is using this library, please feel free to open an issue or PR to amend this list. +If your company or project is using this library, feel free to open an issue or PR to amend this list. - [Coder](https://github.com/cdr) - [Tatsu Works](https://github.com/tatsuworks) - Ingresses 20 TB in websocket data every month on their Discord bot. diff --git a/accept.go b/accept.go index 7ef72ad2..11611d81 100644 --- a/accept.go +++ b/accept.go @@ -34,7 +34,7 @@ type AcceptOptions struct { // // The only time you need this is if your javascript is running on a different domain // than your WebSocket server. - // Please think carefully about whether you really need this option before you use it. + // Think carefully about whether you really need this option before you use it. // If you do, remember that if you store secure data in cookies, you wil need to verify the // Origin header yourself otherwise you are exposing yourself to a CSRF attack. InsecureSkipVerify bool diff --git a/ci/fmt.sh b/ci/fmt.sh index 4eda00fc..697c37e0 100755 --- a/ci/fmt.sh +++ b/ci/fmt.sh @@ -35,7 +35,7 @@ check() { if [[ ${CI:-} && $(unstaged_files) != "" ]]; then echo echo "Files need generation or are formatted incorrectly." - echo "Please run:" + echo "Run:" echo "./ci/fmt.sh" echo git status diff --git a/dial.go b/dial.go index 2ed836bd..51d2af80 100644 --- a/dial.go +++ b/dial.go @@ -61,7 +61,7 @@ func dial(ctx context.Context, u string, opts *DialOptions) (_ *Conn, _ *http.Re opts.HTTPClient = http.DefaultClient } if opts.HTTPClient.Timeout > 0 { - return nil, nil, fmt.Errorf("please use context for cancellation instead of http.Client.Timeout; see https://github.com/nhooyr/websocket/issues/67") + return nil, nil, fmt.Errorf("use context for cancellation instead of http.Client.Timeout; see https://github.com/nhooyr/websocket/issues/67") } if opts.HTTPHeader == nil { opts.HTTPHeader = http.Header{} diff --git a/doc.go b/doc.go index d715a052..18995257 100644 --- a/doc.go +++ b/doc.go @@ -1,6 +1,6 @@ // Package websocket is a minimal and idiomatic implementation of the WebSocket protocol. // -// See https://tools.ietf.org/html/rfc6455 +// https://tools.ietf.org/html/rfc6455 // // Conn, Dial, and Accept are the main entrypoints into this package. Use Dial to dial // a WebSocket server, Accept to accept a WebSocket client dial and then Conn to interact @@ -10,8 +10,9 @@ // // The wsjson and wspb subpackages contain helpers for JSON and ProtoBuf messages. // -// Please see https://nhooyr.io/websocket for more overview docs and a +// See https://nhooyr.io/websocket for more overview docs and a // comparison with existing implementations. // -// Please be sure to use the https://golang.org/x/xerrors package when inspecting returned errors. +// Use the errors.As function new in Go 1.13 to check for websocket.CloseError. +// See the CloseError example. package websocket // import "nhooyr.io/websocket" diff --git a/websocket.go b/websocket.go index 56aca705..9976d0fa 100644 --- a/websocket.go +++ b/websocket.go @@ -24,7 +24,7 @@ import ( // You must always read from the connection. Otherwise control // frames will not be handled. See the docs on Reader and CloseRead. // -// Please be sure to call Close on the connection when you +// Be sure to call Close on the connection when you // are finished with it to release the associated resources. // // Every error from Read or Reader will cause the connection diff --git a/websocket_autobahn_python_test.go b/websocket_autobahn_python_test.go index ae7e3022..a1e5cccb 100644 --- a/websocket_autobahn_python_test.go +++ b/websocket_autobahn_python_test.go @@ -234,7 +234,7 @@ func checkWSTestIndex(t *testing.T, path string) { if failed { path = strings.Replace(path, ".json", ".html", 1) if os.Getenv("CI") == "" { - t.Errorf("wstest found failure, please see %q (output as an artifact in CI)", path) + t.Errorf("wstest found failure, see %q (output as an artifact in CI)", path) } } } From aad02d363c667e8090d72e73df7d4678d40a2436 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 6 Sep 2019 15:34:23 -0500 Subject: [PATCH 128/519] Fix shfmt in fmt.sh --- ci/fmt.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/fmt.sh b/ci/fmt.sh index 697c37e0..b1b03429 100755 --- a/ci/fmt.sh +++ b/ci/fmt.sh @@ -16,7 +16,7 @@ gen() { fmt() { gofmt -w -s . go run go.coder.com/go-tools/cmd/goimports -w "-local=$(go list -m)" . - go run mvdan.cc/sh/cmd/shfmt -i 2 -w -s -sr .. + go run mvdan.cc/sh/cmd/shfmt -i 2 -w -s -sr . # shellcheck disable=SC2046 npx prettier \ --write \ From 27104bdeb98d981a335da9faeb0efd1b8d4f8b65 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 8 Sep 2019 12:43:37 -0500 Subject: [PATCH 129/519] Fix contributing guidelines --- .github/ISSUE_TEMPLATE.md | 6 ++++-- .github/PULL_REQUEST_TEMPLATE.md | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 939d3200..fce01709 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,2 +1,4 @@ - - + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 12e6e7a0..901c994a 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,2 +1,4 @@ - - + From c5f5a80a36daeba12277197a74d5e7aeb7162991 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Sep 2019 15:58:13 -0500 Subject: [PATCH 130/519] Change release badge to light blue Closes #139 --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f799b070..06549b98 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # websocket [![GoDoc](https://godoc.org/nhooyr.io/websocket?status.svg)](https://godoc.org/nhooyr.io/websocket) -[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/nhooyr/websocket?color=critical&sort=semver)](https://github.com/nhooyr/websocket/releases) -[![Codecov](https://img.shields.io/codecov/c/github/nhooyr/websocket.svg?color=success)](https://codecov.io/gh/nhooyr/websocket) -[![CI](https://img.shields.io/circleci/build/github/nhooyr/websocket?label=ci)](https://github.com/nhooyr/websocket/commits/master) +[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/nhooyr/websocket?color=9cf&sort=semver)](https://github.com/nhooyr/websocket/releases) +[![Codecov](https://img.shields.io/codecov/c/github/nhooyr/websocket.svg?color=)](https://codecov.io/gh/nhooyr/websocket) +[![CI](https://img.shields.io/circleci/build/github/nhooyr/websocket?label=ci&color=success)](https://github.com/nhooyr/websocket/commits/master) websocket is a minimal and idiomatic WebSocket library for Go. From 406c9b3eb2163835feda7601e720d5774bd3e768 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Sep 2019 16:00:15 -0500 Subject: [PATCH 131/519] Make release badge violet and move to front Looks much cleaner. --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 06549b98..33da6162 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # websocket +[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/nhooyr/websocket?color=violet&sort=semver)](https://github.com/nhooyr/websocket/releases) [![GoDoc](https://godoc.org/nhooyr.io/websocket?status.svg)](https://godoc.org/nhooyr.io/websocket) -[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/nhooyr/websocket?color=9cf&sort=semver)](https://github.com/nhooyr/websocket/releases) -[![Codecov](https://img.shields.io/codecov/c/github/nhooyr/websocket.svg?color=)](https://codecov.io/gh/nhooyr/websocket) -[![CI](https://img.shields.io/circleci/build/github/nhooyr/websocket?label=ci&color=success)](https://github.com/nhooyr/websocket/commits/master) +[![Codecov](https://img.shields.io/codecov/c/github/nhooyr/websocket.svg?color=brightgreen)](https://codecov.io/gh/nhooyr/websocket) +[![CI](https://img.shields.io/circleci/build/github/nhooyr/websocket?label=ci&color=brightgreen)](https://github.com/nhooyr/websocket/commits/master) websocket is a minimal and idiomatic WebSocket library for Go. From aaae855dc27b4e14c4326347a4b69a0c21f79a90 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Sep 2019 17:41:28 -0500 Subject: [PATCH 132/519] Remove violet release badge in favour of light blue Violet got old fast. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 33da6162..3edfb717 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # websocket -[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/nhooyr/websocket?color=violet&sort=semver)](https://github.com/nhooyr/websocket/releases) +[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/nhooyr/websocket?color=9cf&sort=semver)](https://github.com/nhooyr/websocket/releases) [![GoDoc](https://godoc.org/nhooyr.io/websocket?status.svg)](https://godoc.org/nhooyr.io/websocket) [![Codecov](https://img.shields.io/codecov/c/github/nhooyr/websocket.svg?color=brightgreen)](https://codecov.io/gh/nhooyr/websocket) [![CI](https://img.shields.io/circleci/build/github/nhooyr/websocket?label=ci&color=brightgreen)](https://github.com/nhooyr/websocket/commits/master) From 007beaf055e7b06e6d2efbae053f49cd2c7de24d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Sep 2019 17:49:02 -0500 Subject: [PATCH 133/519] Use custom hex README.md badge colors --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 3edfb717..782251be 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # websocket -[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/nhooyr/websocket?color=9cf&sort=semver)](https://github.com/nhooyr/websocket/releases) +[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/nhooyr/websocket?color=6b9ded&sort=semver)](https://github.com/nhooyr/websocket/releases) [![GoDoc](https://godoc.org/nhooyr.io/websocket?status.svg)](https://godoc.org/nhooyr.io/websocket) -[![Codecov](https://img.shields.io/codecov/c/github/nhooyr/websocket.svg?color=brightgreen)](https://codecov.io/gh/nhooyr/websocket) +[![Codecov](https://img.shields.io/codecov/c/github/nhooyr/websocket.svg?color=65d6a4)](https://codecov.io/gh/nhooyr/websocket) [![CI](https://img.shields.io/circleci/build/github/nhooyr/websocket?label=ci&color=brightgreen)](https://github.com/nhooyr/websocket/commits/master) websocket is a minimal and idiomatic WebSocket library for Go. From cf821951eab8180ad9bebbe6b0cb85c78bc40dba Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Sep 2019 17:52:47 -0500 Subject: [PATCH 134/519] Switch to GitHub Actions for CI Closes #138 --- .circleci/config.yml | 69 ---------------------------------------- .github/workflows/ci.yml | 30 +++++++++++++++++ README.md | 2 +- ci/image/Dockerfile | 7 ++++ ci/run.sh | 2 +- ci/test.sh | 4 +-- 6 files changed, 40 insertions(+), 74 deletions(-) delete mode 100644 .circleci/config.yml create mode 100644 .github/workflows/ci.yml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 0209153a..00000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,69 +0,0 @@ -version: 2 -jobs: - fmt: - docker: - - image: nhooyr/websocket-ci@sha256:77e37211ded3c528e947439e294fbfc03b4fb9f9537c4e5198d5b304fd1df435 - steps: - - checkout - - restore_cache: - keys: - - go-v4-{{ checksum "go.sum" }} - # Fallback to using the latest cache if no exact match is found. - - go-v4- - - run: ./ci/fmt.sh - - save_cache: - paths: - - /root/gopath - - /root/.cache/go-build - key: go-v4-{{ checksum "go.sum" }} - - lint: - docker: - - image: nhooyr/websocket-ci@sha256:77e37211ded3c528e947439e294fbfc03b4fb9f9537c4e5198d5b304fd1df435 - steps: - - checkout - - restore_cache: - keys: - - go-v4-{{ checksum "go.sum" }} - # Fallback to using the latest cache if no exact match is found. - - go-v4- - - run: ./ci/lint.sh - - save_cache: - paths: - - /root/gopath - - /root/.cache/go-build - key: go-v4-{{ checksum "go.sum" }} - - test: - docker: - - image: nhooyr/websocket-ci@sha256:77e37211ded3c528e947439e294fbfc03b4fb9f9537c4e5198d5b304fd1df435 - steps: - - checkout - - restore_cache: - keys: - - go-v4-{{ checksum "go.sum" }} - # Fallback to using the latest cache if no exact match is found. - - go-v4- - - run: ./ci/test.sh - - store_artifacts: - path: ci/out - destination: out - - save_cache: - paths: - - /root/gopath - - /root/.cache/go-build - key: go-v4-{{ checksum "go.sum" }} - - store_test_results: - path: ci/out - -workflows: - version: 2 - fmt: - jobs: - - fmt - lint: - jobs: - - lint - test: - jobs: - - test diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..8e63eb17 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,30 @@ +name: ci +on: [push] + +jobs: + fmt: + runs-on: ubuntu-latest + container: docker://nhooyr/websocket-ci@sha256:549cc2716fd1ff08608b39a52af95a67bf9f490f6f31933cccd94e750985e2dc + steps: + - uses: actions/checkout@v1 + with: + fetch-depth: 1 + - run: ./ci/fmt.sh + lint: + runs-on: ubuntu-latest + container: docker://nhooyr/websocket-ci@sha256:549cc2716fd1ff08608b39a52af95a67bf9f490f6f31933cccd94e750985e2dc + steps: + - uses: actions/checkout@v1 + with: + fetch-depth: 1 + - run: ./ci/lint.sh + test: + runs-on: ubuntu-latest + container: docker://nhooyr/websocket-ci@sha256:549cc2716fd1ff08608b39a52af95a67bf9f490f6f31933cccd94e750985e2dc + steps: + - uses: actions/checkout@v1 + with: + fetch-depth: 1 + - run: ./ci/test.sh + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/README.md b/README.md index 782251be..f25dc79e 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/nhooyr/websocket?color=6b9ded&sort=semver)](https://github.com/nhooyr/websocket/releases) [![GoDoc](https://godoc.org/nhooyr.io/websocket?status.svg)](https://godoc.org/nhooyr.io/websocket) [![Codecov](https://img.shields.io/codecov/c/github/nhooyr/websocket.svg?color=65d6a4)](https://codecov.io/gh/nhooyr/websocket) -[![CI](https://img.shields.io/circleci/build/github/nhooyr/websocket?label=ci&color=brightgreen)](https://github.com/nhooyr/websocket/commits/master) +[![Actions Status](https://github.com/nhooyr/websocket/workflows/ci/badge.svg)](https://github.com/nhooyr/websocket/actions) websocket is a minimal and idiomatic WebSocket library for Go. diff --git a/ci/image/Dockerfile b/ci/image/Dockerfile index 4477d646..b59bc3af 100644 --- a/ci/image/Dockerfile +++ b/ci/image/Dockerfile @@ -4,9 +4,16 @@ ENV DEBIAN_FRONTEND=noninteractive ENV GOPATH=/root/gopath ENV GOFLAGS="-mod=readonly" ENV PAGER=cat +ENV CI=true RUN apt-get update && \ apt-get install -y shellcheck npm && \ npm install -g prettier RUN git config --global color.ui always + +# Cache go modules. +COPY . /tmp/websocket +RUN cd /tmp/websocket && \ + go mod download && \ + rm -rf /tmp/websocket diff --git a/ci/run.sh b/ci/run.sh index 56da2d93..8867b860 100755 --- a/ci/run.sh +++ b/ci/run.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash -# This script is for local testing. See .circleci for CI. +# This script is for local testing. See .github/workflows/ci.yml for CI. set -euo pipefail cd "$(dirname "${0}")" diff --git a/ci/test.sh b/ci/test.sh index fca42ff0..83564bab 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -6,8 +6,6 @@ cd "$(git rev-parse --show-toplevel)" argv=( go run gotest.tools/gotestsum - # https://circleci.com/docs/2.0/collect-test-data/ - "--junitfile=ci/out/websocket/testReport.xml" "--format=short-verbose" -- "-vet=off" @@ -33,5 +31,5 @@ mv ci/out/coverage2.prof ci/out/coverage.prof go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html if [[ ${CI:-} ]]; then - bash <(curl -s https://codecov.io/bash) -R . -f ci/out/coverage.prof + bash <(curl -s https://codecov.io/bash) -Z -R . -f ci/out/coverage.prof fi From 6448e2712d2b252287961046aa8965f6d8f4c44d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Sep 2019 20:31:13 -0500 Subject: [PATCH 135/519] Use -race in CI --- ci/fmt.sh | 2 +- ci/test.sh | 24 +++++++++++------------- go.mod | 5 +++-- go.sum | 10 ++++++---- 4 files changed, 21 insertions(+), 20 deletions(-) diff --git a/ci/fmt.sh b/ci/fmt.sh index b1b03429..dee94e87 100755 --- a/ci/fmt.sh +++ b/ci/fmt.sh @@ -32,7 +32,7 @@ unstaged_files() { } check() { - if [[ ${CI:-} && $(unstaged_files) != "" ]]; then + if [[ ${CI-} && $(unstaged_files) != "" ]]; then echo echo "Files need generation or are formatted incorrectly." echo "Run:" diff --git a/ci/test.sh b/ci/test.sh index 83564bab..81d6f462 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -4,22 +4,20 @@ set -euo pipefail cd "$(dirname "${0}")" cd "$(git rev-parse --show-toplevel)" -argv=( - go run gotest.tools/gotestsum - "--format=short-verbose" - -- - "-vet=off" +argv+=(go test + "-parallel=1024" + "-coverprofile=ci/out/coverage.prof" + "-coverpkg=./..." ) -# Interactive usage does not want to turn off vet or use gotestsum. -if [[ $# -gt 0 ]]; then - argv=(go test "$@") + +if [[ ${CI-} ]]; then + argv+=( + "-race" + ) fi -# We always want coverage and race detection. argv+=( - "-parallel=512" - "-coverprofile=ci/out/coverage.prof" - "-coverpkg=./..." + "$@" ) mkdir -p ci/out/websocket @@ -30,6 +28,6 @@ grep -v _string.go < ci/out/coverage.prof > ci/out/coverage2.prof mv ci/out/coverage2.prof ci/out/coverage.prof go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html -if [[ ${CI:-} ]]; then +if [[ ${CI-} ]]; then bash <(curl -s https://codecov.io/bash) -Z -R . -f ci/out/coverage.prof fi diff --git a/go.mod b/go.mod index d9c48955..c247f54a 100644 --- a/go.mod +++ b/go.mod @@ -12,13 +12,14 @@ require ( github.com/mattn/go-isatty v0.0.9 // indirect github.com/pkg/errors v0.8.1 // indirect github.com/sirupsen/logrus v1.4.2 // indirect - github.com/spf13/pflag v1.0.3 // indirect + github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/testify v1.4.0 // indirect go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16 go.uber.org/atomic v1.4.0 // indirect go.uber.org/multierr v1.1.0 golang.org/x/lint v0.0.0-20190409202823-959b441ac422 - golang.org/x/sys v0.0.0-20190904005037-43c01164e931 // indirect + golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect + golang.org/x/sys v0.0.0-20190919044723-0c1ff786ef13 // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 golang.org/x/tools v0.0.0-20190903163617-be0da057c5e3 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect diff --git a/go.sum b/go.sum index 03310997..a6a86413 100644 --- a/go.sum +++ b/go.sum @@ -53,8 +53,8 @@ github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4 github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= -github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -80,14 +80,16 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190904005037-43c01164e931 h1:+WYfosiOJzB4BjsISl1Rv4ZLUy+VYcF+u+0Y9jcerv8= -golang.org/x/sys v0.0.0-20190904005037-43c01164e931/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190919044723-0c1ff786ef13 h1:/zi0zzlPHWXYXrO1LjNRByFu8sdGgCkj2JLDdBIB84k= +golang.org/x/sys v0.0.0-20190919044723-0c1ff786ef13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= From c9409047854a468a59e2dd1acb5067ddde919ba8 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 20 Sep 2019 22:17:57 -0500 Subject: [PATCH 136/519] Improve MessageType godoc --- messagetype.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/messagetype.go b/messagetype.go index 6a1205ee..d6436b0b 100644 --- a/messagetype.go +++ b/messagetype.go @@ -9,9 +9,9 @@ type MessageType int // MessageType constants. const ( // MessageText is for UTF-8 encoded text messages like JSON. - MessageText MessageType = MessageType(opText) + MessageText MessageType = iota + 1 // MessageBinary is for binary messages like Protobufs. - MessageBinary MessageType = MessageType(opBinary) + MessageBinary ) // Above I've explicitly included the types of the constants for stringer. From f6a26896c967a2f6e782f1e4a216204cebaa1507 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 20 Sep 2019 22:18:29 -0500 Subject: [PATCH 137/519] Implement low level typed interface to the browser WebSocket API Updates #121 --- internal/websocketjs/websocketjs.go | 149 +++++++++++++++++++++++ internal/websocketjs/websocketjs_test.go | 26 ++++ 2 files changed, 175 insertions(+) create mode 100644 internal/websocketjs/websocketjs.go create mode 100644 internal/websocketjs/websocketjs_test.go diff --git a/internal/websocketjs/websocketjs.go b/internal/websocketjs/websocketjs.go new file mode 100644 index 00000000..02659563 --- /dev/null +++ b/internal/websocketjs/websocketjs.go @@ -0,0 +1,149 @@ +// +build js + +// websocketjs implements typed access to the browser javascript WebSocket API. +// https://developer.mozilla.org/en-US/docs/Web/API/WebSocket +package websocketjs + +import ( + "context" + "syscall/js" +) + +func handleJSError(err *error, onErr func()) { + r := recover() + + if jsErr, ok := r.(js.Error); ok { + *err = jsErr + + if onErr != nil { + onErr() + } + return + } + + if r != nil { + panic(r) + } +} + +func New(ctx context.Context, url string, protocols []string) (c *WebSocket, err error) { + defer handleJSError(&err, func() { + c = nil + }) + + jsProtocols := make([]interface{}, len(protocols)) + for i, p := range protocols { + jsProtocols[i] = p + } + + c = &WebSocket{ + v: js.Global().Get("WebSocket").New(url, jsProtocols), + } + + c.setBinaryType("arrayBuffer") + + c.Extensions = c.v.Get("extensions").String() + c.Protocol = c.v.Get("protocol").String() + c.URL = c.v.Get("url").String() + + return c, nil +} + +type WebSocket struct { + Extensions string + Protocol string + URL string + + v js.Value +} + +func (c *WebSocket) setBinaryType(typ string) { + c.v.Set("binaryType", string(typ)) +} + +func (c *WebSocket) BufferedAmount() uint32 { + return uint32(c.v.Get("bufferedAmount").Int()) +} + +func (c *WebSocket) addEventListener(eventType string, fn func(e js.Value)) { + c.v.Call("addEventListener", eventType, js.FuncOf(func(this js.Value, args []js.Value) interface{} { + fn(args[0]) + return nil + })) +} + +type CloseEvent struct { + Code uint16 + Reason string + WasClean bool +} + +func (c *WebSocket) OnClose(fn func(CloseEvent)) { + c.addEventListener("close", func(e js.Value) { + ce := CloseEvent{ + Code: uint16(e.Get("code").Int()), + Reason: e.Get("reason").String(), + WasClean: e.Get("wasClean").Bool(), + } + fn(ce) + }) +} + +func (c *WebSocket) OnError(fn func(e js.Value)) { + c.addEventListener("error", fn) +} + +type MessageEvent struct { + Data []byte + // There are more types to the interface but we don't use them. + // See https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent +} + +func (c *WebSocket) OnMessage(fn func(m MessageEvent)) { + c.addEventListener("message", func(e js.Value) { + arrayBuffer := e.Get("data") + data := extractArrayBuffer(arrayBuffer) + + me := MessageEvent{ + Data: data, + } + fn(me) + + return + }) +} + +func (c *WebSocket) OnOpen(fn func(e js.Value)) { + c.addEventListener("open", fn) +} + +func (c *WebSocket) Close(code int, reason string) (err error) { + defer handleJSError(&err, nil) + c.v.Call("close", code, reason) + return err +} + +func (c *WebSocket) SendText(v string) (err error) { + defer handleJSError(&err, nil) + c.v.Call("send", v) + return err +} + +func (c *WebSocket) SendBytes(v []byte) (err error) { + defer handleJSError(&err, nil) + c.v.Call("send", uint8Array(v)) + return err +} + +func extractArrayBuffer(arrayBuffer js.Value) []byte { + uint8Array := js.Global().Get("Uint8Array").New(arrayBuffer) + dst := make([]byte, uint8Array.Length()) + js.CopyBytesToGo(dst, uint8Array) + return dst +} + +func uint8Array(src []byte) js.Value { + uint8Array := js.Global().Get("Uint8Array").New(len(src)) + js.CopyBytesToJS(uint8Array, src) + return uint8Array +} diff --git a/internal/websocketjs/websocketjs_test.go b/internal/websocketjs/websocketjs_test.go new file mode 100644 index 00000000..46819bd3 --- /dev/null +++ b/internal/websocketjs/websocketjs_test.go @@ -0,0 +1,26 @@ +// +build js + +package websocketjs + +import ( + "context" + "syscall/js" + "testing" + "time" +) + +func TestWebSocket(t *testing.T) { + t.Parallel() + + c, err := New(context.Background(), "ws://localhost:8081", nil) + if err != nil { + t.Fatal(err) + } + + c.OnError(func(e js.Value) { + t.Log(js.Global().Get("JSON").Call("stringify", e)) + t.Log(c.v.Get("readyState")) + }) + + time.Sleep(time.Second) +} From 128b1a2f34032f92bff186f80be2abcf9083ed8f Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 21 Sep 2019 12:41:55 -0500 Subject: [PATCH 138/519] Add wasm CI --- .github/workflows/ci.yml | 14 +++++++++++--- ci/image/Dockerfile | 11 ++++++++--- ci/run.sh | 1 + ci/wasm.sh | 11 +++++++++++ go.mod | 4 ++-- go.sum | 8 ++++---- .../{websocketjs/websocketjs.go => wsjs/wsjs.go} | 12 +++++++++--- .../websocketjs_test.go => wsjs/wsjs_test.go} | 2 +- 8 files changed, 47 insertions(+), 16 deletions(-) create mode 100755 ci/wasm.sh rename internal/{websocketjs/websocketjs.go => wsjs/wsjs.go} (92%) rename internal/{websocketjs/websocketjs_test.go => wsjs/wsjs_test.go} (94%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8e63eb17..4d889ab5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,7 @@ on: [push] jobs: fmt: runs-on: ubuntu-latest - container: docker://nhooyr/websocket-ci@sha256:549cc2716fd1ff08608b39a52af95a67bf9f490f6f31933cccd94e750985e2dc + container: docker://nhooyr/websocket-ci@sha256:6f6a00284eff008ad2cece8f3d0b4a2a3a8f2fcf7a54c691c64a92403abc4c75 steps: - uses: actions/checkout@v1 with: @@ -12,7 +12,7 @@ jobs: - run: ./ci/fmt.sh lint: runs-on: ubuntu-latest - container: docker://nhooyr/websocket-ci@sha256:549cc2716fd1ff08608b39a52af95a67bf9f490f6f31933cccd94e750985e2dc + container: docker://nhooyr/websocket-ci@sha256:6f6a00284eff008ad2cece8f3d0b4a2a3a8f2fcf7a54c691c64a92403abc4c75 steps: - uses: actions/checkout@v1 with: @@ -20,7 +20,7 @@ jobs: - run: ./ci/lint.sh test: runs-on: ubuntu-latest - container: docker://nhooyr/websocket-ci@sha256:549cc2716fd1ff08608b39a52af95a67bf9f490f6f31933cccd94e750985e2dc + container: docker://nhooyr/websocket-ci@sha256:6f6a00284eff008ad2cece8f3d0b4a2a3a8f2fcf7a54c691c64a92403abc4c75 steps: - uses: actions/checkout@v1 with: @@ -28,3 +28,11 @@ jobs: - run: ./ci/test.sh env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + wasm: + runs-on: ubuntu-latest + container: docker://nhooyr/websocket-ci@sha256:6f6a00284eff008ad2cece8f3d0b4a2a3a8f2fcf7a54c691c64a92403abc4c75 + steps: + - uses: actions/checkout@v1 + with: + fetch-depth: 1 + - run: ./ci/wasm.sh diff --git a/ci/image/Dockerfile b/ci/image/Dockerfile index b59bc3af..8d792159 100644 --- a/ci/image/Dockerfile +++ b/ci/image/Dockerfile @@ -2,18 +2,23 @@ FROM golang:1 ENV DEBIAN_FRONTEND=noninteractive ENV GOPATH=/root/gopath +ENV PATH=$GOPATH/bin:$PATH ENV GOFLAGS="-mod=readonly" ENV PAGER=cat ENV CI=true RUN apt-get update && \ - apt-get install -y shellcheck npm && \ + apt-get install -y shellcheck npm chromium && \ npm install -g prettier +# https://github.com/golang/go/wiki/WebAssembly#running-tests-in-the-browser +RUN go get github.com/agnivade/wasmbrowsertest && \ + mv $GOPATH/bin/wasmbrowsertest $GOPATH/bin/go_js_wasm_exec + RUN git config --global color.ui always -# Cache go modules. +# Cache go modules and build cache. COPY . /tmp/websocket RUN cd /tmp/websocket && \ - go mod download && \ + CI= ./ci/run.sh && \ rm -rf /tmp/websocket diff --git a/ci/run.sh b/ci/run.sh index 8867b860..9e47d291 100755 --- a/ci/run.sh +++ b/ci/run.sh @@ -9,3 +9,4 @@ cd "$(git rev-parse --show-toplevel)" ./ci/fmt.sh ./ci/lint.sh ./ci/test.sh +./ci/wasm.sh diff --git a/ci/wasm.sh b/ci/wasm.sh new file mode 100755 index 00000000..943d3806 --- /dev/null +++ b/ci/wasm.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -euo pipefail +cd "$(dirname "${0}")" +cd "$(git rev-parse --show-toplevel)" + +GOOS=js GOARCH=wasm go vet ./... +go install golang.org/x/lint/golint +# Get passing later. +#GOOS=js GOARCH=wasm golint -set_exit_status ./... +GOOS=js GOARCH=wasm go test ./internal/wsjs diff --git a/go.mod b/go.mod index c247f54a..34a7f872 100644 --- a/go.mod +++ b/go.mod @@ -17,11 +17,11 @@ require ( go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16 go.uber.org/atomic v1.4.0 // indirect go.uber.org/multierr v1.1.0 - golang.org/x/lint v0.0.0-20190409202823-959b441ac422 + golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect golang.org/x/sys v0.0.0-20190919044723-0c1ff786ef13 // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 - golang.org/x/tools v0.0.0-20190903163617-be0da057c5e3 + golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gotest.tools/gotestsum v0.3.5 mvdan.cc/sh v2.6.4+incompatible diff --git a/go.sum b/go.sum index a6a86413..97d6a835 100644 --- a/go.sum +++ b/go.sum @@ -70,8 +70,8 @@ go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/ golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422 h1:QzoH/1pFpZguR8NrRHLcO6jKqfv2zpuSqZLgdm7ZmjI= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac h1:8R1esu+8QioDxo4E4mX6bFztO+dMTM49DNAaWfO5OeY= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -95,8 +95,8 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190903163617-be0da057c5e3 h1:1cLrGl9PL64Mzl9NATDCqFE57dVYwWOkoPXvppEnjO4= -golang.org/x/tools v0.0.0-20190903163617-be0da057c5e3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72 h1:bw9doJza/SFBEweII/rHQh338oozWyiFsBRHtrflcws= +golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= diff --git a/internal/websocketjs/websocketjs.go b/internal/wsjs/wsjs.go similarity index 92% rename from internal/websocketjs/websocketjs.go rename to internal/wsjs/wsjs.go index 02659563..4adb71ad 100644 --- a/internal/websocketjs/websocketjs.go +++ b/internal/wsjs/wsjs.go @@ -1,8 +1,8 @@ // +build js -// websocketjs implements typed access to the browser javascript WebSocket API. +// Package wsjs implements typed access to the browser javascript WebSocket API. // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket -package websocketjs +package wsjs import ( "context" @@ -101,8 +101,14 @@ type MessageEvent struct { func (c *WebSocket) OnMessage(fn func(m MessageEvent)) { c.addEventListener("message", func(e js.Value) { + var data []byte + arrayBuffer := e.Get("data") - data := extractArrayBuffer(arrayBuffer) + if arrayBuffer.Type() == js.TypeString { + data = []byte(arrayBuffer.String()) + } else { + data = extractArrayBuffer(arrayBuffer) + } me := MessageEvent{ Data: data, diff --git a/internal/websocketjs/websocketjs_test.go b/internal/wsjs/wsjs_test.go similarity index 94% rename from internal/websocketjs/websocketjs_test.go rename to internal/wsjs/wsjs_test.go index 46819bd3..4f5f1878 100644 --- a/internal/websocketjs/websocketjs_test.go +++ b/internal/wsjs/wsjs_test.go @@ -1,6 +1,6 @@ // +build js -package websocketjs +package wsjs import ( "context" From 599534b9f2b28d520d1476faa5febc4c8b9031ed Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 21 Sep 2019 13:20:45 -0500 Subject: [PATCH 139/519] Implement core API for WASM Closes #121 --- accept.go | 8 ++ accept_test.go | 19 +++ ci/wasm.sh | 5 +- dial.go | 6 + dial_test.go | 12 ++ doc.go | 2 + example_echo_test.go | 2 + example_test.go | 2 + export_test.go | 2 + go.mod | 1 + go.sum | 1 + header.go | 2 + header_test.go | 2 + internal/echoserver/echoserver.go | 11 ++ internal/wsjs/wsjs.go | 72 ++++++---- internal/wsjs/wsjs_test.go | 26 ---- netconn.go | 2 +- netconn_js.go | 17 +++ netconn_normal.go | 12 ++ opcode.go | 2 +- opcode_string.go | 2 +- websocket.go | 6 +- websocket_autobahn_python_test.go | 3 +- websocket_bench_test.go | 2 + websocket_js.go | 211 ++++++++++++++++++++++++++++++ websocket_js_test.go | 20 +++ websocket_test.go | 2 + wsjson/wsjson.go | 2 + wsjson/wsjson_js.go | 58 ++++++++ wspb/wspb.go | 5 +- wspb/wspb_js.go | 67 ++++++++++ xor.go | 2 + xor_test.go | 2 + 33 files changed, 523 insertions(+), 65 deletions(-) create mode 100644 internal/echoserver/echoserver.go delete mode 100644 internal/wsjs/wsjs_test.go create mode 100644 netconn_js.go create mode 100644 netconn_normal.go create mode 100644 websocket_js.go create mode 100644 websocket_js_test.go create mode 100644 wsjson/wsjson_js.go create mode 100644 wspb/wspb_js.go diff --git a/accept.go b/accept.go index 11611d81..e68a049b 100644 --- a/accept.go +++ b/accept.go @@ -1,3 +1,5 @@ +// +build !js + package websocket import ( @@ -41,6 +43,12 @@ type AcceptOptions struct { } func verifyClientRequest(w http.ResponseWriter, r *http.Request) error { + if !r.ProtoAtLeast(1, 1) { + err := fmt.Errorf("websocket protocol violation: handshake request must be at least HTTP/1.1: %q", r.Proto) + http.Error(w, err.Error(), http.StatusBadRequest) + return err + } + if !headerValuesContainsToken(r.Header, "Connection", "Upgrade") { err := fmt.Errorf("websocket protocol violation: Connection header %q does not contain Upgrade", r.Header.Get("Connection")) http.Error(w, err.Error(), http.StatusBadRequest) diff --git a/accept_test.go b/accept_test.go index 6602a8d0..44a956a8 100644 --- a/accept_test.go +++ b/accept_test.go @@ -1,3 +1,5 @@ +// +build !js + package websocket import ( @@ -45,6 +47,7 @@ func Test_verifyClientHandshake(t *testing.T) { testCases := []struct { name string method string + http1 bool h map[string]string success bool }{ @@ -86,6 +89,16 @@ func Test_verifyClientHandshake(t *testing.T) { "Sec-WebSocket-Key": "", }, }, + { + name: "badHTTPVersion", + h: map[string]string{ + "Connection": "Upgrade", + "Upgrade": "websocket", + "Sec-WebSocket-Version": "13", + "Sec-WebSocket-Key": "meow123", + }, + http1: true, + }, { name: "success", h: map[string]string{ @@ -106,6 +119,12 @@ func Test_verifyClientHandshake(t *testing.T) { w := httptest.NewRecorder() r := httptest.NewRequest(tc.method, "/", nil) + r.ProtoMajor = 1 + r.ProtoMinor = 1 + if tc.http1 { + r.ProtoMinor = 0 + } + for k, v := range tc.h { r.Header.Set(k, v) } diff --git a/ci/wasm.sh b/ci/wasm.sh index 943d3806..9894fca6 100755 --- a/ci/wasm.sh +++ b/ci/wasm.sh @@ -6,6 +6,5 @@ cd "$(git rev-parse --show-toplevel)" GOOS=js GOARCH=wasm go vet ./... go install golang.org/x/lint/golint -# Get passing later. -#GOOS=js GOARCH=wasm golint -set_exit_status ./... -GOOS=js GOARCH=wasm go test ./internal/wsjs +GOOS=js GOARCH=wasm golint -set_exit_status ./... +GOOS=js GOARCH=wasm go test ./... diff --git a/dial.go b/dial.go index 51d2af80..79232aac 100644 --- a/dial.go +++ b/dial.go @@ -1,3 +1,5 @@ +// +build !js + package websocket import ( @@ -149,6 +151,10 @@ func verifyServerResponse(r *http.Request, resp *http.Response) error { ) } + if proto := resp.Header.Get("Sec-WebSocket-Protocol"); proto != "" && !headerValuesContainsToken(r.Header, "Sec-WebSocket-Protocol", proto) { + return fmt.Errorf("websocket protocol violation: unexpected Sec-WebSocket-Protocol from server: %q", proto) + } + return nil } diff --git a/dial_test.go b/dial_test.go index 96537bdb..083b9bf3 100644 --- a/dial_test.go +++ b/dial_test.go @@ -1,3 +1,5 @@ +// +build !js + package websocket import ( @@ -97,6 +99,16 @@ func Test_verifyServerHandshake(t *testing.T) { }, success: false, }, + { + name: "badSecWebSocketProtocol", + response: func(w http.ResponseWriter) { + w.Header().Set("Connection", "Upgrade") + w.Header().Set("Upgrade", "websocket") + w.Header().Set("Sec-WebSocket-Protocol", "xd") + w.WriteHeader(http.StatusSwitchingProtocols) + }, + success: false, + }, { name: "success", response: func(w http.ResponseWriter) { diff --git a/doc.go b/doc.go index 18995257..cb33c5c9 100644 --- a/doc.go +++ b/doc.go @@ -1,3 +1,5 @@ +// +build !js + // Package websocket is a minimal and idiomatic implementation of the WebSocket protocol. // // https://tools.ietf.org/html/rfc6455 diff --git a/example_echo_test.go b/example_echo_test.go index aad32675..b1afe8b3 100644 --- a/example_echo_test.go +++ b/example_echo_test.go @@ -1,3 +1,5 @@ +// +build !js + package websocket_test import ( diff --git a/example_test.go b/example_test.go index 36cab2bd..2cedddf3 100644 --- a/example_test.go +++ b/example_test.go @@ -1,3 +1,5 @@ +// +build !js + package websocket_test import ( diff --git a/export_test.go b/export_test.go index 5a0d1c32..32340b56 100644 --- a/export_test.go +++ b/export_test.go @@ -1,3 +1,5 @@ +// +build !js + package websocket import ( diff --git a/go.mod b/go.mod index 34a7f872..6b3f28ad 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( golang.org/x/sys v0.0.0-20190919044723-0c1ff786ef13 // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72 + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gotest.tools/gotestsum v0.3.5 mvdan.cc/sh v2.6.4+incompatible diff --git a/go.sum b/go.sum index 97d6a835..de366e52 100644 --- a/go.sum +++ b/go.sum @@ -97,6 +97,7 @@ golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxb golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72 h1:bw9doJza/SFBEweII/rHQh338oozWyiFsBRHtrflcws= golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= diff --git a/header.go b/header.go index 6eb8610f..613b1d15 100644 --- a/header.go +++ b/header.go @@ -1,3 +1,5 @@ +// +build !js + package websocket import ( diff --git a/header_test.go b/header_test.go index 45d0535a..5d0fd6a2 100644 --- a/header_test.go +++ b/header_test.go @@ -1,3 +1,5 @@ +// +build !js + package websocket import ( diff --git a/internal/echoserver/echoserver.go b/internal/echoserver/echoserver.go new file mode 100644 index 00000000..905ede2b --- /dev/null +++ b/internal/echoserver/echoserver.go @@ -0,0 +1,11 @@ +package echoserver + +import ( + "net/http" +) + +// EchoServer provides a streaming WebSocket echo server +// for use in tests. +func EchoServer(w http.ResponseWriter, r *http.Request) { + +} diff --git a/internal/wsjs/wsjs.go b/internal/wsjs/wsjs.go index 4adb71ad..f83b766c 100644 --- a/internal/wsjs/wsjs.go +++ b/internal/wsjs/wsjs.go @@ -1,11 +1,11 @@ // +build js // Package wsjs implements typed access to the browser javascript WebSocket API. +// // https://developer.mozilla.org/en-US/docs/Web/API/WebSocket package wsjs import ( - "context" "syscall/js" ) @@ -26,9 +26,10 @@ func handleJSError(err *error, onErr func()) { } } -func New(ctx context.Context, url string, protocols []string) (c *WebSocket, err error) { +// New is a wrapper around the javascript WebSocket constructor. +func New(url string, protocols []string) (c WebSocket, err error) { defer handleJSError(&err, func() { - c = nil + c = WebSocket{} }) jsProtocols := make([]interface{}, len(protocols)) @@ -36,7 +37,7 @@ func New(ctx context.Context, url string, protocols []string) (c *WebSocket, err jsProtocols[i] = p } - c = &WebSocket{ + c = WebSocket{ v: js.Global().Get("WebSocket").New(url, jsProtocols), } @@ -49,6 +50,7 @@ func New(ctx context.Context, url string, protocols []string) (c *WebSocket, err return c, nil } +// WebSocket is a wrapper around a javascript WebSocket object. type WebSocket struct { Extensions string Protocol string @@ -57,29 +59,33 @@ type WebSocket struct { v js.Value } -func (c *WebSocket) setBinaryType(typ string) { +func (c WebSocket) setBinaryType(typ string) { c.v.Set("binaryType", string(typ)) } -func (c *WebSocket) BufferedAmount() uint32 { - return uint32(c.v.Get("bufferedAmount").Int()) -} - -func (c *WebSocket) addEventListener(eventType string, fn func(e js.Value)) { - c.v.Call("addEventListener", eventType, js.FuncOf(func(this js.Value, args []js.Value) interface{} { +func (c WebSocket) addEventListener(eventType string, fn func(e js.Value)) func() { + f := js.FuncOf(func(this js.Value, args []js.Value) interface{} { fn(args[0]) return nil - })) + }) + c.v.Call("addEventListener", eventType, f) + + return func() { + c.v.Call("removeEventListener", eventType, f) + f.Release() + } } +// CloseEvent is the type passed to a WebSocket close handler. type CloseEvent struct { Code uint16 Reason string WasClean bool } -func (c *WebSocket) OnClose(fn func(CloseEvent)) { - c.addEventListener("close", func(e js.Value) { +// OnClose registers a function to be called when the WebSocket is closed. +func (c WebSocket) OnClose(fn func(CloseEvent)) (remove func()) { + return c.addEventListener("close", func(e js.Value) { ce := CloseEvent{ Code: uint16(e.Get("code").Int()), Reason: e.Get("reason").String(), @@ -89,23 +95,29 @@ func (c *WebSocket) OnClose(fn func(CloseEvent)) { }) } -func (c *WebSocket) OnError(fn func(e js.Value)) { - c.addEventListener("error", fn) +// OnError registers a function to be called when there is an error +// with the WebSocket. +func (c WebSocket) OnError(fn func(e js.Value)) (remove func()) { + return c.addEventListener("error", fn) } +// MessageEvent is the type passed to a message handler. type MessageEvent struct { - Data []byte - // There are more types to the interface but we don't use them. + // string or []byte. + Data interface{} + + // There are more fields to the interface but we don't use them. // See https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent } -func (c *WebSocket) OnMessage(fn func(m MessageEvent)) { - c.addEventListener("message", func(e js.Value) { - var data []byte +// OnMessage registers a function to be called when the websocket receives a message. +func (c WebSocket) OnMessage(fn func(m MessageEvent)) (remove func()) { + return c.addEventListener("message", func(e js.Value) { + var data interface{} arrayBuffer := e.Get("data") if arrayBuffer.Type() == js.TypeString { - data = []byte(arrayBuffer.String()) + data = arrayBuffer.String() } else { data = extractArrayBuffer(arrayBuffer) } @@ -119,23 +131,29 @@ func (c *WebSocket) OnMessage(fn func(m MessageEvent)) { }) } -func (c *WebSocket) OnOpen(fn func(e js.Value)) { - c.addEventListener("open", fn) +// OnOpen registers a function to be called when the websocket is opened. +func (c WebSocket) OnOpen(fn func(e js.Value)) (remove func()) { + return c.addEventListener("open", fn) } -func (c *WebSocket) Close(code int, reason string) (err error) { +// Close closes the WebSocket with the given code and reason. +func (c WebSocket) Close(code int, reason string) (err error) { defer handleJSError(&err, nil) c.v.Call("close", code, reason) return err } -func (c *WebSocket) SendText(v string) (err error) { +// SendText sends the given string as a text message +// on the WebSocket. +func (c WebSocket) SendText(v string) (err error) { defer handleJSError(&err, nil) c.v.Call("send", v) return err } -func (c *WebSocket) SendBytes(v []byte) (err error) { +// SendBytes sends the given message as a binary message +// on the WebSocket. +func (c WebSocket) SendBytes(v []byte) (err error) { defer handleJSError(&err, nil) c.v.Call("send", uint8Array(v)) return err diff --git a/internal/wsjs/wsjs_test.go b/internal/wsjs/wsjs_test.go deleted file mode 100644 index 4f5f1878..00000000 --- a/internal/wsjs/wsjs_test.go +++ /dev/null @@ -1,26 +0,0 @@ -// +build js - -package wsjs - -import ( - "context" - "syscall/js" - "testing" - "time" -) - -func TestWebSocket(t *testing.T) { - t.Parallel() - - c, err := New(context.Background(), "ws://localhost:8081", nil) - if err != nil { - t.Fatal(err) - } - - c.OnError(func(e js.Value) { - t.Log(js.Global().Get("JSON").Call("stringify", e)) - t.Log(c.v.Get("readyState")) - }) - - time.Sleep(time.Second) -} diff --git a/netconn.go b/netconn.go index 20b99c2a..8efdade2 100644 --- a/netconn.go +++ b/netconn.go @@ -93,7 +93,7 @@ func (c *netConn) Read(p []byte) (int, error) { } if c.reader == nil { - typ, r, err := c.c.Reader(c.readContext) + typ, r, err := c.netConnReader(c.readContext) if err != nil { var ce CloseError if errors.As(err, &ce) && (ce.Code == StatusNormalClosure) || (ce.Code == StatusGoingAway) { diff --git a/netconn_js.go b/netconn_js.go new file mode 100644 index 00000000..5cd15d47 --- /dev/null +++ b/netconn_js.go @@ -0,0 +1,17 @@ +// +build js + +package websocket + +import ( + "bytes" + "context" + "io" +) + +func (c *netConn) netConnReader(ctx context.Context) (MessageType, io.Reader, error) { + typ, p, err := c.c.Read(ctx) + if err != nil { + return 0, nil, err + } + return typ, bytes.NewReader(p), nil +} diff --git a/netconn_normal.go b/netconn_normal.go new file mode 100644 index 00000000..0db551d4 --- /dev/null +++ b/netconn_normal.go @@ -0,0 +1,12 @@ +// +build !js + +package websocket + +import ( + "context" + "io" +) + +func (c *netConn) netConnReader(ctx context.Context) (MessageType, io.Reader, error) { + return c.c.Reader(c.readContext) +} diff --git a/opcode.go b/opcode.go index 86f94bd9..df708aa0 100644 --- a/opcode.go +++ b/opcode.go @@ -3,7 +3,7 @@ package websocket // opcode represents a WebSocket Opcode. type opcode int -//go:generate go run golang.org/x/tools/cmd/stringer -type=opcode +//go:generate go run golang.org/x/tools/cmd/stringer -type=opcode -tags js // opcode constants. const ( diff --git a/opcode_string.go b/opcode_string.go index 740b5e70..d7b88961 100644 --- a/opcode_string.go +++ b/opcode_string.go @@ -1,4 +1,4 @@ -// Code generated by "stringer -type=opcode"; DO NOT EDIT. +// Code generated by "stringer -type=opcode -tags js"; DO NOT EDIT. package websocket diff --git a/websocket.go b/websocket.go index 9976d0fa..596d89f3 100644 --- a/websocket.go +++ b/websocket.go @@ -1,3 +1,5 @@ +// +build !js + package websocket import ( @@ -438,8 +440,8 @@ func (r *messageReader) eof() bool { func (r *messageReader) Read(p []byte) (int, error) { n, err := r.read(p) if err != nil { - // Have to return io.EOF directly for now, we cannot wrap as xerrors - // isn't used in stdlib. + // Have to return io.EOF directly for now, we cannot wrap as errors.Is + // isn't used widely yet. if errors.Is(err, io.EOF) { return n, io.EOF } diff --git a/websocket_autobahn_python_test.go b/websocket_autobahn_python_test.go index a1e5cccb..4e8b588e 100644 --- a/websocket_autobahn_python_test.go +++ b/websocket_autobahn_python_test.go @@ -1,6 +1,7 @@ // This file contains the old autobahn test suite tests that use the -// python binary. The approach is very clunky and slow so new tests +// python binary. The approach is clunky and slow so new tests // have been written in pure Go in websocket_test.go. +// These have been kept for correctness purposes and are occasionally ran. // +build autobahn-python package websocket_test diff --git a/websocket_bench_test.go b/websocket_bench_test.go index 6a54fab2..9598e873 100644 --- a/websocket_bench_test.go +++ b/websocket_bench_test.go @@ -1,3 +1,5 @@ +// +build !js + package websocket_test import ( diff --git a/websocket_js.go b/websocket_js.go new file mode 100644 index 00000000..aab10494 --- /dev/null +++ b/websocket_js.go @@ -0,0 +1,211 @@ +package websocket // import "nhooyr.io/websocket" + +import ( + "context" + "errors" + "fmt" + "net/http" + "reflect" + "runtime" + "sync" + "syscall/js" + + "golang.org/x/xerrors" + + "nhooyr.io/websocket/internal/wsjs" +) + +// Conn provides a wrapper around the browser WebSocket API. +type Conn struct { + ws wsjs.WebSocket + + closeOnce sync.Once + closed chan struct{} + closeErr error + + releaseOnClose func() + releaseOnMessage func() + + readch chan wsjs.MessageEvent +} + +func (c *Conn) close(err error) { + c.closeOnce.Do(func() { + runtime.SetFinalizer(c, nil) + + c.closeErr = fmt.Errorf("websocket closed: %w", err) + close(c.closed) + + c.releaseOnClose() + c.releaseOnMessage() + }) +} + +func (c *Conn) init() { + c.closed = make(chan struct{}) + c.readch = make(chan wsjs.MessageEvent, 1) + + c.releaseOnClose = c.ws.OnClose(func(e wsjs.CloseEvent) { + cerr := CloseError{ + Code: StatusCode(e.Code), + Reason: e.Reason, + } + + c.close(fmt.Errorf("received close frame: %w", cerr)) + }) + + c.releaseOnMessage = c.ws.OnMessage(func(e wsjs.MessageEvent) { + c.readch <- e + }) + + runtime.SetFinalizer(c, func(c *Conn) { + c.ws.Close(int(StatusInternalError), "internal error") + c.close(errors.New("connection garbage collected")) + }) +} + +// Read attempts to read a message from the connection. +// The maximum time spent waiting is bounded by the context. +func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { + typ, p, err := c.read(ctx) + if err != nil { + return 0, nil, fmt.Errorf("failed to read: %w", err) + } + return typ, p, nil +} + +func (c *Conn) read(ctx context.Context) (MessageType, []byte, error) { + var me wsjs.MessageEvent + select { + case <-ctx.Done(): + return 0, nil, ctx.Err() + case me = <-c.readch: + case <-c.closed: + return 0, nil, c.closeErr + } + + switch p := me.Data.(type) { + case string: + return MessageText, []byte(p), nil + case []byte: + return MessageBinary, p, nil + default: + panic("websocket: unexpected data type from wsjs OnMessage: " + reflect.TypeOf(me.Data).String()) + } +} + +// Write writes a message of the given type to the connection. +// Always non blocking. +func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { + err := c.write(ctx, typ, p) + if err != nil { + return fmt.Errorf("failed to write: %w", err) + } + return nil +} + +func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) error { + if c.isClosed() { + return c.closeErr + } + switch typ { + case MessageBinary: + return c.ws.SendBytes(p) + case MessageText: + return c.ws.SendText(string(p)) + default: + return fmt.Errorf("unexpected message type: %v", typ) + } +} + +func (c *Conn) isClosed() bool { + select { + case <-c.closed: + return true + default: + return false + } +} + +// Close closes the websocket with the given code and reason. +func (c *Conn) Close(code StatusCode, reason string) error { + if c.isClosed() { + return fmt.Errorf("already closed: %w", c.closeErr) + } + + cerr := CloseError{ + Code: code, + Reason: reason, + } + + err := fmt.Errorf("sent close frame: %v", cerr) + + err2 := c.ws.Close(int(code), reason) + if err2 != nil { + err = err2 + } + c.close(err) + + if !xerrors.Is(c.closeErr, cerr) { + return xerrors.Errorf("failed to close websocket: %w", err) + } + + return nil +} + +// Subprotocol returns the negotiated subprotocol. +// An empty string means the default protocol. +func (c *Conn) Subprotocol() string { + return c.ws.Protocol +} + +// DialOptions represents the options available to pass to Dial. +type DialOptions struct { + // Subprotocols lists the subprotocols to negotiate with the server. + Subprotocols []string +} + +// Dial creates a new WebSocket connection to the given url with the given options. +// The passed context bounds the maximum time spent waiting for the connection to open. +// The returned *http.Response is always nil or the zero value. It's only in the signature +// to match the core API. +func Dial(ctx context.Context, url string, opts *DialOptions) (*Conn, *http.Response, error) { + c, resp, err := dial(ctx, url, opts) + if err != nil { + return nil, resp, fmt.Errorf("failed to dial: %w", err) + } + return c, resp, nil +} + +func dial(ctx context.Context, url string, opts *DialOptions) (*Conn, *http.Response, error) { + if opts == nil { + opts = &DialOptions{} + } + + ws, err := wsjs.New(url, opts.Subprotocols) + if err != nil { + return nil, nil, err + } + + c := &Conn{ + ws: ws, + } + c.init() + + opench := make(chan struct{}) + releaseOpen := ws.OnOpen(func(e js.Value) { + close(opench) + }) + defer releaseOpen() + + select { + case <-ctx.Done(): + return nil, nil, ctx.Err() + case <-opench: + case <-c.closed: + return c, nil, c.closeErr + } + + // Have to return a non nil response as the normal API does that. + return c, &http.Response{}, nil +} diff --git a/websocket_js_test.go b/websocket_js_test.go new file mode 100644 index 00000000..332c9628 --- /dev/null +++ b/websocket_js_test.go @@ -0,0 +1,20 @@ +package websocket_test + +import ( + "context" + "testing" + "time" + + "nhooyr.io/websocket" +) + +func TestWebSocket(t *testing.T) { + t.Parallel() + + _, _, err := websocket.Dial(context.Background(), "ws://localhost:8081", nil) + if err != nil { + t.Fatal(err) + } + + time.Sleep(time.Second) +} diff --git a/websocket_test.go b/websocket_test.go index 1aa8b201..eedef845 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -1,3 +1,5 @@ +// +build !js + package websocket_test import ( diff --git a/wsjson/wsjson.go b/wsjson/wsjson.go index 1e63f940..ffdd24ac 100644 --- a/wsjson/wsjson.go +++ b/wsjson/wsjson.go @@ -1,3 +1,5 @@ +// +build !js + // Package wsjson provides websocket helpers for JSON messages. package wsjson // import "nhooyr.io/websocket/wsjson" diff --git a/wsjson/wsjson_js.go b/wsjson/wsjson_js.go new file mode 100644 index 00000000..2e6074ad --- /dev/null +++ b/wsjson/wsjson_js.go @@ -0,0 +1,58 @@ +// +build js + +package wsjson + +import ( + "context" + "encoding/json" + "fmt" + + "nhooyr.io/websocket" +) + +// Read reads a json message from c into v. +func Read(ctx context.Context, c *websocket.Conn, v interface{}) error { + err := read(ctx, c, v) + if err != nil { + return fmt.Errorf("failed to read json: %w", err) + } + return nil +} + +func read(ctx context.Context, c *websocket.Conn, v interface{}) error { + typ, b, err := c.Read(ctx) + if err != nil { + return err + } + + if typ != websocket.MessageText { + c.Close(websocket.StatusUnsupportedData, "can only accept text messages") + return fmt.Errorf("unexpected frame type for json (expected %v): %v", websocket.MessageText, typ) + } + + err = json.Unmarshal(b, v) + if err != nil { + c.Close(websocket.StatusInvalidFramePayloadData, "failed to unmarshal JSON") + return fmt.Errorf("failed to unmarshal json: %w", err) + } + + return nil +} + +// Write writes the json message v to c. +func Write(ctx context.Context, c *websocket.Conn, v interface{}) error { + err := write(ctx, c, v) + if err != nil { + return fmt.Errorf("failed to write json: %w", err) + } + return nil +} + +func write(ctx context.Context, c *websocket.Conn, v interface{}) error { + b, err := json.Marshal(v) + if err != nil { + return err + } + + return c.Write(ctx, websocket.MessageBinary, b) +} diff --git a/wspb/wspb.go b/wspb/wspb.go index 8613a080..b32b0c1b 100644 --- a/wspb/wspb.go +++ b/wspb/wspb.go @@ -1,3 +1,5 @@ +// +build !js + // Package wspb provides websocket helpers for protobuf messages. package wspb // import "nhooyr.io/websocket/wspb" @@ -5,7 +7,6 @@ import ( "bytes" "context" "fmt" - "sync" "github.com/golang/protobuf/proto" @@ -63,8 +64,6 @@ func Write(ctx context.Context, c *websocket.Conn, v proto.Message) error { return nil } -var writeBufPool sync.Pool - func write(ctx context.Context, c *websocket.Conn, v proto.Message) error { b := bpool.Get() pb := proto.NewBuffer(b.Bytes()) diff --git a/wspb/wspb_js.go b/wspb/wspb_js.go new file mode 100644 index 00000000..6f69eddd --- /dev/null +++ b/wspb/wspb_js.go @@ -0,0 +1,67 @@ +// +build js + +package wspb // import "nhooyr.io/websocket/wspb" + +import ( + "bytes" + "context" + "fmt" + + "github.com/golang/protobuf/proto" + + "nhooyr.io/websocket" + "nhooyr.io/websocket/internal/bpool" +) + +// Read reads a protobuf message from c into v. +func Read(ctx context.Context, c *websocket.Conn, v proto.Message) error { + err := read(ctx, c, v) + if err != nil { + return fmt.Errorf("failed to read protobuf: %w", err) + } + return nil +} + +func read(ctx context.Context, c *websocket.Conn, v proto.Message) error { + typ, p, err := c.Read(ctx) + if err != nil { + return err + } + + if typ != websocket.MessageBinary { + c.Close(websocket.StatusUnsupportedData, "can only accept binary messages") + return fmt.Errorf("unexpected frame type for protobuf (expected %v): %v", websocket.MessageBinary, typ) + } + + err = proto.Unmarshal(p, v) + if err != nil { + c.Close(websocket.StatusInvalidFramePayloadData, "failed to unmarshal protobuf") + return fmt.Errorf("failed to unmarshal protobuf: %w", err) + } + + return nil +} + +// Write writes the protobuf message v to c. +func Write(ctx context.Context, c *websocket.Conn, v proto.Message) error { + err := write(ctx, c, v) + if err != nil { + return fmt.Errorf("failed to write protobuf: %w", err) + } + return nil +} + +func write(ctx context.Context, c *websocket.Conn, v proto.Message) error { + b := bpool.Get() + pb := proto.NewBuffer(b.Bytes()) + defer func() { + bpool.Put(bytes.NewBuffer(pb.Bytes())) + }() + + err := pb.Marshal(v) + if err != nil { + return fmt.Errorf("failed to marshal protobuf: %w", err) + } + + return c.Write(ctx, websocket.MessageBinary, pb.Bytes()) +} diff --git a/xor.go b/xor.go index 852930df..f9fe2051 100644 --- a/xor.go +++ b/xor.go @@ -1,3 +1,5 @@ +// +build !js + package websocket import ( diff --git a/xor_test.go b/xor_test.go index 634af606..70047a9c 100644 --- a/xor_test.go +++ b/xor_test.go @@ -1,3 +1,5 @@ +// +build !js + package websocket import ( From ff4d818be5b6398839cf3c50838a1c145f2b7df0 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 21 Sep 2019 18:58:27 -0500 Subject: [PATCH 140/519] Add WASM test --- cmp_test.go => assert_test.go | 46 +++++++++++++++++++ ci/run.sh | 7 +++ ci/test.sh | 10 +++-- ci/wasm.sh | 8 +++- internal/echoserver/echoserver.go | 11 ----- internal/wsecho/cmd/main.go | 21 +++++++++ internal/wsecho/wsecho.go | 73 +++++++++++++++++++++++++++++++ websocket_autobahn_python_test.go | 6 ++- websocket_bench_test.go | 3 +- websocket_js.go | 10 ++--- websocket_js_test.go | 25 ++++++++++- websocket_test.go | 71 ++---------------------------- wsjson/wsjson_js.go | 2 +- 13 files changed, 198 insertions(+), 95 deletions(-) rename cmp_test.go => assert_test.go (55%) delete mode 100644 internal/echoserver/echoserver.go create mode 100644 internal/wsecho/cmd/main.go create mode 100644 internal/wsecho/wsecho.go diff --git a/cmp_test.go b/assert_test.go similarity index 55% rename from cmp_test.go rename to assert_test.go index ad4cd75a..2f05337e 100644 --- a/cmp_test.go +++ b/assert_test.go @@ -1,9 +1,14 @@ package websocket_test import ( + "context" + "fmt" "reflect" "github.com/google/go-cmp/cmp" + + "nhooyr.io/websocket" + "nhooyr.io/websocket/wsjson" ) // https://github.com/google/go-cmp/issues/40#issuecomment-328615283 @@ -51,3 +56,44 @@ func structTypes(v reflect.Value, m map[reflect.Type]struct{}) { } } } + +func assertEqualf(exp, act interface{}, f string, v ...interface{}) error { + if diff := cmpDiff(exp, act); diff != "" { + return fmt.Errorf(f+": %v", append(v, diff)...) + } + return nil +} + +func assertJSONEcho(ctx context.Context, c *websocket.Conn, n int) error { + exp := randString(n) + err := wsjson.Write(ctx, c, exp) + if err != nil { + return err + } + + var act interface{} + err = wsjson.Read(ctx, c, &act) + if err != nil { + return err + } + + return assertEqualf(exp, act, "unexpected JSON") +} + +func assertJSONRead(ctx context.Context, c *websocket.Conn, exp interface{}) error { + var act interface{} + err := wsjson.Read(ctx, c, &act) + if err != nil { + return err + } + + return assertEqualf(exp, act, "unexpected JSON") +} + +func randBytes(n int) []byte { + return make([]byte, n) +} + +func randString(n int) string { + return string(randBytes(n)) +} diff --git a/ci/run.sh b/ci/run.sh index 9e47d291..1e386ff1 100755 --- a/ci/run.sh +++ b/ci/run.sh @@ -6,7 +6,14 @@ set -euo pipefail cd "$(dirname "${0}")" cd "$(git rev-parse --show-toplevel)" +echo "--- fmt" ./ci/fmt.sh + +echo "--- lint" ./ci/lint.sh + +echo "--- test" ./ci/test.sh + +echo "--- wasm" ./ci/wasm.sh diff --git a/ci/test.sh b/ci/test.sh index 81d6f462..1f5b5102 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -16,9 +16,13 @@ if [[ ${CI-} ]]; then ) fi -argv+=( - "$@" -) +if [[ $# -gt 0 ]]; then + argv+=( + "$@" + ) +else + argv+=(./...) +fi mkdir -p ci/out/websocket "${argv[@]}" diff --git a/ci/wasm.sh b/ci/wasm.sh index 9894fca6..2870365f 100755 --- a/ci/wasm.sh +++ b/ci/wasm.sh @@ -4,7 +4,13 @@ set -euo pipefail cd "$(dirname "${0}")" cd "$(git rev-parse --show-toplevel)" +stdout="$(mktemp -d)/stdout" +mkfifo "$stdout" +go run ./internal/wsecho/cmd > "$stdout" & + +WS_ECHO_SERVER_URL="$(head -n 1 "$stdout")" + GOOS=js GOARCH=wasm go vet ./... go install golang.org/x/lint/golint GOOS=js GOARCH=wasm golint -set_exit_status ./... -GOOS=js GOARCH=wasm go test ./... +GOOS=js GOARCH=wasm go test ./... -args "$WS_ECHO_SERVER_URL" diff --git a/internal/echoserver/echoserver.go b/internal/echoserver/echoserver.go deleted file mode 100644 index 905ede2b..00000000 --- a/internal/echoserver/echoserver.go +++ /dev/null @@ -1,11 +0,0 @@ -package echoserver - -import ( - "net/http" -) - -// EchoServer provides a streaming WebSocket echo server -// for use in tests. -func EchoServer(w http.ResponseWriter, r *http.Request) { - -} diff --git a/internal/wsecho/cmd/main.go b/internal/wsecho/cmd/main.go new file mode 100644 index 00000000..9d9dc82b --- /dev/null +++ b/internal/wsecho/cmd/main.go @@ -0,0 +1,21 @@ +// +build !js + +package main + +import ( + "fmt" + "net/http" + "net/http/httptest" + "runtime" + "strings" + + "nhooyr.io/websocket/internal/wsecho" +) + +func main() { + s := httptest.NewServer(http.HandlerFunc(wsecho.Serve)) + wsURL := strings.Replace(s.URL, "http", "ws", 1) + fmt.Printf("%v\n", wsURL) + + runtime.Goexit() +} diff --git a/internal/wsecho/wsecho.go b/internal/wsecho/wsecho.go new file mode 100644 index 00000000..1792d0e0 --- /dev/null +++ b/internal/wsecho/wsecho.go @@ -0,0 +1,73 @@ +// +build !js + +package wsecho + +import ( + "context" + "io" + "log" + "net/http" + "time" + + "nhooyr.io/websocket" +) + +// Serve provides a streaming WebSocket echo server +// for use in tests. +func Serve(w http.ResponseWriter, r *http.Request) { + c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + Subprotocols: []string{"echo"}, + InsecureSkipVerify: true, + }) + if err != nil { + log.Printf("echo server: failed to accept: %+v", err) + return + } + defer c.Close(websocket.StatusInternalError, "") + + Loop(r.Context(), c) +} + +// Loop echos every msg received from c until an error +// occurs or the context expires. +// The read limit is set to 1 << 40. +func Loop(ctx context.Context, c *websocket.Conn) { + defer c.Close(websocket.StatusInternalError, "") + + c.SetReadLimit(1 << 40) + + ctx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + + b := make([]byte, 32768) + echo := func() error { + typ, r, err := c.Reader(ctx) + if err != nil { + return err + } + + w, err := c.Writer(ctx, typ) + if err != nil { + return err + } + + _, err = io.CopyBuffer(w, r, b) + if err != nil { + return err + } + + err = w.Close() + if err != nil { + return err + } + + return nil + } + + for { + err := echo() + if err != nil { + return + } + } +} diff --git a/websocket_autobahn_python_test.go b/websocket_autobahn_python_test.go index 4e8b588e..62aa3f8e 100644 --- a/websocket_autobahn_python_test.go +++ b/websocket_autobahn_python_test.go @@ -20,6 +20,8 @@ import ( "strings" "testing" "time" + + "nhooyr.io/websocket/internal/wsecho" ) // https://github.com/crossbario/autobahn-python/tree/master/wstest @@ -34,7 +36,7 @@ func TestPythonAutobahnServer(t *testing.T) { t.Logf("server handshake failed: %+v", err) return } - echoLoop(r.Context(), c) + wsecho.Loop(r.Context(), c) })) defer s.Close() @@ -186,7 +188,7 @@ func TestPythonAutobahnClientOld(t *testing.T) { if err != nil { t.Fatal(err) } - echoLoop(ctx, c) + wsecho.Loop(ctx, c) }() } diff --git a/websocket_bench_test.go b/websocket_bench_test.go index 9598e873..ff2fd704 100644 --- a/websocket_bench_test.go +++ b/websocket_bench_test.go @@ -13,6 +13,7 @@ import ( "time" "nhooyr.io/websocket" + "nhooyr.io/websocket/internal/wsecho" ) func BenchmarkConn(b *testing.B) { @@ -54,7 +55,7 @@ func benchConn(b *testing.B, echo, stream bool, size int) { return err } if echo { - echoLoop(r.Context(), c) + wsecho.Loop(r.Context(), c) } else { discardLoop(r.Context(), c) } diff --git a/websocket_js.go b/websocket_js.go index aab10494..a83dc872 100644 --- a/websocket_js.go +++ b/websocket_js.go @@ -59,7 +59,7 @@ func (c *Conn) init() { }) runtime.SetFinalizer(c, func(c *Conn) { - c.ws.Close(int(StatusInternalError), "internal error") + c.ws.Close(int(StatusInternalError), "") c.close(errors.New("connection garbage collected")) }) } @@ -133,12 +133,10 @@ func (c *Conn) Close(code StatusCode, reason string) error { return fmt.Errorf("already closed: %w", c.closeErr) } - cerr := CloseError{ + err := fmt.Errorf("sent close frame: %v", CloseError{ Code: code, Reason: reason, - } - - err := fmt.Errorf("sent close frame: %v", cerr) + }) err2 := c.ws.Close(int(code), reason) if err2 != nil { @@ -146,7 +144,7 @@ func (c *Conn) Close(code StatusCode, reason string) error { } c.close(err) - if !xerrors.Is(c.closeErr, cerr) { + if !xerrors.Is(c.closeErr, err) { return xerrors.Errorf("failed to close websocket: %w", err) } diff --git a/websocket_js_test.go b/websocket_js_test.go index 332c9628..56058cee 100644 --- a/websocket_js_test.go +++ b/websocket_js_test.go @@ -2,19 +2,40 @@ package websocket_test import ( "context" + "net/http" + "os" "testing" "time" "nhooyr.io/websocket" ) +var wsEchoServerURL = os.Args[1] + func TestWebSocket(t *testing.T) { t.Parallel() - _, _, err := websocket.Dial(context.Background(), "ws://localhost:8081", nil) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + c, resp, err := websocket.Dial(ctx, wsEchoServerURL, nil) if err != nil { t.Fatal(err) } + defer c.Close(websocket.StatusInternalError, "") - time.Sleep(time.Second) + err = assertEqualf(&http.Response{}, resp, "unexpected http response") + if err != nil { + t.Fatal(err) + } + + err = assertJSONEcho(ctx, c, 4096) + if err != nil { + t.Fatal(err) + } + + err = c.Close(websocket.StatusNormalClosure, "") + if err != nil { + t.Fatal(err) + } } diff --git a/websocket_test.go b/websocket_test.go index eedef845..838eb8e7 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -29,6 +29,7 @@ import ( "go.uber.org/multierr" "nhooyr.io/websocket" + "nhooyr.io/websocket/internal/wsecho" "nhooyr.io/websocket/wsjson" "nhooyr.io/websocket/wspb" ) @@ -966,7 +967,7 @@ func TestAutobahn(t *testing.T) { ctx := r.Context() if testingClient { - echoLoop(r.Context(), c) + wsecho.Loop(r.Context(), c) return nil } @@ -1007,7 +1008,7 @@ func TestAutobahn(t *testing.T) { return } - echoLoop(ctx, c) + wsecho.Loop(ctx, c) } t.Run(name, func(t *testing.T) { t.Parallel() @@ -1849,47 +1850,6 @@ func TestAutobahn(t *testing.T) { }) } -func echoLoop(ctx context.Context, c *websocket.Conn) { - defer c.Close(websocket.StatusInternalError, "") - - c.SetReadLimit(1 << 40) - - ctx, cancel := context.WithTimeout(ctx, time.Minute) - defer cancel() - - b := make([]byte, 32768) - echo := func() error { - typ, r, err := c.Reader(ctx) - if err != nil { - return err - } - - w, err := c.Writer(ctx, typ) - if err != nil { - return err - } - - _, err = io.CopyBuffer(w, r, b) - if err != nil { - return err - } - - err = w.Close() - if err != nil { - return err - } - - return nil - } - - for { - err := echo() - if err != nil { - return - } - } -} - func assertCloseStatus(err error, code websocket.StatusCode) error { var cerr websocket.CloseError if !errors.As(err, &cerr) { @@ -1898,24 +1858,6 @@ func assertCloseStatus(err error, code websocket.StatusCode) error { return assertEqualf(code, cerr.Code, "unexpected status code") } -func assertJSONRead(ctx context.Context, c *websocket.Conn, exp interface{}) (err error) { - var act interface{} - err = wsjson.Read(ctx, c, &act) - if err != nil { - return err - } - - return assertEqualf(exp, act, "unexpected JSON") -} - -func randBytes(n int) []byte { - return make([]byte, n) -} - -func randString(n int) string { - return string(randBytes(n)) -} - func assertEcho(ctx context.Context, c *websocket.Conn, typ websocket.MessageType, n int) error { p := randBytes(n) err := c.Write(ctx, typ, p) @@ -1949,13 +1891,6 @@ func assertSubprotocol(c *websocket.Conn, exp string) error { return assertEqualf(exp, c.Subprotocol(), "unexpected subprotocol") } -func assertEqualf(exp, act interface{}, f string, v ...interface{}) error { - if diff := cmpDiff(exp, act); diff != "" { - return fmt.Errorf(f+": %v", append(v, diff)...) - } - return nil -} - func assertNetConnRead(r io.Reader, exp string) error { act := make([]byte, len(exp)) _, err := r.Read(act) diff --git a/wsjson/wsjson_js.go b/wsjson/wsjson_js.go index 2e6074ad..5b88ce3b 100644 --- a/wsjson/wsjson_js.go +++ b/wsjson/wsjson_js.go @@ -54,5 +54,5 @@ func write(ctx context.Context, c *websocket.Conn, v interface{}) error { return err } - return c.Write(ctx, websocket.MessageBinary, b) + return c.Write(ctx, websocket.MessageText, b) } From 26f0793104aaf0f5555b575d0e2cb51fc580a41c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 21 Sep 2019 19:52:15 -0500 Subject: [PATCH 141/519] Add WASM and GopherJS docs --- .github/workflows/ci.yml | 8 +++---- README.md | 4 +++- ci/fmt.sh | 2 +- ci/tools.go | 1 + ci/wasm.sh | 25 ++++++++++++++------ doc.go | 23 ++++++++++++++++++ go.mod | 1 + go.sum | 30 ++++++++++++++++++++++++ internal/wsecho/cmd/{ => wsecho}/main.go | 0 internal/wsecho/wsecho.go | 6 ++--- statuscode.go | 21 ++++++++++------- statuscode_string.go | 6 ++--- websocket_js.go | 2 +- websocket_js_test.go | 6 ++--- websocket_test.go | 4 ++-- 15 files changed, 105 insertions(+), 34 deletions(-) rename internal/wsecho/cmd/{ => wsecho}/main.go (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4d889ab5..b07c54b8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,7 @@ on: [push] jobs: fmt: runs-on: ubuntu-latest - container: docker://nhooyr/websocket-ci@sha256:6f6a00284eff008ad2cece8f3d0b4a2a3a8f2fcf7a54c691c64a92403abc4c75 + container: docker://nhooyr/websocket-ci@sha256:b6331f8f64803c8b1bbd2a0ee9e2547317e0de2348bccd9c8dbcc1d88ff5747f steps: - uses: actions/checkout@v1 with: @@ -12,7 +12,7 @@ jobs: - run: ./ci/fmt.sh lint: runs-on: ubuntu-latest - container: docker://nhooyr/websocket-ci@sha256:6f6a00284eff008ad2cece8f3d0b4a2a3a8f2fcf7a54c691c64a92403abc4c75 + container: docker://nhooyr/websocket-ci@sha256:b6331f8f64803c8b1bbd2a0ee9e2547317e0de2348bccd9c8dbcc1d88ff5747f steps: - uses: actions/checkout@v1 with: @@ -20,7 +20,7 @@ jobs: - run: ./ci/lint.sh test: runs-on: ubuntu-latest - container: docker://nhooyr/websocket-ci@sha256:6f6a00284eff008ad2cece8f3d0b4a2a3a8f2fcf7a54c691c64a92403abc4c75 + container: docker://nhooyr/websocket-ci@sha256:b6331f8f64803c8b1bbd2a0ee9e2547317e0de2348bccd9c8dbcc1d88ff5747f steps: - uses: actions/checkout@v1 with: @@ -30,7 +30,7 @@ jobs: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} wasm: runs-on: ubuntu-latest - container: docker://nhooyr/websocket-ci@sha256:6f6a00284eff008ad2cece8f3d0b4a2a3a8f2fcf7a54c691c64a92403abc4c75 + container: docker://nhooyr/websocket-ci@sha256:b6331f8f64803c8b1bbd2a0ee9e2547317e0de2348bccd9c8dbcc1d88ff5747f steps: - uses: actions/checkout@v1 with: diff --git a/README.md b/README.md index f25dc79e..8b98ac04 100644 --- a/README.md +++ b/README.md @@ -23,11 +23,11 @@ go get nhooyr.io/websocket - JSON and ProtoBuf helpers in the [wsjson](https://godoc.org/nhooyr.io/websocket/wsjson) and [wspb](https://godoc.org/nhooyr.io/websocket/wspb) subpackages - Highly optimized by default - Concurrent writes out of the box +- [Complete WASM](https://godoc.org/nhooyr.io/websocket#hdr-WASM) support ## Roadmap - [ ] WebSockets over HTTP/2 [#4](https://github.com/nhooyr/websocket/issues/4) -- [ ] WASM Compilation [#121](https://github.com/nhooyr/websocket/issues/121) ## Examples @@ -131,6 +131,8 @@ which results in awkward control flow. With nhooyr/websocket you use the Ping me that sends a ping and also waits for the pong, though you must be reading from the connection for the pong to be read. +Additionally, nhooyr.io/websocket can compile to [WASM](https://godoc.org/nhooyr.io/websocket#hdr-WASM) for the browser. + In terms of performance, the differences mostly depend on your application code. nhooyr/websocket reuses message buffers out of the box if you use the wsjson and wspb subpackages. As mentioned above, nhooyr/websocket also supports concurrent writers. diff --git a/ci/fmt.sh b/ci/fmt.sh index dee94e87..d6251e05 100755 --- a/ci/fmt.sh +++ b/ci/fmt.sh @@ -18,7 +18,7 @@ fmt() { go run go.coder.com/go-tools/cmd/goimports -w "-local=$(go list -m)" . go run mvdan.cc/sh/cmd/shfmt -i 2 -w -s -sr . # shellcheck disable=SC2046 - npx prettier \ + npx -q prettier \ --write \ --print-width 120 \ --no-semi \ diff --git a/ci/tools.go b/ci/tools.go index 5aebe7d4..1ec11eb4 100644 --- a/ci/tools.go +++ b/ci/tools.go @@ -4,6 +4,7 @@ package ci // See https://github.com/go-modules-by-example/index/blob/master/010_tools/README.md import ( + _ "github.com/agnivade/wasmbrowsertest" _ "go.coder.com/go-tools/cmd/goimports" _ "golang.org/x/lint/golint" _ "golang.org/x/tools/cmd/stringer" diff --git a/ci/wasm.sh b/ci/wasm.sh index 2870365f..eb4a0cf3 100755 --- a/ci/wasm.sh +++ b/ci/wasm.sh @@ -4,13 +4,24 @@ set -euo pipefail cd "$(dirname "${0}")" cd "$(git rev-parse --show-toplevel)" -stdout="$(mktemp -d)/stdout" -mkfifo "$stdout" -go run ./internal/wsecho/cmd > "$stdout" & - -WS_ECHO_SERVER_URL="$(head -n 1 "$stdout")" - GOOS=js GOARCH=wasm go vet ./... + go install golang.org/x/lint/golint GOOS=js GOARCH=wasm golint -set_exit_status ./... -GOOS=js GOARCH=wasm go test ./... -args "$WS_ECHO_SERVER_URL" + +wsEchoOut="$(mktemp -d)/stdout" +mkfifo "$wsEchoOut" +go install ./internal/wsecho/cmd/wsecho +wsecho > "$wsEchoOut" & + +WS_ECHO_SERVER_URL="$(timeout 10s head -n 1 "$wsEchoOut")" || true +if [[ -z $WS_ECHO_SERVER_URL ]]; then + echo "./internal/wsecho/cmd/wsecho failed to start in 10s" + exit 1 +fi + +go install github.com/agnivade/wasmbrowsertest +GOOS=js GOARCH=wasm go test -exec=wasmbrowsertest ./... -args "$WS_ECHO_SERVER_URL" + +kill %1 +wait -n || true diff --git a/doc.go b/doc.go index cb33c5c9..4c07d37a 100644 --- a/doc.go +++ b/doc.go @@ -17,4 +17,27 @@ // // Use the errors.As function new in Go 1.13 to check for websocket.CloseError. // See the CloseError example. +// +// WASM +// +// The client side fully supports compiling to WASM. +// It wraps the WebSocket browser API. +// See https://developer.mozilla.org/en-US/docs/Web/API/WebSocket +// +// Thus the unsupported features when compiling to WASM are: +// - Accept API +// - Reader/Writer API +// - SetReadLimit +// - Ping +// - HTTPClient and HTTPHeader dial options +// +// The *http.Response returned by Dial will always either be nil or &http.Response{} as +// we do not have access to the handshake response in the browser. +// +// Writes are also always async so the passed context is no-op. +// +// Everything else is fully supported. This includes the wsjson and wspb helper packages. +// +// Once https://github.com/gopherjs/gopherjs/issues/929 is closed, GopherJS should be supported +// as well. package websocket // import "nhooyr.io/websocket" diff --git a/go.mod b/go.mod index 6b3f28ad..ab46375c 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module nhooyr.io/websocket go 1.13 require ( + github.com/agnivade/wasmbrowsertest v0.3.0 github.com/fatih/color v1.7.0 // indirect github.com/golang/protobuf v1.3.2 github.com/google/go-cmp v0.3.1 diff --git a/go.sum b/go.sum index de366e52..4af00946 100644 --- a/go.sum +++ b/go.sum @@ -1,23 +1,44 @@ +github.com/agnivade/wasmbrowsertest v0.3.0 h1:5pAabhWzTVCLoVWqYejEbmWyzNGFR7K/Nu5lsmD1fVc= +github.com/agnivade/wasmbrowsertest v0.3.0/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI= +github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw= +github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0 h1:4Wocv9f+KWF4GtZudyrn8JSBTgHQbGp86mcsoH7j1iQ= +github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw= +github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901 h1:tg66ykM8VYqP9k4DFQwSMnYv84HNTruF+GR6kefFNg4= +github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/fatih/color v1.6.0 h1:66qjqZk8kalYAvDRtM1AdAJQI0tj4Wrue3Eq3B3pmFU= github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c h1:DLLAPVFrk9iNzljMKF512CUmrFImQ6WU3sDiUS4IRqk= +github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f h1:Jnx61latede7zDD3DiiP4gmNz33uK0U5HDUaF0a/HVQ= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307 h1:vl4eIlySbjertFaNwiMjXsGrFVK25aOWLq7n+3gh2ls= +github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= @@ -27,6 +48,10 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481 h1:IaSjLMT6WvkoZZjspGxy3rdaTEmWLoRm49WbtVUi9sA= +github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= @@ -61,6 +86,8 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc h1:RTUQlKzoZZVG3umWNzOYeFecQLIh+dbxXvJp1zPQJTI= +github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A= go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16 h1:3gGa1bM0nG7Ruhu5b7wKnoOOwAD/fJ8iyyAcpOzDG3A= go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw= go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= @@ -86,7 +113,10 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190919044723-0c1ff786ef13 h1:/zi0zzlPHWXYXrO1LjNRByFu8sdGgCkj2JLDdBIB84k= golang.org/x/sys v0.0.0-20190919044723-0c1ff786ef13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/wsecho/cmd/main.go b/internal/wsecho/cmd/wsecho/main.go similarity index 100% rename from internal/wsecho/cmd/main.go rename to internal/wsecho/cmd/wsecho/main.go diff --git a/internal/wsecho/wsecho.go b/internal/wsecho/wsecho.go index 1792d0e0..8f531f1d 100644 --- a/internal/wsecho/wsecho.go +++ b/internal/wsecho/wsecho.go @@ -30,16 +30,16 @@ func Serve(w http.ResponseWriter, r *http.Request) { // Loop echos every msg received from c until an error // occurs or the context expires. -// The read limit is set to 1 << 40. +// The read limit is set to 1 << 30. func Loop(ctx context.Context, c *websocket.Conn) { defer c.Close(websocket.StatusInternalError, "") - c.SetReadLimit(1 << 40) + c.SetReadLimit(1 << 30) ctx, cancel := context.WithTimeout(ctx, time.Minute) defer cancel() - b := make([]byte, 32768) + b := make([]byte, 32<<10) echo := func() error { typ, r, err := c.Reader(ctx) if err != nil { diff --git a/statuscode.go b/statuscode.go index d2a64d62..e7bb9499 100644 --- a/statuscode.go +++ b/statuscode.go @@ -18,12 +18,15 @@ const ( StatusGoingAway StatusProtocolError StatusUnsupportedData + _ // 1004 is reserved. + StatusNoStatusRcvd - // statusAbnormalClosure is unexported because it isn't necessary, at least until WASM. - // The error returned will indicate whether the connection was closed or not or what happened. - // It only makes sense for browser clients. - statusAbnormalClosure + + // This StatusCode is only exported for use with WASM. + // In pure Go, the returned error will indicate whether the connection was closed or not or what happened. + StatusAbnormalClosure + StatusInvalidFramePayloadData StatusPolicyViolation StatusMessageTooBig @@ -32,10 +35,10 @@ const ( StatusServiceRestart StatusTryAgainLater StatusBadGateway - // statusTLSHandshake is unexported because we just return - // the handshake error in dial. We do not return a conn - // so there is nothing to use this on. At least until WASM. - statusTLSHandshake + + // This StatusCode is only exported for use with WASM. + // In pure Go, the returned error will indicate whether there was a TLS handshake failure. + StatusTLSHandshake ) // CloseError represents a WebSocket close frame. @@ -79,7 +82,7 @@ func parseClosePayload(p []byte) (CloseError, error) { // and https://tools.ietf.org/html/rfc6455#section-7.4.1 func validWireCloseCode(code StatusCode) bool { switch code { - case 1004, StatusNoStatusRcvd, statusAbnormalClosure, statusTLSHandshake: + case 1004, StatusNoStatusRcvd, StatusAbnormalClosure, StatusTLSHandshake: return false } diff --git a/statuscode_string.go b/statuscode_string.go index 11725e4d..fc8cea0d 100644 --- a/statuscode_string.go +++ b/statuscode_string.go @@ -13,7 +13,7 @@ func _() { _ = x[StatusProtocolError-1002] _ = x[StatusUnsupportedData-1003] _ = x[StatusNoStatusRcvd-1005] - _ = x[statusAbnormalClosure-1006] + _ = x[StatusAbnormalClosure-1006] _ = x[StatusInvalidFramePayloadData-1007] _ = x[StatusPolicyViolation-1008] _ = x[StatusMessageTooBig-1009] @@ -22,12 +22,12 @@ func _() { _ = x[StatusServiceRestart-1012] _ = x[StatusTryAgainLater-1013] _ = x[StatusBadGateway-1014] - _ = x[statusTLSHandshake-1015] + _ = x[StatusTLSHandshake-1015] } const ( _StatusCode_name_0 = "StatusNormalClosureStatusGoingAwayStatusProtocolErrorStatusUnsupportedData" - _StatusCode_name_1 = "StatusNoStatusRcvdstatusAbnormalClosureStatusInvalidFramePayloadDataStatusPolicyViolationStatusMessageTooBigStatusMandatoryExtensionStatusInternalErrorStatusServiceRestartStatusTryAgainLaterStatusBadGatewaystatusTLSHandshake" + _StatusCode_name_1 = "StatusNoStatusRcvdStatusAbnormalClosureStatusInvalidFramePayloadDataStatusPolicyViolationStatusMessageTooBigStatusMandatoryExtensionStatusInternalErrorStatusServiceRestartStatusTryAgainLaterStatusBadGatewayStatusTLSHandshake" ) var ( diff --git a/websocket_js.go b/websocket_js.go index a83dc872..0782e046 100644 --- a/websocket_js.go +++ b/websocket_js.go @@ -170,7 +170,7 @@ type DialOptions struct { func Dial(ctx context.Context, url string, opts *DialOptions) (*Conn, *http.Response, error) { c, resp, err := dial(ctx, url, opts) if err != nil { - return nil, resp, fmt.Errorf("failed to dial: %w", err) + return nil, resp, fmt.Errorf("failed to websocket dial: %w", err) } return c, resp, nil } diff --git a/websocket_js_test.go b/websocket_js_test.go index 56058cee..9ced6581 100644 --- a/websocket_js_test.go +++ b/websocket_js_test.go @@ -2,19 +2,19 @@ package websocket_test import ( "context" + "flag" "net/http" - "os" "testing" "time" "nhooyr.io/websocket" ) -var wsEchoServerURL = os.Args[1] - func TestWebSocket(t *testing.T) { t.Parallel() + wsEchoServerURL := flag.Arg(0) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() diff --git a/websocket_test.go b/websocket_test.go index 838eb8e7..36a52245 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -963,7 +963,6 @@ func TestAutobahn(t *testing.T) { return err } defer c.Close(websocket.StatusInternalError, "") - c.SetReadLimit(1 << 40) ctx := r.Context() if testingClient { @@ -971,6 +970,7 @@ func TestAutobahn(t *testing.T) { return nil } + c.SetReadLimit(1 << 30) err = fn(ctx, c) if err != nil { return err @@ -997,9 +997,9 @@ func TestAutobahn(t *testing.T) { t.Fatal(err) } defer c.Close(websocket.StatusInternalError, "") - c.SetReadLimit(1 << 40) if testingClient { + c.SetReadLimit(1 << 30) err = fn(ctx, c) if err != nil { t.Fatalf("client failed: %+v", err) From 5da52be0abba3d584d5b0ac72cf44410ca2050e9 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 21 Sep 2019 21:52:15 -0500 Subject: [PATCH 142/519] Update line count in the README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8b98ac04..ed22b1de 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ go get nhooyr.io/websocket ## Features - Minimal and idiomatic API -- Tiny codebase at 1700 lines +- Tiny codebase at 2200 lines - First class [context.Context](https://blog.golang.org/context) support - Thorough tests, fully passes the [autobahn-testsuite](https://github.com/crossbario/autobahn-testsuite) - [Zero dependencies](https://godoc.org/nhooyr.io/websocket?imports) @@ -115,7 +115,7 @@ Just compare the godoc of The API for nhooyr/websocket has been designed such that there is only one way to do things which makes it easy to use correctly. Not only is the API simpler, the implementation is -only 1700 lines whereas gorilla/websocket is at 3500 lines. That's more code to maintain, +only 2200 lines whereas gorilla/websocket is at 3500 lines. That's more code to maintain, more code to test, more code to document and more surface area for bugs. Moreover, nhooyr/websocket has support for newer Go idioms such as context.Context and From 76a6a2631ff5efecc76b904553e692886b0e0b76 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 22 Sep 2019 12:21:46 -0500 Subject: [PATCH 143/519] Improve wasm test coverage --- assert_test.go | 29 ++++++++++++++++++-- ci/wasm.sh | 19 +++++++------ go.mod | 1 - internal/wsecho/cmd/wsecho/main.go | 21 --------------- internal/wsecho/wsecho.go | 22 ++------------- internal/wsjs/wsjs.go | 2 +- internal/wsjstest/main.go | 43 ++++++++++++++++++++++++++++++ netconn_js.go | 17 ------------ netconn_normal.go | 12 --------- websocket.go | 4 +++ websocket_js.go | 22 ++++++++++----- websocket_js_test.go | 20 +++++++++++--- websocket_test.go | 21 --------------- 13 files changed, 120 insertions(+), 113 deletions(-) delete mode 100644 internal/wsecho/cmd/wsecho/main.go create mode 100644 internal/wsjstest/main.go delete mode 100644 netconn_js.go delete mode 100644 netconn_normal.go diff --git a/assert_test.go b/assert_test.go index 2f05337e..cddae99d 100644 --- a/assert_test.go +++ b/assert_test.go @@ -2,7 +2,9 @@ package websocket_test import ( "context" + "encoding/hex" "fmt" + "math/rand" "reflect" "github.com/google/go-cmp/cmp" @@ -91,9 +93,32 @@ func assertJSONRead(ctx context.Context, c *websocket.Conn, exp interface{}) err } func randBytes(n int) []byte { - return make([]byte, n) + b := make([]byte, n) + rand.Read(b) + return b } func randString(n int) string { - return string(randBytes(n)) + return hex.EncodeToString(randBytes(n))[:n] +} + +func assertEcho(ctx context.Context, c *websocket.Conn, typ websocket.MessageType, n int) error { + p := randBytes(n) + err := c.Write(ctx, typ, p) + if err != nil { + return err + } + typ2, p2, err := c.Read(ctx) + if err != nil { + return err + } + err = assertEqualf(typ, typ2, "unexpected data type") + if err != nil { + return err + } + return assertEqualf(p, p2, "unexpected payload") +} + +func assertSubprotocol(c *websocket.Conn, exp string) error { + return assertEqualf(exp, c.Subprotocol(), "unexpected subprotocol") } diff --git a/ci/wasm.sh b/ci/wasm.sh index eb4a0cf3..0290f188 100755 --- a/ci/wasm.sh +++ b/ci/wasm.sh @@ -9,19 +9,22 @@ GOOS=js GOARCH=wasm go vet ./... go install golang.org/x/lint/golint GOOS=js GOARCH=wasm golint -set_exit_status ./... -wsEchoOut="$(mktemp -d)/stdout" -mkfifo "$wsEchoOut" -go install ./internal/wsecho/cmd/wsecho -wsecho > "$wsEchoOut" & +wsjstestOut="$(mktemp -d)/stdout" +mkfifo "$wsjstestOut" +go install ./internal/wsjstest +timeout 30s wsjstest > "$wsjstestOut" & +wsjstestPID=$! -WS_ECHO_SERVER_URL="$(timeout 10s head -n 1 "$wsEchoOut")" || true +WS_ECHO_SERVER_URL="$(timeout 10s head -n 1 "$wsjstestOut")" || true if [[ -z $WS_ECHO_SERVER_URL ]]; then - echo "./internal/wsecho/cmd/wsecho failed to start in 10s" + echo "./internal/wsjstest failed to start in 10s" exit 1 fi go install github.com/agnivade/wasmbrowsertest GOOS=js GOARCH=wasm go test -exec=wasmbrowsertest ./... -args "$WS_ECHO_SERVER_URL" -kill %1 -wait -n || true +if ! wait "$wsjstestPID"; then + echo "wsjstest exited unsuccessfully" + exit 1 +fi diff --git a/go.mod b/go.mod index ab46375c..86a9403b 100644 --- a/go.mod +++ b/go.mod @@ -23,7 +23,6 @@ require ( golang.org/x/sys v0.0.0-20190919044723-0c1ff786ef13 // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72 - golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect gotest.tools/gotestsum v0.3.5 mvdan.cc/sh v2.6.4+incompatible diff --git a/internal/wsecho/cmd/wsecho/main.go b/internal/wsecho/cmd/wsecho/main.go deleted file mode 100644 index 9d9dc82b..00000000 --- a/internal/wsecho/cmd/wsecho/main.go +++ /dev/null @@ -1,21 +0,0 @@ -// +build !js - -package main - -import ( - "fmt" - "net/http" - "net/http/httptest" - "runtime" - "strings" - - "nhooyr.io/websocket/internal/wsecho" -) - -func main() { - s := httptest.NewServer(http.HandlerFunc(wsecho.Serve)) - wsURL := strings.Replace(s.URL, "http", "ws", 1) - fmt.Printf("%v\n", wsURL) - - runtime.Goexit() -} diff --git a/internal/wsecho/wsecho.go b/internal/wsecho/wsecho.go index 8f531f1d..c408f07f 100644 --- a/internal/wsecho/wsecho.go +++ b/internal/wsecho/wsecho.go @@ -5,33 +5,15 @@ package wsecho import ( "context" "io" - "log" - "net/http" "time" "nhooyr.io/websocket" ) -// Serve provides a streaming WebSocket echo server -// for use in tests. -func Serve(w http.ResponseWriter, r *http.Request) { - c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ - Subprotocols: []string{"echo"}, - InsecureSkipVerify: true, - }) - if err != nil { - log.Printf("echo server: failed to accept: %+v", err) - return - } - defer c.Close(websocket.StatusInternalError, "") - - Loop(r.Context(), c) -} - // Loop echos every msg received from c until an error // occurs or the context expires. // The read limit is set to 1 << 30. -func Loop(ctx context.Context, c *websocket.Conn) { +func Loop(ctx context.Context, c *websocket.Conn) error { defer c.Close(websocket.StatusInternalError, "") c.SetReadLimit(1 << 30) @@ -67,7 +49,7 @@ func Loop(ctx context.Context, c *websocket.Conn) { for { err := echo() if err != nil { - return + return err } } } diff --git a/internal/wsjs/wsjs.go b/internal/wsjs/wsjs.go index f83b766c..68078cf2 100644 --- a/internal/wsjs/wsjs.go +++ b/internal/wsjs/wsjs.go @@ -41,7 +41,7 @@ func New(url string, protocols []string) (c WebSocket, err error) { v: js.Global().Get("WebSocket").New(url, jsProtocols), } - c.setBinaryType("arrayBuffer") + c.setBinaryType("arraybuffer") c.Extensions = c.v.Get("extensions").String() c.Protocol = c.v.Get("protocol").String() diff --git a/internal/wsjstest/main.go b/internal/wsjstest/main.go new file mode 100644 index 00000000..a1ad1b02 --- /dev/null +++ b/internal/wsjstest/main.go @@ -0,0 +1,43 @@ +// +build !js + +package main + +import ( + "errors" + "fmt" + "log" + "net/http" + "net/http/httptest" + "os" + "runtime" + "strings" + + "nhooyr.io/websocket" + "nhooyr.io/websocket/internal/wsecho" +) + +func main() { + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + Subprotocols: []string{"echo"}, + InsecureSkipVerify: true, + }) + if err != nil { + log.Fatalf("echo server: failed to accept: %+v", err) + } + defer c.Close(websocket.StatusInternalError, "") + + err = wsecho.Loop(r.Context(), c) + + var ce websocket.CloseError + if !errors.As(err, &ce) || ce.Code != websocket.StatusNormalClosure { + log.Fatalf("unexpected loop error: %+v", err) + } + + os.Exit(0) + })) + wsURL := strings.Replace(s.URL, "http", "ws", 1) + fmt.Printf("%v\n", wsURL) + + runtime.Goexit() +} diff --git a/netconn_js.go b/netconn_js.go deleted file mode 100644 index 5cd15d47..00000000 --- a/netconn_js.go +++ /dev/null @@ -1,17 +0,0 @@ -// +build js - -package websocket - -import ( - "bytes" - "context" - "io" -) - -func (c *netConn) netConnReader(ctx context.Context) (MessageType, io.Reader, error) { - typ, p, err := c.c.Read(ctx) - if err != nil { - return 0, nil, err - } - return typ, bytes.NewReader(p), nil -} diff --git a/netconn_normal.go b/netconn_normal.go deleted file mode 100644 index 0db551d4..00000000 --- a/netconn_normal.go +++ /dev/null @@ -1,12 +0,0 @@ -// +build !js - -package websocket - -import ( - "context" - "io" -) - -func (c *netConn) netConnReader(ctx context.Context) (MessageType, io.Reader, error) { - return c.c.Reader(c.readContext) -} diff --git a/websocket.go b/websocket.go index 596d89f3..bbadb9bc 100644 --- a/websocket.go +++ b/websocket.go @@ -946,3 +946,7 @@ func (c *Conn) extractBufioWriterBuf(w io.Writer) { c.bw.Reset(w) } + +func (c *netConn) netConnReader(ctx context.Context) (MessageType, io.Reader, error) { + return c.c.Reader(c.readContext) +} diff --git a/websocket_js.go b/websocket_js.go index 0782e046..14f198d1 100644 --- a/websocket_js.go +++ b/websocket_js.go @@ -1,17 +1,17 @@ package websocket // import "nhooyr.io/websocket" import ( + "bytes" "context" "errors" "fmt" + "io" "net/http" "reflect" "runtime" "sync" "syscall/js" - "golang.org/x/xerrors" - "nhooyr.io/websocket/internal/wsjs" ) @@ -35,9 +35,6 @@ func (c *Conn) close(err error) { c.closeErr = fmt.Errorf("websocket closed: %w", err) close(c.closed) - - c.releaseOnClose() - c.releaseOnMessage() }) } @@ -52,6 +49,9 @@ func (c *Conn) init() { } c.close(fmt.Errorf("received close frame: %w", cerr)) + + c.releaseOnClose() + c.releaseOnMessage() }) c.releaseOnMessage = c.ws.OnMessage(func(e wsjs.MessageEvent) { @@ -144,8 +144,8 @@ func (c *Conn) Close(code StatusCode, reason string) error { } c.close(err) - if !xerrors.Is(c.closeErr, err) { - return xerrors.Errorf("failed to close websocket: %w", err) + if !errors.Is(c.closeErr, err) { + return fmt.Errorf("failed to close websocket: %w", err) } return nil @@ -207,3 +207,11 @@ func dial(ctx context.Context, url string, opts *DialOptions) (*Conn, *http.Resp // Have to return a non nil response as the normal API does that. return c, &http.Response{}, nil } + +func (c *netConn) netConnReader(ctx context.Context) (MessageType, io.Reader, error) { + typ, p, err := c.c.Read(ctx) + if err != nil { + return 0, nil, err + } + return typ, bytes.NewReader(p), nil +} diff --git a/websocket_js_test.go b/websocket_js_test.go index 9ced6581..1142190c 100644 --- a/websocket_js_test.go +++ b/websocket_js_test.go @@ -10,7 +10,7 @@ import ( "nhooyr.io/websocket" ) -func TestWebSocket(t *testing.T) { +func TestConn(t *testing.T) { t.Parallel() wsEchoServerURL := flag.Arg(0) @@ -18,18 +18,30 @@ func TestWebSocket(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() - c, resp, err := websocket.Dial(ctx, wsEchoServerURL, nil) + c, resp, err := websocket.Dial(ctx, wsEchoServerURL, &websocket.DialOptions{ + Subprotocols: []string{"echo"}, + }) if err != nil { t.Fatal(err) } defer c.Close(websocket.StatusInternalError, "") + assertSubprotocol(c, "echo") + if err != nil { + t.Fatal(err) + } + err = assertEqualf(&http.Response{}, resp, "unexpected http response") if err != nil { t.Fatal(err) } - err = assertJSONEcho(ctx, c, 4096) + err = assertJSONEcho(ctx, c, 16) + if err != nil { + t.Fatal(err) + } + + err = assertEcho(ctx, c, websocket.MessageBinary, 16) if err != nil { t.Fatal(err) } @@ -38,4 +50,6 @@ func TestWebSocket(t *testing.T) { if err != nil { t.Fatal(err) } + + time.Sleep(time.Millisecond * 100) } diff --git a/websocket_test.go b/websocket_test.go index 36a52245..2fabba54 100644 --- a/websocket_test.go +++ b/websocket_test.go @@ -1858,23 +1858,6 @@ func assertCloseStatus(err error, code websocket.StatusCode) error { return assertEqualf(code, cerr.Code, "unexpected status code") } -func assertEcho(ctx context.Context, c *websocket.Conn, typ websocket.MessageType, n int) error { - p := randBytes(n) - err := c.Write(ctx, typ, p) - if err != nil { - return err - } - typ2, p2, err := c.Read(ctx) - if err != nil { - return err - } - err = assertEqualf(typ, typ2, "unexpected data type") - if err != nil { - return err - } - return assertEqualf(p, p2, "unexpected payload") -} - func assertProtobufRead(ctx context.Context, c *websocket.Conn, exp interface{}) error { expType := reflect.TypeOf(exp) actv := reflect.New(expType.Elem()) @@ -1887,10 +1870,6 @@ func assertProtobufRead(ctx context.Context, c *websocket.Conn, exp interface{}) return assertEqualf(exp, act, "unexpected protobuf") } -func assertSubprotocol(c *websocket.Conn, exp string) error { - return assertEqualf(exp, c.Subprotocol(), "unexpected subprotocol") -} - func assertNetConnRead(r io.Reader, exp string) error { act := make([]byte, len(exp)) _, err := r.Read(act) From 5c656edde219b8d78a6aaea576349d89df4b55fb Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 22 Sep 2019 13:16:26 -0500 Subject: [PATCH 144/519] Restructure library Removes the tiny filenames polluting the root directory. --- websocket.go => conn.go | 2 +- websocket_test.go => conn_test.go | 369 +++++++++++++++++++++ dial.go | 200 ----------- dial_test.go | 146 -------- doc.go | 7 +- frame.go | 423 ++++++++++++++++++++++++ statuscode_string.go => frame_string.go | 54 ++- frame_test.go | 373 +++++++++++++++++++++ accept.go => handshake.go | 187 +++++++++++ accept_test.go => handshake_test.go | 138 ++++++++ header.go | 158 --------- header_test.go | 155 --------- messagetype.go | 17 - messagetype_string.go | 25 -- opcode.go | 31 -- opcode_string.go | 39 --- statuscode.go | 113 ------- statuscode_test.go | 157 --------- websocket_autobahn_python_test.go | 243 -------------- websocket_bench_test.go | 148 --------- websocket_js_test.go | 2 - xor.go | 127 ------- xor_test.go | 84 ----- 23 files changed, 1547 insertions(+), 1651 deletions(-) rename websocket.go => conn.go (99%) rename websocket_test.go => conn_test.go (84%) delete mode 100644 dial.go delete mode 100644 dial_test.go create mode 100644 frame.go rename statuscode_string.go => frame_string.go (51%) create mode 100644 frame_test.go rename accept.go => handshake.go (54%) rename accept_test.go => handshake_test.go (63%) delete mode 100644 header.go delete mode 100644 header_test.go delete mode 100644 messagetype.go delete mode 100644 messagetype_string.go delete mode 100644 opcode.go delete mode 100644 opcode_string.go delete mode 100644 statuscode.go delete mode 100644 statuscode_test.go delete mode 100644 websocket_autobahn_python_test.go delete mode 100644 websocket_bench_test.go delete mode 100644 xor.go delete mode 100644 xor_test.go diff --git a/websocket.go b/conn.go similarity index 99% rename from websocket.go rename to conn.go index bbadb9bc..bc115e38 100644 --- a/websocket.go +++ b/conn.go @@ -138,7 +138,7 @@ func (c *Conn) close(err error) { // closeErr. c.closer.Close() - // See comment in dial.go + // See comment on bufioReaderPool in handshake.go if c.client { // By acquiring the locks, we ensure no goroutine will touch the bufio reader or writer // and we can safely return them. diff --git a/websocket_test.go b/conn_test.go similarity index 84% rename from websocket_test.go rename to conn_test.go index 2fabba54..8846979d 100644 --- a/websocket_test.go +++ b/conn_test.go @@ -12,10 +12,13 @@ import ( "io" "io/ioutil" "math/rand" + "net" "net/http" "net/http/cookiejar" "net/http/httptest" "net/url" + "os" + "os/exec" "reflect" "strconv" "strings" @@ -1962,3 +1965,369 @@ func assertReadMessage(ctx context.Context, c *websocket.Conn, typ websocket.Mes } return assertEqualf(p, actP, "unexpected frame %v payload", actTyp) } + +func BenchmarkConn(b *testing.B) { + sizes := []int{ + 2, + 16, + 32, + 512, + 4096, + 16384, + } + + b.Run("write", func(b *testing.B) { + for _, size := range sizes { + b.Run(strconv.Itoa(size), func(b *testing.B) { + b.Run("stream", func(b *testing.B) { + benchConn(b, false, true, size) + }) + b.Run("buffer", func(b *testing.B) { + benchConn(b, false, false, size) + }) + }) + } + }) + + b.Run("echo", func(b *testing.B) { + for _, size := range sizes { + b.Run(strconv.Itoa(size), func(b *testing.B) { + benchConn(b, true, true, size) + }) + } + }) +} + +func benchConn(b *testing.B, echo, stream bool, size int) { + s, closeFn := testServer(b, func(w http.ResponseWriter, r *http.Request) error { + c, err := websocket.Accept(w, r, nil) + if err != nil { + return err + } + if echo { + wsecho.Loop(r.Context(), c) + } else { + discardLoop(r.Context(), c) + } + return nil + }, false) + defer closeFn() + + wsURL := strings.Replace(s.URL, "http", "ws", 1) + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) + defer cancel() + + c, _, err := websocket.Dial(ctx, wsURL, nil) + if err != nil { + b.Fatal(err) + } + defer c.Close(websocket.StatusInternalError, "") + + msg := []byte(strings.Repeat("2", size)) + readBuf := make([]byte, len(msg)) + b.SetBytes(int64(len(msg))) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + if stream { + w, err := c.Writer(ctx, websocket.MessageText) + if err != nil { + b.Fatal(err) + } + + _, err = w.Write(msg) + if err != nil { + b.Fatal(err) + } + + err = w.Close() + if err != nil { + b.Fatal(err) + } + } else { + err = c.Write(ctx, websocket.MessageText, msg) + if err != nil { + b.Fatal(err) + } + } + + if echo { + _, r, err := c.Reader(ctx) + if err != nil { + b.Fatal(err) + } + + _, err = io.ReadFull(r, readBuf) + if err != nil { + b.Fatal(err) + } + } + } + b.StopTimer() + + c.Close(websocket.StatusNormalClosure, "") +} + +func discardLoop(ctx context.Context, c *websocket.Conn) { + defer c.Close(websocket.StatusInternalError, "") + + ctx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + + b := make([]byte, 32768) + echo := func() error { + _, r, err := c.Reader(ctx) + if err != nil { + return err + } + + _, err = io.CopyBuffer(ioutil.Discard, r, b) + if err != nil { + return err + } + return nil + } + + for { + err := echo() + if err != nil { + return + } + } +} + +func TestAutobahnPython(t *testing.T) { + // This test contains the old autobahn test suite tests that use the + // python binary. The approach is clunky and slow so new tests + // have been written in pure Go in websocket_test.go. + // These have been kept for correctness purposes and are occasionally ran. + if os.Getenv("AUTOBAHN_PYTHON") == "" { + t.Skip("Set $AUTOBAHN_PYTHON to run tests against the python autobahn test suite") + } + + t.Run("server", testServerAutobahnPython) + t.Run("client", testClientAutobahnPython) +} + +// https://github.com/crossbario/autobahn-python/tree/master/wstest +func testServerAutobahnPython(t *testing.T) { + t.Parallel() + + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + Subprotocols: []string{"echo"}, + }) + if err != nil { + t.Logf("server handshake failed: %+v", err) + return + } + wsecho.Loop(r.Context(), c) + })) + defer s.Close() + + spec := map[string]interface{}{ + "outdir": "ci/out/wstestServerReports", + "servers": []interface{}{ + map[string]interface{}{ + "agent": "main", + "url": strings.Replace(s.URL, "http", "ws", 1), + }, + }, + "cases": []string{"*"}, + // We skip the UTF-8 handling tests as there isn't any reason to reject invalid UTF-8, just + // more performance overhead. 7.5.1 is the same. + // 12.* and 13.* as we do not support compression. + "exclude-cases": []string{"6.*", "7.5.1", "12.*", "13.*"}, + } + specFile, err := ioutil.TempFile("", "websocketFuzzingClient.json") + if err != nil { + t.Fatalf("failed to create temp file for fuzzingclient.json: %v", err) + } + defer specFile.Close() + + e := json.NewEncoder(specFile) + e.SetIndent("", "\t") + err = e.Encode(spec) + if err != nil { + t.Fatalf("failed to write spec: %v", err) + } + + err = specFile.Close() + if err != nil { + t.Fatalf("failed to close file: %v", err) + } + + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, time.Minute*10) + defer cancel() + + args := []string{"--mode", "fuzzingclient", "--spec", specFile.Name()} + wstest := exec.CommandContext(ctx, "wstest", args...) + out, err := wstest.CombinedOutput() + if err != nil { + t.Fatalf("failed to run wstest: %v\nout:\n%s", err, out) + } + + checkWSTestIndex(t, "./ci/out/wstestServerReports/index.json") +} + +func unusedListenAddr() (string, error) { + l, err := net.Listen("tcp", "localhost:0") + if err != nil { + return "", err + } + l.Close() + return l.Addr().String(), nil +} + +// https://github.com/crossbario/autobahn-python/blob/master/wstest/testee_client_aio.py +func testClientAutobahnPython(t *testing.T) { + t.Parallel() + + if os.Getenv("AUTOBAHN_PYTHON") == "" { + t.Skip("Set $AUTOBAHN_PYTHON to test against the python autobahn test suite") + } + + serverAddr, err := unusedListenAddr() + if err != nil { + t.Fatalf("failed to get unused listen addr for wstest: %v", err) + } + + wsServerURL := "ws://" + serverAddr + + spec := map[string]interface{}{ + "url": wsServerURL, + "outdir": "ci/out/wstestClientReports", + "cases": []string{"*"}, + // See TestAutobahnServer for the reasons why we exclude these. + "exclude-cases": []string{"6.*", "7.5.1", "12.*", "13.*"}, + } + specFile, err := ioutil.TempFile("", "websocketFuzzingServer.json") + if err != nil { + t.Fatalf("failed to create temp file for fuzzingserver.json: %v", err) + } + defer specFile.Close() + + e := json.NewEncoder(specFile) + e.SetIndent("", "\t") + err = e.Encode(spec) + if err != nil { + t.Fatalf("failed to write spec: %v", err) + } + + err = specFile.Close() + if err != nil { + t.Fatalf("failed to close file: %v", err) + } + + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, time.Minute*10) + defer cancel() + + args := []string{"--mode", "fuzzingserver", "--spec", specFile.Name(), + // Disables some server that runs as part of fuzzingserver mode. + // See https://github.com/crossbario/autobahn-testsuite/blob/058db3a36b7c3a1edf68c282307c6b899ca4857f/autobahntestsuite/autobahntestsuite/wstest.py#L124 + "--webport=0", + } + wstest := exec.CommandContext(ctx, "wstest", args...) + err = wstest.Start() + if err != nil { + t.Fatal(err) + } + defer func() { + err := wstest.Process.Kill() + if err != nil { + t.Error(err) + } + }() + + // Let it come up. + time.Sleep(time.Second * 5) + + var cases int + func() { + c, _, err := websocket.Dial(ctx, wsServerURL+"/getCaseCount", nil) + if err != nil { + t.Fatal(err) + } + defer c.Close(websocket.StatusInternalError, "") + + _, r, err := c.Reader(ctx) + if err != nil { + t.Fatal(err) + } + b, err := ioutil.ReadAll(r) + if err != nil { + t.Fatal(err) + } + cases, err = strconv.Atoi(string(b)) + if err != nil { + t.Fatal(err) + } + + c.Close(websocket.StatusNormalClosure, "") + }() + + for i := 1; i <= cases; i++ { + func() { + ctx, cancel := context.WithTimeout(ctx, time.Second*45) + defer cancel() + + c, _, err := websocket.Dial(ctx, fmt.Sprintf(wsServerURL+"/runCase?case=%v&agent=main", i), nil) + if err != nil { + t.Fatal(err) + } + wsecho.Loop(ctx, c) + }() + } + + c, _, err := websocket.Dial(ctx, fmt.Sprintf(wsServerURL+"/updateReports?agent=main"), nil) + if err != nil { + t.Fatal(err) + } + c.Close(websocket.StatusNormalClosure, "") + + checkWSTestIndex(t, "./ci/out/wstestClientReports/index.json") +} + +func checkWSTestIndex(t *testing.T, path string) { + wstestOut, err := ioutil.ReadFile(path) + if err != nil { + t.Fatalf("failed to read index.json: %v", err) + } + + var indexJSON map[string]map[string]struct { + Behavior string `json:"behavior"` + BehaviorClose string `json:"behaviorClose"` + } + err = json.Unmarshal(wstestOut, &indexJSON) + if err != nil { + t.Fatalf("failed to unmarshal index.json: %v", err) + } + + var failed bool + for _, tests := range indexJSON { + for test, result := range tests { + switch result.Behavior { + case "OK", "NON-STRICT", "INFORMATIONAL": + default: + failed = true + t.Errorf("test %v failed", test) + } + switch result.BehaviorClose { + case "OK", "INFORMATIONAL": + default: + failed = true + t.Errorf("bad close behaviour for test %v", test) + } + } + } + + if failed { + path = strings.Replace(path, ".json", ".html", 1) + if os.Getenv("CI") == "" { + t.Errorf("wstest found failure, see %q (output as an artifact in CI)", path) + } + } +} diff --git a/dial.go b/dial.go deleted file mode 100644 index 79232aac..00000000 --- a/dial.go +++ /dev/null @@ -1,200 +0,0 @@ -// +build !js - -package websocket - -import ( - "bufio" - "bytes" - "context" - "encoding/base64" - "fmt" - "io" - "io/ioutil" - "math/rand" - "net/http" - "net/url" - "strings" - "sync" -) - -// DialOptions represents the options available to pass to Dial. -type DialOptions struct { - // HTTPClient is the http client used for the handshake. - // Its Transport must return writable bodies - // for WebSocket handshakes. - // http.Transport does this correctly beginning with Go 1.12. - HTTPClient *http.Client - - // HTTPHeader specifies the HTTP headers included in the handshake request. - HTTPHeader http.Header - - // Subprotocols lists the subprotocols to negotiate with the server. - Subprotocols []string -} - -// Dial performs a WebSocket handshake on the given url with the given options. -// The response is the WebSocket handshake response from the server. -// If an error occurs, the returned response may be non nil. However, you can only -// read the first 1024 bytes of its body. -// -// You never need to close the resp.Body yourself. -// -// This function requires at least Go 1.12 to succeed as it uses a new feature -// in net/http to perform WebSocket handshakes and get a writable body -// from the transport. See https://github.com/golang/go/issues/26937#issuecomment-415855861 -func Dial(ctx context.Context, u string, opts *DialOptions) (*Conn, *http.Response, error) { - c, r, err := dial(ctx, u, opts) - if err != nil { - return nil, r, fmt.Errorf("failed to websocket dial: %w", err) - } - return c, r, nil -} - -func dial(ctx context.Context, u string, opts *DialOptions) (_ *Conn, _ *http.Response, err error) { - if opts == nil { - opts = &DialOptions{} - } - - // Shallow copy to ensure defaults do not affect user passed options. - opts2 := *opts - opts = &opts2 - - if opts.HTTPClient == nil { - opts.HTTPClient = http.DefaultClient - } - if opts.HTTPClient.Timeout > 0 { - return nil, nil, fmt.Errorf("use context for cancellation instead of http.Client.Timeout; see https://github.com/nhooyr/websocket/issues/67") - } - if opts.HTTPHeader == nil { - opts.HTTPHeader = http.Header{} - } - - parsedURL, err := url.Parse(u) - if err != nil { - return nil, nil, fmt.Errorf("failed to parse url: %w", err) - } - - switch parsedURL.Scheme { - case "ws": - parsedURL.Scheme = "http" - case "wss": - parsedURL.Scheme = "https" - default: - return nil, nil, fmt.Errorf("unexpected url scheme: %q", parsedURL.Scheme) - } - - req, _ := http.NewRequest("GET", parsedURL.String(), nil) - req = req.WithContext(ctx) - req.Header = opts.HTTPHeader - req.Header.Set("Connection", "Upgrade") - req.Header.Set("Upgrade", "websocket") - req.Header.Set("Sec-WebSocket-Version", "13") - req.Header.Set("Sec-WebSocket-Key", makeSecWebSocketKey()) - if len(opts.Subprotocols) > 0 { - req.Header.Set("Sec-WebSocket-Protocol", strings.Join(opts.Subprotocols, ",")) - } - - resp, err := opts.HTTPClient.Do(req) - if err != nil { - return nil, nil, fmt.Errorf("failed to send handshake request: %w", err) - } - defer func() { - if err != nil { - // We read a bit of the body for easier debugging. - r := io.LimitReader(resp.Body, 1024) - b, _ := ioutil.ReadAll(r) - resp.Body.Close() - resp.Body = ioutil.NopCloser(bytes.NewReader(b)) - } - }() - - err = verifyServerResponse(req, resp) - if err != nil { - return nil, resp, err - } - - rwc, ok := resp.Body.(io.ReadWriteCloser) - if !ok { - return nil, resp, fmt.Errorf("response body is not a io.ReadWriteCloser: %T", rwc) - } - - c := &Conn{ - subprotocol: resp.Header.Get("Sec-WebSocket-Protocol"), - br: getBufioReader(rwc), - bw: getBufioWriter(rwc), - closer: rwc, - client: true, - } - c.extractBufioWriterBuf(rwc) - c.init() - - return c, resp, nil -} - -func verifyServerResponse(r *http.Request, resp *http.Response) error { - if resp.StatusCode != http.StatusSwitchingProtocols { - return fmt.Errorf("expected handshake response status code %v but got %v", http.StatusSwitchingProtocols, resp.StatusCode) - } - - if !headerValuesContainsToken(resp.Header, "Connection", "Upgrade") { - return fmt.Errorf("websocket protocol violation: Connection header %q does not contain Upgrade", resp.Header.Get("Connection")) - } - - if !headerValuesContainsToken(resp.Header, "Upgrade", "WebSocket") { - return fmt.Errorf("websocket protocol violation: Upgrade header %q does not contain websocket", resp.Header.Get("Upgrade")) - } - - if resp.Header.Get("Sec-WebSocket-Accept") != secWebSocketAccept(r.Header.Get("Sec-WebSocket-Key")) { - return fmt.Errorf("websocket protocol violation: invalid Sec-WebSocket-Accept %q, key %q", - resp.Header.Get("Sec-WebSocket-Accept"), - r.Header.Get("Sec-WebSocket-Key"), - ) - } - - if proto := resp.Header.Get("Sec-WebSocket-Protocol"); proto != "" && !headerValuesContainsToken(r.Header, "Sec-WebSocket-Protocol", proto) { - return fmt.Errorf("websocket protocol violation: unexpected Sec-WebSocket-Protocol from server: %q", proto) - } - - return nil -} - -// The below pools can only be used by the client because http.Hijacker will always -// have a bufio.Reader/Writer for us so it doesn't make sense to use a pool on top. - -var bufioReaderPool = sync.Pool{ - New: func() interface{} { - return bufio.NewReader(nil) - }, -} - -func getBufioReader(r io.Reader) *bufio.Reader { - br := bufioReaderPool.Get().(*bufio.Reader) - br.Reset(r) - return br -} - -func returnBufioReader(br *bufio.Reader) { - bufioReaderPool.Put(br) -} - -var bufioWriterPool = sync.Pool{ - New: func() interface{} { - return bufio.NewWriter(nil) - }, -} - -func getBufioWriter(w io.Writer) *bufio.Writer { - bw := bufioWriterPool.Get().(*bufio.Writer) - bw.Reset(w) - return bw -} - -func returnBufioWriter(bw *bufio.Writer) { - bufioWriterPool.Put(bw) -} - -func makeSecWebSocketKey() string { - b := make([]byte, 16) - rand.Read(b) - return base64.StdEncoding.EncodeToString(b) -} diff --git a/dial_test.go b/dial_test.go deleted file mode 100644 index 083b9bf3..00000000 --- a/dial_test.go +++ /dev/null @@ -1,146 +0,0 @@ -// +build !js - -package websocket - -import ( - "context" - "net/http" - "net/http/httptest" - "testing" - "time" -) - -func TestBadDials(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - url string - opts *DialOptions - }{ - { - name: "badURL", - url: "://noscheme", - }, - { - name: "badURLScheme", - url: "ftp://nhooyr.io", - }, - { - name: "badHTTPClient", - url: "ws://nhooyr.io", - opts: &DialOptions{ - HTTPClient: &http.Client{ - Timeout: time.Minute, - }, - }, - }, - { - name: "badTLS", - url: "wss://totallyfake.nhooyr.io", - }, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - defer cancel() - - _, _, err := Dial(ctx, tc.url, tc.opts) - if err == nil { - t.Fatalf("expected non nil error: %+v", err) - } - }) - } -} - -func Test_verifyServerHandshake(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - response func(w http.ResponseWriter) - success bool - }{ - { - name: "badStatus", - response: func(w http.ResponseWriter) { - w.WriteHeader(http.StatusOK) - }, - success: false, - }, - { - name: "badConnection", - response: func(w http.ResponseWriter) { - w.Header().Set("Connection", "???") - w.WriteHeader(http.StatusSwitchingProtocols) - }, - success: false, - }, - { - name: "badUpgrade", - response: func(w http.ResponseWriter) { - w.Header().Set("Connection", "Upgrade") - w.Header().Set("Upgrade", "???") - w.WriteHeader(http.StatusSwitchingProtocols) - }, - success: false, - }, - { - name: "badSecWebSocketAccept", - response: func(w http.ResponseWriter) { - w.Header().Set("Connection", "Upgrade") - w.Header().Set("Upgrade", "websocket") - w.Header().Set("Sec-WebSocket-Accept", "xd") - w.WriteHeader(http.StatusSwitchingProtocols) - }, - success: false, - }, - { - name: "badSecWebSocketProtocol", - response: func(w http.ResponseWriter) { - w.Header().Set("Connection", "Upgrade") - w.Header().Set("Upgrade", "websocket") - w.Header().Set("Sec-WebSocket-Protocol", "xd") - w.WriteHeader(http.StatusSwitchingProtocols) - }, - success: false, - }, - { - name: "success", - response: func(w http.ResponseWriter) { - w.Header().Set("Connection", "Upgrade") - w.Header().Set("Upgrade", "websocket") - w.WriteHeader(http.StatusSwitchingProtocols) - }, - success: true, - }, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - w := httptest.NewRecorder() - tc.response(w) - resp := w.Result() - - r := httptest.NewRequest("GET", "/", nil) - key := makeSecWebSocketKey() - r.Header.Set("Sec-WebSocket-Key", key) - - if resp.Header.Get("Sec-WebSocket-Accept") == "" { - resp.Header.Set("Sec-WebSocket-Accept", secWebSocketAccept(key)) - } - - err := verifyServerResponse(r, resp) - if (err == nil) != tc.success { - t.Fatalf("unexpected error: %+v", err) - } - }) - } -} diff --git a/doc.go b/doc.go index 4c07d37a..da6f3222 100644 --- a/doc.go +++ b/doc.go @@ -22,13 +22,12 @@ // // The client side fully supports compiling to WASM. // It wraps the WebSocket browser API. +// // See https://developer.mozilla.org/en-US/docs/Web/API/WebSocket // // Thus the unsupported features when compiling to WASM are: -// - Accept API -// - Reader/Writer API -// - SetReadLimit -// - Ping +// - Accept and AcceptOptions +// - Conn's Reader, Writer, SetReadLimit, Ping methods // - HTTPClient and HTTPHeader dial options // // The *http.Response returned by Dial will always either be nil or &http.Response{} as diff --git a/frame.go b/frame.go new file mode 100644 index 00000000..10cb9e38 --- /dev/null +++ b/frame.go @@ -0,0 +1,423 @@ +package websocket + +import ( + "encoding/binary" + "fmt" + "io" + "math" +) + +//go:generate go run golang.org/x/tools/cmd/stringer -type=opcode,MessageType,StatusCode -output=frame_string.go + +// opcode represents a WebSocket Opcode. +type opcode int + +// opcode constants. +const ( + opContinuation opcode = iota + opText + opBinary + // 3 - 7 are reserved for further non-control frames. + _ + _ + _ + _ + _ + opClose + opPing + opPong + // 11-16 are reserved for further control frames. +) + +func (o opcode) controlOp() bool { + switch o { + case opClose, opPing, opPong: + return true + } + return false +} + +// MessageType represents the type of a WebSocket message. +// See https://tools.ietf.org/html/rfc6455#section-5.6 +type MessageType int + +// MessageType constants. +const ( + // MessageText is for UTF-8 encoded text messages like JSON. + MessageText MessageType = iota + 1 + // MessageBinary is for binary messages like Protobufs. + MessageBinary +) + +// First byte contains fin, rsv1, rsv2, rsv3. +// Second byte contains mask flag and payload len. +// Next 8 bytes are the maximum extended payload length. +// Last 4 bytes are the mask key. +// https://tools.ietf.org/html/rfc6455#section-5.2 +const maxHeaderSize = 1 + 1 + 8 + 4 + +// header represents a WebSocket frame header. +// See https://tools.ietf.org/html/rfc6455#section-5.2 +type header struct { + fin bool + rsv1 bool + rsv2 bool + rsv3 bool + opcode opcode + + payloadLength int64 + + masked bool + maskKey [4]byte +} + +func makeWriteHeaderBuf() []byte { + return make([]byte, maxHeaderSize) +} + +// bytes returns the bytes of the header. +// See https://tools.ietf.org/html/rfc6455#section-5.2 +func writeHeader(b []byte, h header) []byte { + if b == nil { + b = makeWriteHeaderBuf() + } + + b = b[:2] + b[0] = 0 + + if h.fin { + b[0] |= 1 << 7 + } + if h.rsv1 { + b[0] |= 1 << 6 + } + if h.rsv2 { + b[0] |= 1 << 5 + } + if h.rsv3 { + b[0] |= 1 << 4 + } + + b[0] |= byte(h.opcode) + + switch { + case h.payloadLength < 0: + panic(fmt.Sprintf("websocket: invalid header: negative length: %v", h.payloadLength)) + case h.payloadLength <= 125: + b[1] = byte(h.payloadLength) + case h.payloadLength <= math.MaxUint16: + b[1] = 126 + b = b[:len(b)+2] + binary.BigEndian.PutUint16(b[len(b)-2:], uint16(h.payloadLength)) + default: + b[1] = 127 + b = b[:len(b)+8] + binary.BigEndian.PutUint64(b[len(b)-8:], uint64(h.payloadLength)) + } + + if h.masked { + b[1] |= 1 << 7 + b = b[:len(b)+4] + copy(b[len(b)-4:], h.maskKey[:]) + } + + return b +} + +func makeReadHeaderBuf() []byte { + return make([]byte, maxHeaderSize-2) +} + +// readHeader reads a header from the reader. +// See https://tools.ietf.org/html/rfc6455#section-5.2 +func readHeader(b []byte, r io.Reader) (header, error) { + if b == nil { + b = makeReadHeaderBuf() + } + + // We read the first two bytes first so that we know + // exactly how long the header is. + b = b[:2] + _, err := io.ReadFull(r, b) + if err != nil { + return header{}, err + } + + var h header + h.fin = b[0]&(1<<7) != 0 + h.rsv1 = b[0]&(1<<6) != 0 + h.rsv2 = b[0]&(1<<5) != 0 + h.rsv3 = b[0]&(1<<4) != 0 + + h.opcode = opcode(b[0] & 0xf) + + var extra int + + h.masked = b[1]&(1<<7) != 0 + if h.masked { + extra += 4 + } + + payloadLength := b[1] &^ (1 << 7) + switch { + case payloadLength < 126: + h.payloadLength = int64(payloadLength) + case payloadLength == 126: + extra += 2 + case payloadLength == 127: + extra += 8 + } + + if extra == 0 { + return h, nil + } + + b = b[:extra] + _, err = io.ReadFull(r, b) + if err != nil { + return header{}, err + } + + switch { + case payloadLength == 126: + h.payloadLength = int64(binary.BigEndian.Uint16(b)) + b = b[2:] + case payloadLength == 127: + h.payloadLength = int64(binary.BigEndian.Uint64(b)) + if h.payloadLength < 0 { + return header{}, fmt.Errorf("header with negative payload length: %v", h.payloadLength) + } + b = b[8:] + } + + if h.masked { + copy(h.maskKey[:], b) + } + + return h, nil +} + +// StatusCode represents a WebSocket status code. +// https://tools.ietf.org/html/rfc6455#section-7.4 +type StatusCode int + +// These codes were retrieved from: +// https://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number +const ( + StatusNormalClosure StatusCode = 1000 + iota + StatusGoingAway + StatusProtocolError + StatusUnsupportedData + + _ // 1004 is reserved. + + StatusNoStatusRcvd + + // This StatusCode is only exported for use with WASM. + // In non WASM Go, the returned error will indicate whether the connection was closed or not or what happened. + StatusAbnormalClosure + + StatusInvalidFramePayloadData + StatusPolicyViolation + StatusMessageTooBig + StatusMandatoryExtension + StatusInternalError + StatusServiceRestart + StatusTryAgainLater + StatusBadGateway + + // This StatusCode is only exported for use with WASM. + // In non WASM Go, the returned error will indicate whether there was a TLS handshake failure. + StatusTLSHandshake +) + +// CloseError represents a WebSocket close frame. +// It is returned by Conn's methods when a WebSocket close frame is received from +// the peer. +// You will need to use the https://golang.org/pkg/errors/#As function, new in Go 1.13, +// to check for this error. See the CloseError example. +type CloseError struct { + Code StatusCode + Reason string +} + +func (ce CloseError) Error() string { + return fmt.Sprintf("status = %v and reason = %q", ce.Code, ce.Reason) +} + +func parseClosePayload(p []byte) (CloseError, error) { + if len(p) == 0 { + return CloseError{ + Code: StatusNoStatusRcvd, + }, nil + } + + if len(p) < 2 { + return CloseError{}, fmt.Errorf("close payload %q too small, cannot even contain the 2 byte status code", p) + } + + ce := CloseError{ + Code: StatusCode(binary.BigEndian.Uint16(p)), + Reason: string(p[2:]), + } + + if !validWireCloseCode(ce.Code) { + return CloseError{}, fmt.Errorf("invalid status code %v", ce.Code) + } + + return ce, nil +} + +// See http://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number +// and https://tools.ietf.org/html/rfc6455#section-7.4.1 +func validWireCloseCode(code StatusCode) bool { + switch code { + case 1004, StatusNoStatusRcvd, StatusAbnormalClosure, StatusTLSHandshake: + return false + } + + if code >= StatusNormalClosure && code <= StatusBadGateway { + return true + } + if code >= 3000 && code <= 4999 { + return true + } + + return false +} + +const maxControlFramePayload = 125 + +func (ce CloseError) bytes() ([]byte, error) { + if len(ce.Reason) > maxControlFramePayload-2 { + return nil, fmt.Errorf("reason string max is %v but got %q with length %v", maxControlFramePayload-2, ce.Reason, len(ce.Reason)) + } + if !validWireCloseCode(ce.Code) { + return nil, fmt.Errorf("status code %v cannot be set", ce.Code) + } + + buf := make([]byte, 2+len(ce.Reason)) + binary.BigEndian.PutUint16(buf, uint16(ce.Code)) + copy(buf[2:], ce.Reason) + return buf, nil +} + +// xor applies the WebSocket masking algorithm to p +// with the given key where the first 3 bits of pos +// are the starting position in the key. +// See https://tools.ietf.org/html/rfc6455#section-5.3 +// +// The returned value is the position of the next byte +// to be used for masking in the key. This is so that +// unmasking can be performed without the entire frame. +func fastXOR(key [4]byte, keyPos int, b []byte) int { + // If the payload is greater than or equal to 16 bytes, then it's worth + // masking 8 bytes at a time. + // Optimization from https://github.com/golang/go/issues/31586#issuecomment-485530859 + if len(b) >= 16 { + // We first create a key that is 8 bytes long + // and is aligned on the position correctly. + var alignedKey [8]byte + for i := range alignedKey { + alignedKey[i] = key[(i+keyPos)&3] + } + k := binary.LittleEndian.Uint64(alignedKey[:]) + + // At some point in the future we can clean these unrolled loops up. + // See https://github.com/golang/go/issues/31586#issuecomment-487436401 + + // Then we xor until b is less than 128 bytes. + for len(b) >= 128 { + v := binary.LittleEndian.Uint64(b) + binary.LittleEndian.PutUint64(b, v^k) + v = binary.LittleEndian.Uint64(b[8:]) + binary.LittleEndian.PutUint64(b[8:], v^k) + v = binary.LittleEndian.Uint64(b[16:]) + binary.LittleEndian.PutUint64(b[16:], v^k) + v = binary.LittleEndian.Uint64(b[24:]) + binary.LittleEndian.PutUint64(b[24:], v^k) + v = binary.LittleEndian.Uint64(b[32:]) + binary.LittleEndian.PutUint64(b[32:], v^k) + v = binary.LittleEndian.Uint64(b[40:]) + binary.LittleEndian.PutUint64(b[40:], v^k) + v = binary.LittleEndian.Uint64(b[48:]) + binary.LittleEndian.PutUint64(b[48:], v^k) + v = binary.LittleEndian.Uint64(b[56:]) + binary.LittleEndian.PutUint64(b[56:], v^k) + v = binary.LittleEndian.Uint64(b[64:]) + binary.LittleEndian.PutUint64(b[64:], v^k) + v = binary.LittleEndian.Uint64(b[72:]) + binary.LittleEndian.PutUint64(b[72:], v^k) + v = binary.LittleEndian.Uint64(b[80:]) + binary.LittleEndian.PutUint64(b[80:], v^k) + v = binary.LittleEndian.Uint64(b[88:]) + binary.LittleEndian.PutUint64(b[88:], v^k) + v = binary.LittleEndian.Uint64(b[96:]) + binary.LittleEndian.PutUint64(b[96:], v^k) + v = binary.LittleEndian.Uint64(b[104:]) + binary.LittleEndian.PutUint64(b[104:], v^k) + v = binary.LittleEndian.Uint64(b[112:]) + binary.LittleEndian.PutUint64(b[112:], v^k) + v = binary.LittleEndian.Uint64(b[120:]) + binary.LittleEndian.PutUint64(b[120:], v^k) + b = b[128:] + } + + // Then we xor until b is less than 64 bytes. + for len(b) >= 64 { + v := binary.LittleEndian.Uint64(b) + binary.LittleEndian.PutUint64(b, v^k) + v = binary.LittleEndian.Uint64(b[8:]) + binary.LittleEndian.PutUint64(b[8:], v^k) + v = binary.LittleEndian.Uint64(b[16:]) + binary.LittleEndian.PutUint64(b[16:], v^k) + v = binary.LittleEndian.Uint64(b[24:]) + binary.LittleEndian.PutUint64(b[24:], v^k) + v = binary.LittleEndian.Uint64(b[32:]) + binary.LittleEndian.PutUint64(b[32:], v^k) + v = binary.LittleEndian.Uint64(b[40:]) + binary.LittleEndian.PutUint64(b[40:], v^k) + v = binary.LittleEndian.Uint64(b[48:]) + binary.LittleEndian.PutUint64(b[48:], v^k) + v = binary.LittleEndian.Uint64(b[56:]) + binary.LittleEndian.PutUint64(b[56:], v^k) + b = b[64:] + } + + // Then we xor until b is less than 32 bytes. + for len(b) >= 32 { + v := binary.LittleEndian.Uint64(b) + binary.LittleEndian.PutUint64(b, v^k) + v = binary.LittleEndian.Uint64(b[8:]) + binary.LittleEndian.PutUint64(b[8:], v^k) + v = binary.LittleEndian.Uint64(b[16:]) + binary.LittleEndian.PutUint64(b[16:], v^k) + v = binary.LittleEndian.Uint64(b[24:]) + binary.LittleEndian.PutUint64(b[24:], v^k) + b = b[32:] + } + + // Then we xor until b is less than 16 bytes. + for len(b) >= 16 { + v := binary.LittleEndian.Uint64(b) + binary.LittleEndian.PutUint64(b, v^k) + v = binary.LittleEndian.Uint64(b[8:]) + binary.LittleEndian.PutUint64(b[8:], v^k) + b = b[16:] + } + + // Then we xor until b is less than 8 bytes. + for len(b) >= 8 { + v := binary.LittleEndian.Uint64(b) + binary.LittleEndian.PutUint64(b, v^k) + b = b[8:] + } + } + + // xor remaining bytes. + for i := range b { + b[i] ^= key[keyPos&3] + keyPos++ + } + return keyPos & 3 +} diff --git a/statuscode_string.go b/frame_string.go similarity index 51% rename from statuscode_string.go rename to frame_string.go index fc8cea0d..6b32672a 100644 --- a/statuscode_string.go +++ b/frame_string.go @@ -1,9 +1,61 @@ -// Code generated by "stringer -type=StatusCode"; DO NOT EDIT. +// Code generated by "stringer -type=opcode,MessageType,StatusCode -output=frame_string.go"; DO NOT EDIT. package websocket import "strconv" +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[opContinuation-0] + _ = x[opText-1] + _ = x[opBinary-2] + _ = x[opClose-8] + _ = x[opPing-9] + _ = x[opPong-10] +} + +const ( + _opcode_name_0 = "opContinuationopTextopBinary" + _opcode_name_1 = "opCloseopPingopPong" +) + +var ( + _opcode_index_0 = [...]uint8{0, 14, 20, 28} + _opcode_index_1 = [...]uint8{0, 7, 13, 19} +) + +func (i opcode) String() string { + switch { + case 0 <= i && i <= 2: + return _opcode_name_0[_opcode_index_0[i]:_opcode_index_0[i+1]] + case 8 <= i && i <= 10: + i -= 8 + return _opcode_name_1[_opcode_index_1[i]:_opcode_index_1[i+1]] + default: + return "opcode(" + strconv.FormatInt(int64(i), 10) + ")" + } +} +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[MessageText-1] + _ = x[MessageBinary-2] +} + +const _MessageType_name = "MessageTextMessageBinary" + +var _MessageType_index = [...]uint8{0, 11, 24} + +func (i MessageType) String() string { + i -= 1 + if i < 0 || i >= MessageType(len(_MessageType_index)-1) { + return "MessageType(" + strconv.FormatInt(int64(i+1), 10) + ")" + } + return _MessageType_name[_MessageType_index[i]:_MessageType_index[i+1]] +} func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. diff --git a/frame_test.go b/frame_test.go new file mode 100644 index 00000000..1a2054c1 --- /dev/null +++ b/frame_test.go @@ -0,0 +1,373 @@ +// +build !js + +package websocket + +import ( + "bytes" + "io" + "math" + "math/rand" + "strconv" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func randBool() bool { + return rand.Intn(1) == 0 +} + +func TestHeader(t *testing.T) { + t.Parallel() + + t.Run("eof", func(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + bytes []byte + }{ + { + "start", + []byte{0xff}, + }, + { + "middle", + []byte{0xff, 0xff, 0xff}, + }, + } + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + b := bytes.NewBuffer(tc.bytes) + _, err := readHeader(nil, b) + if io.ErrUnexpectedEOF != err { + t.Fatalf("expected %v but got: %v", io.ErrUnexpectedEOF, err) + } + }) + } + }) + + t.Run("writeNegativeLength", func(t *testing.T) { + t.Parallel() + + defer func() { + r := recover() + if r == nil { + t.Fatal("failed to induce panic in writeHeader with negative payload length") + } + }() + + writeHeader(nil, header{ + payloadLength: -1, + }) + }) + + t.Run("readNegativeLength", func(t *testing.T) { + t.Parallel() + + b := writeHeader(nil, header{ + payloadLength: 1<<16 + 1, + }) + + // Make length negative + b[2] |= 1 << 7 + + r := bytes.NewReader(b) + _, err := readHeader(nil, r) + if err == nil { + t.Fatalf("unexpected error value: %+v", err) + } + }) + + t.Run("lengths", func(t *testing.T) { + t.Parallel() + + lengths := []int{ + 124, + 125, + 126, + 4096, + 16384, + 65535, + 65536, + 65537, + 131072, + } + + for _, n := range lengths { + n := n + t.Run(strconv.Itoa(n), func(t *testing.T) { + t.Parallel() + + testHeader(t, header{ + payloadLength: int64(n), + }) + }) + } + }) + + t.Run("fuzz", func(t *testing.T) { + t.Parallel() + + for i := 0; i < 10000; i++ { + h := header{ + fin: randBool(), + rsv1: randBool(), + rsv2: randBool(), + rsv3: randBool(), + opcode: opcode(rand.Intn(1 << 4)), + + masked: randBool(), + payloadLength: rand.Int63(), + } + + if h.masked { + rand.Read(h.maskKey[:]) + } + + testHeader(t, h) + } + }) +} + +func testHeader(t *testing.T, h header) { + b := writeHeader(nil, h) + r := bytes.NewReader(b) + h2, err := readHeader(nil, r) + if err != nil { + t.Logf("header: %#v", h) + t.Logf("bytes: %b", b) + t.Fatalf("failed to read header: %v", err) + } + + if !cmp.Equal(h, h2, cmp.AllowUnexported(header{})) { + t.Logf("header: %#v", h) + t.Logf("bytes: %b", b) + t.Fatalf("parsed and read header differ: %v", cmp.Diff(h, h2, cmp.AllowUnexported(header{}))) + } +} + +func TestCloseError(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + ce CloseError + success bool + }{ + { + name: "normal", + ce: CloseError{ + Code: StatusNormalClosure, + Reason: strings.Repeat("x", maxControlFramePayload-2), + }, + success: true, + }, + { + name: "bigReason", + ce: CloseError{ + Code: StatusNormalClosure, + Reason: strings.Repeat("x", maxControlFramePayload-1), + }, + success: false, + }, + { + name: "bigCode", + ce: CloseError{ + Code: math.MaxUint16, + Reason: strings.Repeat("x", maxControlFramePayload-2), + }, + success: false, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + _, err := tc.ce.bytes() + if (err == nil) != tc.success { + t.Fatalf("unexpected error value: %+v", err) + } + }) + } +} + +func Test_parseClosePayload(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + p []byte + success bool + ce CloseError + }{ + { + name: "normal", + p: append([]byte{0x3, 0xE8}, []byte("hello")...), + success: true, + ce: CloseError{ + Code: StatusNormalClosure, + Reason: "hello", + }, + }, + { + name: "nothing", + success: true, + ce: CloseError{ + Code: StatusNoStatusRcvd, + }, + }, + { + name: "oneByte", + p: []byte{0}, + success: false, + }, + { + name: "badStatusCode", + p: []byte{0x17, 0x70}, + success: false, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ce, err := parseClosePayload(tc.p) + if (err == nil) != tc.success { + t.Fatalf("unexpected expected error value: %+v", err) + } + + if tc.success && tc.ce != ce { + t.Fatalf("unexpected close error: %v", cmp.Diff(tc.ce, ce)) + } + }) + } +} + +func Test_validWireCloseCode(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + code StatusCode + valid bool + }{ + { + name: "normal", + code: StatusNormalClosure, + valid: true, + }, + { + name: "noStatus", + code: StatusNoStatusRcvd, + valid: false, + }, + { + name: "3000", + code: 3000, + valid: true, + }, + { + name: "4999", + code: 4999, + valid: true, + }, + { + name: "unknown", + code: 5000, + valid: false, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + if valid := validWireCloseCode(tc.code); tc.valid != valid { + t.Fatalf("expected %v for %v but got %v", tc.valid, tc.code, valid) + } + }) + } +} + +func Test_xor(t *testing.T) { + t.Parallel() + + key := [4]byte{0xa, 0xb, 0xc, 0xff} + p := []byte{0xa, 0xb, 0xc, 0xf2, 0xc} + pos := 0 + pos = fastXOR(key, pos, p) + + if exp := []byte{0, 0, 0, 0x0d, 0x6}; !cmp.Equal(exp, p) { + t.Fatalf("unexpected mask: %v", cmp.Diff(exp, p)) + } + + if exp := 1; !cmp.Equal(exp, pos) { + t.Fatalf("unexpected mask pos: %v", cmp.Diff(exp, pos)) + } +} + +func basixXOR(maskKey [4]byte, pos int, b []byte) int { + for i := range b { + b[i] ^= maskKey[pos&3] + pos++ + } + return pos & 3 +} + +func BenchmarkXOR(b *testing.B) { + sizes := []int{ + 2, + 16, + 32, + 512, + 4096, + 16384, + } + + fns := []struct { + name string + fn func([4]byte, int, []byte) int + }{ + { + "basic", + basixXOR, + }, + { + "fast", + fastXOR, + }, + } + + var maskKey [4]byte + _, err := rand.Read(maskKey[:]) + if err != nil { + b.Fatalf("failed to populate mask key: %v", err) + } + + for _, size := range sizes { + data := make([]byte, size) + + b.Run(strconv.Itoa(size), func(b *testing.B) { + for _, fn := range fns { + b.Run(fn.name, func(b *testing.B) { + b.ReportAllocs() + b.SetBytes(int64(size)) + + for i := 0; i < b.N; i++ { + fn.fn(maskKey, 0, data) + } + }) + } + }) + } +} diff --git a/accept.go b/handshake.go similarity index 54% rename from accept.go rename to handshake.go index e68a049b..c55dd463 100644 --- a/accept.go +++ b/handshake.go @@ -3,16 +3,21 @@ package websocket import ( + "bufio" "bytes" + "context" "crypto/sha1" "encoding/base64" "errors" "fmt" "io" + "io/ioutil" + "math/rand" "net/http" "net/textproto" "net/url" "strings" + "sync" ) // AcceptOptions represents the options available to pass to Accept. @@ -221,3 +226,185 @@ func authenticateOrigin(r *http.Request) error { } return fmt.Errorf("request Origin %q is not authorized for Host %q", origin, r.Host) } + +// DialOptions represents the options available to pass to Dial. +type DialOptions struct { + // HTTPClient is the http client used for the handshake. + // Its Transport must return writable bodies + // for WebSocket handshakes. + // http.Transport does this correctly beginning with Go 1.12. + HTTPClient *http.Client + + // HTTPHeader specifies the HTTP headers included in the handshake request. + HTTPHeader http.Header + + // Subprotocols lists the subprotocols to negotiate with the server. + Subprotocols []string +} + +// Dial performs a WebSocket handshake on the given url with the given options. +// The response is the WebSocket handshake response from the server. +// If an error occurs, the returned response may be non nil. However, you can only +// read the first 1024 bytes of its body. +// +// You never need to close the resp.Body yourself. +// +// This function requires at least Go 1.12 to succeed as it uses a new feature +// in net/http to perform WebSocket handshakes and get a writable body +// from the transport. See https://github.com/golang/go/issues/26937#issuecomment-415855861 +func Dial(ctx context.Context, u string, opts *DialOptions) (*Conn, *http.Response, error) { + c, r, err := dial(ctx, u, opts) + if err != nil { + return nil, r, fmt.Errorf("failed to websocket dial: %w", err) + } + return c, r, nil +} + +func dial(ctx context.Context, u string, opts *DialOptions) (_ *Conn, _ *http.Response, err error) { + if opts == nil { + opts = &DialOptions{} + } + + // Shallow copy to ensure defaults do not affect user passed options. + opts2 := *opts + opts = &opts2 + + if opts.HTTPClient == nil { + opts.HTTPClient = http.DefaultClient + } + if opts.HTTPClient.Timeout > 0 { + return nil, nil, fmt.Errorf("use context for cancellation instead of http.Client.Timeout; see https://github.com/nhooyr/websocket/issues/67") + } + if opts.HTTPHeader == nil { + opts.HTTPHeader = http.Header{} + } + + parsedURL, err := url.Parse(u) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse url: %w", err) + } + + switch parsedURL.Scheme { + case "ws": + parsedURL.Scheme = "http" + case "wss": + parsedURL.Scheme = "https" + default: + return nil, nil, fmt.Errorf("unexpected url scheme: %q", parsedURL.Scheme) + } + + req, _ := http.NewRequest("GET", parsedURL.String(), nil) + req = req.WithContext(ctx) + req.Header = opts.HTTPHeader + req.Header.Set("Connection", "Upgrade") + req.Header.Set("Upgrade", "websocket") + req.Header.Set("Sec-WebSocket-Version", "13") + req.Header.Set("Sec-WebSocket-Key", makeSecWebSocketKey()) + if len(opts.Subprotocols) > 0 { + req.Header.Set("Sec-WebSocket-Protocol", strings.Join(opts.Subprotocols, ",")) + } + + resp, err := opts.HTTPClient.Do(req) + if err != nil { + return nil, nil, fmt.Errorf("failed to send handshake request: %w", err) + } + defer func() { + if err != nil { + // We read a bit of the body for easier debugging. + r := io.LimitReader(resp.Body, 1024) + b, _ := ioutil.ReadAll(r) + resp.Body.Close() + resp.Body = ioutil.NopCloser(bytes.NewReader(b)) + } + }() + + err = verifyServerResponse(req, resp) + if err != nil { + return nil, resp, err + } + + rwc, ok := resp.Body.(io.ReadWriteCloser) + if !ok { + return nil, resp, fmt.Errorf("response body is not a io.ReadWriteCloser: %T", rwc) + } + + c := &Conn{ + subprotocol: resp.Header.Get("Sec-WebSocket-Protocol"), + br: getBufioReader(rwc), + bw: getBufioWriter(rwc), + closer: rwc, + client: true, + } + c.extractBufioWriterBuf(rwc) + c.init() + + return c, resp, nil +} + +func verifyServerResponse(r *http.Request, resp *http.Response) error { + if resp.StatusCode != http.StatusSwitchingProtocols { + return fmt.Errorf("expected handshake response status code %v but got %v", http.StatusSwitchingProtocols, resp.StatusCode) + } + + if !headerValuesContainsToken(resp.Header, "Connection", "Upgrade") { + return fmt.Errorf("websocket protocol violation: Connection header %q does not contain Upgrade", resp.Header.Get("Connection")) + } + + if !headerValuesContainsToken(resp.Header, "Upgrade", "WebSocket") { + return fmt.Errorf("websocket protocol violation: Upgrade header %q does not contain websocket", resp.Header.Get("Upgrade")) + } + + if resp.Header.Get("Sec-WebSocket-Accept") != secWebSocketAccept(r.Header.Get("Sec-WebSocket-Key")) { + return fmt.Errorf("websocket protocol violation: invalid Sec-WebSocket-Accept %q, key %q", + resp.Header.Get("Sec-WebSocket-Accept"), + r.Header.Get("Sec-WebSocket-Key"), + ) + } + + if proto := resp.Header.Get("Sec-WebSocket-Protocol"); proto != "" && !headerValuesContainsToken(r.Header, "Sec-WebSocket-Protocol", proto) { + return fmt.Errorf("websocket protocol violation: unexpected Sec-WebSocket-Protocol from server: %q", proto) + } + + return nil +} + +// The below pools can only be used by the client because http.Hijacker will always +// have a bufio.Reader/Writer for us so it doesn't make sense to use a pool on top. + +var bufioReaderPool = sync.Pool{ + New: func() interface{} { + return bufio.NewReader(nil) + }, +} + +func getBufioReader(r io.Reader) *bufio.Reader { + br := bufioReaderPool.Get().(*bufio.Reader) + br.Reset(r) + return br +} + +func returnBufioReader(br *bufio.Reader) { + bufioReaderPool.Put(br) +} + +var bufioWriterPool = sync.Pool{ + New: func() interface{} { + return bufio.NewWriter(nil) + }, +} + +func getBufioWriter(w io.Writer) *bufio.Writer { + bw := bufioWriterPool.Get().(*bufio.Writer) + bw.Reset(w) + return bw +} + +func returnBufioWriter(bw *bufio.Writer) { + bufioWriterPool.Put(bw) +} + +func makeSecWebSocketKey() string { + b := make([]byte, 16) + rand.Read(b) + return base64.StdEncoding.EncodeToString(b) +} diff --git a/accept_test.go b/handshake_test.go similarity index 63% rename from accept_test.go rename to handshake_test.go index 44a956a8..a3d98163 100644 --- a/accept_test.go +++ b/handshake_test.go @@ -3,9 +3,12 @@ package websocket import ( + "context" + "net/http" "net/http/httptest" "strings" "testing" + "time" ) func TestAccept(t *testing.T) { @@ -243,3 +246,138 @@ func Test_authenticateOrigin(t *testing.T) { }) } } + +func TestBadDials(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + url string + opts *DialOptions + }{ + { + name: "badURL", + url: "://noscheme", + }, + { + name: "badURLScheme", + url: "ftp://nhooyr.io", + }, + { + name: "badHTTPClient", + url: "ws://nhooyr.io", + opts: &DialOptions{ + HTTPClient: &http.Client{ + Timeout: time.Minute, + }, + }, + }, + { + name: "badTLS", + url: "wss://totallyfake.nhooyr.io", + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + _, _, err := Dial(ctx, tc.url, tc.opts) + if err == nil { + t.Fatalf("expected non nil error: %+v", err) + } + }) + } +} + +func Test_verifyServerHandshake(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + response func(w http.ResponseWriter) + success bool + }{ + { + name: "badStatus", + response: func(w http.ResponseWriter) { + w.WriteHeader(http.StatusOK) + }, + success: false, + }, + { + name: "badConnection", + response: func(w http.ResponseWriter) { + w.Header().Set("Connection", "???") + w.WriteHeader(http.StatusSwitchingProtocols) + }, + success: false, + }, + { + name: "badUpgrade", + response: func(w http.ResponseWriter) { + w.Header().Set("Connection", "Upgrade") + w.Header().Set("Upgrade", "???") + w.WriteHeader(http.StatusSwitchingProtocols) + }, + success: false, + }, + { + name: "badSecWebSocketAccept", + response: func(w http.ResponseWriter) { + w.Header().Set("Connection", "Upgrade") + w.Header().Set("Upgrade", "websocket") + w.Header().Set("Sec-WebSocket-Accept", "xd") + w.WriteHeader(http.StatusSwitchingProtocols) + }, + success: false, + }, + { + name: "badSecWebSocketProtocol", + response: func(w http.ResponseWriter) { + w.Header().Set("Connection", "Upgrade") + w.Header().Set("Upgrade", "websocket") + w.Header().Set("Sec-WebSocket-Protocol", "xd") + w.WriteHeader(http.StatusSwitchingProtocols) + }, + success: false, + }, + { + name: "success", + response: func(w http.ResponseWriter) { + w.Header().Set("Connection", "Upgrade") + w.Header().Set("Upgrade", "websocket") + w.WriteHeader(http.StatusSwitchingProtocols) + }, + success: true, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + w := httptest.NewRecorder() + tc.response(w) + resp := w.Result() + + r := httptest.NewRequest("GET", "/", nil) + key := makeSecWebSocketKey() + r.Header.Set("Sec-WebSocket-Key", key) + + if resp.Header.Get("Sec-WebSocket-Accept") == "" { + resp.Header.Set("Sec-WebSocket-Accept", secWebSocketAccept(key)) + } + + err := verifyServerResponse(r, resp) + if (err == nil) != tc.success { + t.Fatalf("unexpected error: %+v", err) + } + }) + } +} diff --git a/header.go b/header.go deleted file mode 100644 index 613b1d15..00000000 --- a/header.go +++ /dev/null @@ -1,158 +0,0 @@ -// +build !js - -package websocket - -import ( - "encoding/binary" - "fmt" - "io" - "math" -) - -// First byte contains fin, rsv1, rsv2, rsv3. -// Second byte contains mask flag and payload len. -// Next 8 bytes are the maximum extended payload length. -// Last 4 bytes are the mask key. -// https://tools.ietf.org/html/rfc6455#section-5.2 -const maxHeaderSize = 1 + 1 + 8 + 4 - -// header represents a WebSocket frame header. -// See https://tools.ietf.org/html/rfc6455#section-5.2 -type header struct { - fin bool - rsv1 bool - rsv2 bool - rsv3 bool - opcode opcode - - payloadLength int64 - - masked bool - maskKey [4]byte -} - -func makeWriteHeaderBuf() []byte { - return make([]byte, maxHeaderSize) -} - -// bytes returns the bytes of the header. -// See https://tools.ietf.org/html/rfc6455#section-5.2 -func writeHeader(b []byte, h header) []byte { - if b == nil { - b = makeWriteHeaderBuf() - } - - b = b[:2] - b[0] = 0 - - if h.fin { - b[0] |= 1 << 7 - } - if h.rsv1 { - b[0] |= 1 << 6 - } - if h.rsv2 { - b[0] |= 1 << 5 - } - if h.rsv3 { - b[0] |= 1 << 4 - } - - b[0] |= byte(h.opcode) - - switch { - case h.payloadLength < 0: - panic(fmt.Sprintf("websocket: invalid header: negative length: %v", h.payloadLength)) - case h.payloadLength <= 125: - b[1] = byte(h.payloadLength) - case h.payloadLength <= math.MaxUint16: - b[1] = 126 - b = b[:len(b)+2] - binary.BigEndian.PutUint16(b[len(b)-2:], uint16(h.payloadLength)) - default: - b[1] = 127 - b = b[:len(b)+8] - binary.BigEndian.PutUint64(b[len(b)-8:], uint64(h.payloadLength)) - } - - if h.masked { - b[1] |= 1 << 7 - b = b[:len(b)+4] - copy(b[len(b)-4:], h.maskKey[:]) - } - - return b -} - -func makeReadHeaderBuf() []byte { - return make([]byte, maxHeaderSize-2) -} - -// readHeader reads a header from the reader. -// See https://tools.ietf.org/html/rfc6455#section-5.2 -func readHeader(b []byte, r io.Reader) (header, error) { - if b == nil { - b = makeReadHeaderBuf() - } - - // We read the first two bytes first so that we know - // exactly how long the header is. - b = b[:2] - _, err := io.ReadFull(r, b) - if err != nil { - return header{}, err - } - - var h header - h.fin = b[0]&(1<<7) != 0 - h.rsv1 = b[0]&(1<<6) != 0 - h.rsv2 = b[0]&(1<<5) != 0 - h.rsv3 = b[0]&(1<<4) != 0 - - h.opcode = opcode(b[0] & 0xf) - - var extra int - - h.masked = b[1]&(1<<7) != 0 - if h.masked { - extra += 4 - } - - payloadLength := b[1] &^ (1 << 7) - switch { - case payloadLength < 126: - h.payloadLength = int64(payloadLength) - case payloadLength == 126: - extra += 2 - case payloadLength == 127: - extra += 8 - } - - if extra == 0 { - return h, nil - } - - b = b[:extra] - _, err = io.ReadFull(r, b) - if err != nil { - return header{}, err - } - - switch { - case payloadLength == 126: - h.payloadLength = int64(binary.BigEndian.Uint16(b)) - b = b[2:] - case payloadLength == 127: - h.payloadLength = int64(binary.BigEndian.Uint64(b)) - if h.payloadLength < 0 { - return header{}, fmt.Errorf("header with negative payload length: %v", h.payloadLength) - } - b = b[8:] - } - - if h.masked { - copy(h.maskKey[:], b) - } - - return h, nil -} diff --git a/header_test.go b/header_test.go deleted file mode 100644 index 5d0fd6a2..00000000 --- a/header_test.go +++ /dev/null @@ -1,155 +0,0 @@ -// +build !js - -package websocket - -import ( - "bytes" - "io" - "math/rand" - "strconv" - "testing" - "time" - - "github.com/google/go-cmp/cmp" -) - -func init() { - rand.Seed(time.Now().UnixNano()) -} - -func randBool() bool { - return rand.Intn(1) == 0 -} - -func TestHeader(t *testing.T) { - t.Parallel() - - t.Run("eof", func(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - bytes []byte - }{ - { - "start", - []byte{0xff}, - }, - { - "middle", - []byte{0xff, 0xff, 0xff}, - }, - } - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - b := bytes.NewBuffer(tc.bytes) - _, err := readHeader(nil, b) - if io.ErrUnexpectedEOF != err { - t.Fatalf("expected %v but got: %v", io.ErrUnexpectedEOF, err) - } - }) - } - }) - - t.Run("writeNegativeLength", func(t *testing.T) { - t.Parallel() - - defer func() { - r := recover() - if r == nil { - t.Fatal("failed to induce panic in writeHeader with negative payload length") - } - }() - - writeHeader(nil, header{ - payloadLength: -1, - }) - }) - - t.Run("readNegativeLength", func(t *testing.T) { - t.Parallel() - - b := writeHeader(nil, header{ - payloadLength: 1<<16 + 1, - }) - - // Make length negative - b[2] |= 1 << 7 - - r := bytes.NewReader(b) - _, err := readHeader(nil, r) - if err == nil { - t.Fatalf("unexpected error value: %+v", err) - } - }) - - t.Run("lengths", func(t *testing.T) { - t.Parallel() - - lengths := []int{ - 124, - 125, - 126, - 4096, - 16384, - 65535, - 65536, - 65537, - 131072, - } - - for _, n := range lengths { - n := n - t.Run(strconv.Itoa(n), func(t *testing.T) { - t.Parallel() - - testHeader(t, header{ - payloadLength: int64(n), - }) - }) - } - }) - - t.Run("fuzz", func(t *testing.T) { - t.Parallel() - - for i := 0; i < 10000; i++ { - h := header{ - fin: randBool(), - rsv1: randBool(), - rsv2: randBool(), - rsv3: randBool(), - opcode: opcode(rand.Intn(1 << 4)), - - masked: randBool(), - payloadLength: rand.Int63(), - } - - if h.masked { - rand.Read(h.maskKey[:]) - } - - testHeader(t, h) - } - }) -} - -func testHeader(t *testing.T, h header) { - b := writeHeader(nil, h) - r := bytes.NewReader(b) - h2, err := readHeader(nil, r) - if err != nil { - t.Logf("header: %#v", h) - t.Logf("bytes: %b", b) - t.Fatalf("failed to read header: %v", err) - } - - if !cmp.Equal(h, h2, cmp.AllowUnexported(header{})) { - t.Logf("header: %#v", h) - t.Logf("bytes: %b", b) - t.Fatalf("parsed and read header differ: %v", cmp.Diff(h, h2, cmp.AllowUnexported(header{}))) - } -} diff --git a/messagetype.go b/messagetype.go deleted file mode 100644 index d6436b0b..00000000 --- a/messagetype.go +++ /dev/null @@ -1,17 +0,0 @@ -package websocket - -// MessageType represents the type of a WebSocket message. -// See https://tools.ietf.org/html/rfc6455#section-5.6 -type MessageType int - -//go:generate go run golang.org/x/tools/cmd/stringer -type=MessageType - -// MessageType constants. -const ( - // MessageText is for UTF-8 encoded text messages like JSON. - MessageText MessageType = iota + 1 - // MessageBinary is for binary messages like Protobufs. - MessageBinary -) - -// Above I've explicitly included the types of the constants for stringer. diff --git a/messagetype_string.go b/messagetype_string.go deleted file mode 100644 index bc62db93..00000000 --- a/messagetype_string.go +++ /dev/null @@ -1,25 +0,0 @@ -// Code generated by "stringer -type=MessageType"; DO NOT EDIT. - -package websocket - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[MessageText-1] - _ = x[MessageBinary-2] -} - -const _MessageType_name = "MessageTextMessageBinary" - -var _MessageType_index = [...]uint8{0, 11, 24} - -func (i MessageType) String() string { - i -= 1 - if i < 0 || i >= MessageType(len(_MessageType_index)-1) { - return "MessageType(" + strconv.FormatInt(int64(i+1), 10) + ")" - } - return _MessageType_name[_MessageType_index[i]:_MessageType_index[i+1]] -} diff --git a/opcode.go b/opcode.go deleted file mode 100644 index df708aa0..00000000 --- a/opcode.go +++ /dev/null @@ -1,31 +0,0 @@ -package websocket - -// opcode represents a WebSocket Opcode. -type opcode int - -//go:generate go run golang.org/x/tools/cmd/stringer -type=opcode -tags js - -// opcode constants. -const ( - opContinuation opcode = iota - opText - opBinary - // 3 - 7 are reserved for further non-control frames. - _ - _ - _ - _ - _ - opClose - opPing - opPong - // 11-16 are reserved for further control frames. -) - -func (o opcode) controlOp() bool { - switch o { - case opClose, opPing, opPong: - return true - } - return false -} diff --git a/opcode_string.go b/opcode_string.go deleted file mode 100644 index d7b88961..00000000 --- a/opcode_string.go +++ /dev/null @@ -1,39 +0,0 @@ -// Code generated by "stringer -type=opcode -tags js"; DO NOT EDIT. - -package websocket - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[opContinuation-0] - _ = x[opText-1] - _ = x[opBinary-2] - _ = x[opClose-8] - _ = x[opPing-9] - _ = x[opPong-10] -} - -const ( - _opcode_name_0 = "opContinuationopTextopBinary" - _opcode_name_1 = "opCloseopPingopPong" -) - -var ( - _opcode_index_0 = [...]uint8{0, 14, 20, 28} - _opcode_index_1 = [...]uint8{0, 7, 13, 19} -) - -func (i opcode) String() string { - switch { - case 0 <= i && i <= 2: - return _opcode_name_0[_opcode_index_0[i]:_opcode_index_0[i+1]] - case 8 <= i && i <= 10: - i -= 8 - return _opcode_name_1[_opcode_index_1[i]:_opcode_index_1[i+1]] - default: - return "opcode(" + strconv.FormatInt(int64(i), 10) + ")" - } -} diff --git a/statuscode.go b/statuscode.go deleted file mode 100644 index e7bb9499..00000000 --- a/statuscode.go +++ /dev/null @@ -1,113 +0,0 @@ -package websocket - -import ( - "encoding/binary" - "fmt" -) - -// StatusCode represents a WebSocket status code. -// https://tools.ietf.org/html/rfc6455#section-7.4 -type StatusCode int - -//go:generate go run golang.org/x/tools/cmd/stringer -type=StatusCode - -// These codes were retrieved from: -// https://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number -const ( - StatusNormalClosure StatusCode = 1000 + iota - StatusGoingAway - StatusProtocolError - StatusUnsupportedData - - _ // 1004 is reserved. - - StatusNoStatusRcvd - - // This StatusCode is only exported for use with WASM. - // In pure Go, the returned error will indicate whether the connection was closed or not or what happened. - StatusAbnormalClosure - - StatusInvalidFramePayloadData - StatusPolicyViolation - StatusMessageTooBig - StatusMandatoryExtension - StatusInternalError - StatusServiceRestart - StatusTryAgainLater - StatusBadGateway - - // This StatusCode is only exported for use with WASM. - // In pure Go, the returned error will indicate whether there was a TLS handshake failure. - StatusTLSHandshake -) - -// CloseError represents a WebSocket close frame. -// It is returned by Conn's methods when a WebSocket close frame is received from -// the peer. -// You will need to use the https://golang.org/pkg/errors/#As function, new in Go 1.13, -// to check for this error. See the CloseError example. -type CloseError struct { - Code StatusCode - Reason string -} - -func (ce CloseError) Error() string { - return fmt.Sprintf("status = %v and reason = %q", ce.Code, ce.Reason) -} - -func parseClosePayload(p []byte) (CloseError, error) { - if len(p) == 0 { - return CloseError{ - Code: StatusNoStatusRcvd, - }, nil - } - - if len(p) < 2 { - return CloseError{}, fmt.Errorf("close payload %q too small, cannot even contain the 2 byte status code", p) - } - - ce := CloseError{ - Code: StatusCode(binary.BigEndian.Uint16(p)), - Reason: string(p[2:]), - } - - if !validWireCloseCode(ce.Code) { - return CloseError{}, fmt.Errorf("invalid status code %v", ce.Code) - } - - return ce, nil -} - -// See http://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number -// and https://tools.ietf.org/html/rfc6455#section-7.4.1 -func validWireCloseCode(code StatusCode) bool { - switch code { - case 1004, StatusNoStatusRcvd, StatusAbnormalClosure, StatusTLSHandshake: - return false - } - - if code >= StatusNormalClosure && code <= StatusBadGateway { - return true - } - if code >= 3000 && code <= 4999 { - return true - } - - return false -} - -const maxControlFramePayload = 125 - -func (ce CloseError) bytes() ([]byte, error) { - if len(ce.Reason) > maxControlFramePayload-2 { - return nil, fmt.Errorf("reason string max is %v but got %q with length %v", maxControlFramePayload-2, ce.Reason, len(ce.Reason)) - } - if !validWireCloseCode(ce.Code) { - return nil, fmt.Errorf("status code %v cannot be set", ce.Code) - } - - buf := make([]byte, 2+len(ce.Reason)) - binary.BigEndian.PutUint16(buf, uint16(ce.Code)) - copy(buf[2:], ce.Reason) - return buf, nil -} diff --git a/statuscode_test.go b/statuscode_test.go deleted file mode 100644 index b9637868..00000000 --- a/statuscode_test.go +++ /dev/null @@ -1,157 +0,0 @@ -package websocket - -import ( - "math" - "strings" - "testing" - - "github.com/google/go-cmp/cmp" -) - -func TestCloseError(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - ce CloseError - success bool - }{ - { - name: "normal", - ce: CloseError{ - Code: StatusNormalClosure, - Reason: strings.Repeat("x", maxControlFramePayload-2), - }, - success: true, - }, - { - name: "bigReason", - ce: CloseError{ - Code: StatusNormalClosure, - Reason: strings.Repeat("x", maxControlFramePayload-1), - }, - success: false, - }, - { - name: "bigCode", - ce: CloseError{ - Code: math.MaxUint16, - Reason: strings.Repeat("x", maxControlFramePayload-2), - }, - success: false, - }, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - _, err := tc.ce.bytes() - if (err == nil) != tc.success { - t.Fatalf("unexpected error value: %+v", err) - } - }) - } -} - -func Test_parseClosePayload(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - p []byte - success bool - ce CloseError - }{ - { - name: "normal", - p: append([]byte{0x3, 0xE8}, []byte("hello")...), - success: true, - ce: CloseError{ - Code: StatusNormalClosure, - Reason: "hello", - }, - }, - { - name: "nothing", - success: true, - ce: CloseError{ - Code: StatusNoStatusRcvd, - }, - }, - { - name: "oneByte", - p: []byte{0}, - success: false, - }, - { - name: "badStatusCode", - p: []byte{0x17, 0x70}, - success: false, - }, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - ce, err := parseClosePayload(tc.p) - if (err == nil) != tc.success { - t.Fatalf("unexpected expected error value: %+v", err) - } - - if tc.success && tc.ce != ce { - t.Fatalf("unexpected close error: %v", cmp.Diff(tc.ce, ce)) - } - }) - } -} - -func Test_validWireCloseCode(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - code StatusCode - valid bool - }{ - { - name: "normal", - code: StatusNormalClosure, - valid: true, - }, - { - name: "noStatus", - code: StatusNoStatusRcvd, - valid: false, - }, - { - name: "3000", - code: 3000, - valid: true, - }, - { - name: "4999", - code: 4999, - valid: true, - }, - { - name: "unknown", - code: 5000, - valid: false, - }, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - if valid := validWireCloseCode(tc.code); tc.valid != valid { - t.Fatalf("expected %v for %v but got %v", tc.valid, tc.code, valid) - } - }) - } -} diff --git a/websocket_autobahn_python_test.go b/websocket_autobahn_python_test.go deleted file mode 100644 index 62aa3f8e..00000000 --- a/websocket_autobahn_python_test.go +++ /dev/null @@ -1,243 +0,0 @@ -// This file contains the old autobahn test suite tests that use the -// python binary. The approach is clunky and slow so new tests -// have been written in pure Go in websocket_test.go. -// These have been kept for correctness purposes and are occasionally ran. -// +build autobahn-python - -package websocket_test - -import ( - "context" - "encoding/json" - "fmt" - "io/ioutil" - "net" - "net/http" - "net/http/httptest" - "os" - "os/exec" - "strconv" - "strings" - "testing" - "time" - - "nhooyr.io/websocket/internal/wsecho" -) - -// https://github.com/crossbario/autobahn-python/tree/master/wstest -func TestPythonAutobahnServer(t *testing.T) { - t.Parallel() - - s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c, err := Accept(w, r, &AcceptOptions{ - Subprotocols: []string{"echo"}, - }) - if err != nil { - t.Logf("server handshake failed: %+v", err) - return - } - wsecho.Loop(r.Context(), c) - })) - defer s.Close() - - spec := map[string]interface{}{ - "outdir": "ci/out/wstestServerReports", - "servers": []interface{}{ - map[string]interface{}{ - "agent": "main", - "url": strings.Replace(s.URL, "http", "ws", 1), - }, - }, - "cases": []string{"*"}, - // We skip the UTF-8 handling tests as there isn't any reason to reject invalid UTF-8, just - // more performance overhead. 7.5.1 is the same. - // 12.* and 13.* as we do not support compression. - "exclude-cases": []string{"6.*", "7.5.1", "12.*", "13.*"}, - } - specFile, err := ioutil.TempFile("", "websocketFuzzingClient.json") - if err != nil { - t.Fatalf("failed to create temp file for fuzzingclient.json: %v", err) - } - defer specFile.Close() - - e := json.NewEncoder(specFile) - e.SetIndent("", "\t") - err = e.Encode(spec) - if err != nil { - t.Fatalf("failed to write spec: %v", err) - } - - err = specFile.Close() - if err != nil { - t.Fatalf("failed to close file: %v", err) - } - - ctx := context.Background() - ctx, cancel := context.WithTimeout(ctx, time.Minute*10) - defer cancel() - - args := []string{"--mode", "fuzzingclient", "--spec", specFile.Name()} - wstest := exec.CommandContext(ctx, "wstest", args...) - out, err := wstest.CombinedOutput() - if err != nil { - t.Fatalf("failed to run wstest: %v\nout:\n%s", err, out) - } - - checkWSTestIndex(t, "./ci/out/wstestServerReports/index.json") -} - -func unusedListenAddr() (string, error) { - l, err := net.Listen("tcp", "localhost:0") - if err != nil { - return "", err - } - l.Close() - return l.Addr().String(), nil -} - -// https://github.com/crossbario/autobahn-python/blob/master/wstest/testee_client_aio.py -func TestPythonAutobahnClientOld(t *testing.T) { - t.Parallel() - - serverAddr, err := unusedListenAddr() - if err != nil { - t.Fatalf("failed to get unused listen addr for wstest: %v", err) - } - - wsServerURL := "ws://" + serverAddr - - spec := map[string]interface{}{ - "url": wsServerURL, - "outdir": "ci/out/wstestClientReports", - "cases": []string{"*"}, - // See TestAutobahnServer for the reasons why we exclude these. - "exclude-cases": []string{"6.*", "7.5.1", "12.*", "13.*"}, - } - specFile, err := ioutil.TempFile("", "websocketFuzzingServer.json") - if err != nil { - t.Fatalf("failed to create temp file for fuzzingserver.json: %v", err) - } - defer specFile.Close() - - e := json.NewEncoder(specFile) - e.SetIndent("", "\t") - err = e.Encode(spec) - if err != nil { - t.Fatalf("failed to write spec: %v", err) - } - - err = specFile.Close() - if err != nil { - t.Fatalf("failed to close file: %v", err) - } - - ctx := context.Background() - ctx, cancel := context.WithTimeout(ctx, time.Minute*10) - defer cancel() - - args := []string{"--mode", "fuzzingserver", "--spec", specFile.Name(), - // Disables some server that runs as part of fuzzingserver mode. - // See https://github.com/crossbario/autobahn-testsuite/blob/058db3a36b7c3a1edf68c282307c6b899ca4857f/autobahntestsuite/autobahntestsuite/wstest.py#L124 - "--webport=0", - } - wstest := exec.CommandContext(ctx, "wstest", args...) - err = wstest.Start() - if err != nil { - t.Fatal(err) - } - defer func() { - err := wstest.Process.Kill() - if err != nil { - t.Error(err) - } - }() - - // Let it come up. - time.Sleep(time.Second * 5) - - var cases int - func() { - c, _, err := Dial(ctx, wsServerURL+"/getCaseCount", nil) - if err != nil { - t.Fatal(err) - } - defer c.Close(StatusInternalError, "") - - _, r, err := c.Reader(ctx) - if err != nil { - t.Fatal(err) - } - b, err := ioutil.ReadAll(r) - if err != nil { - t.Fatal(err) - } - cases, err = strconv.Atoi(string(b)) - if err != nil { - t.Fatal(err) - } - - c.Close(StatusNormalClosure, "") - }() - - for i := 1; i <= cases; i++ { - func() { - ctx, cancel := context.WithTimeout(ctx, time.Second*45) - defer cancel() - - c, _, err := Dial(ctx, fmt.Sprintf(wsServerURL+"/runCase?case=%v&agent=main", i), nil) - if err != nil { - t.Fatal(err) - } - wsecho.Loop(ctx, c) - }() - } - - c, _, err := Dial(ctx, fmt.Sprintf(wsServerURL+"/updateReports?agent=main"), nil) - if err != nil { - t.Fatal(err) - } - c.Close(StatusNormalClosure, "") - - checkWSTestIndex(t, "./ci/out/wstestClientReports/index.json") -} - -func checkWSTestIndex(t *testing.T, path string) { - wstestOut, err := ioutil.ReadFile(path) - if err != nil { - t.Fatalf("failed to read index.json: %v", err) - } - - var indexJSON map[string]map[string]struct { - Behavior string `json:"behavior"` - BehaviorClose string `json:"behaviorClose"` - } - err = json.Unmarshal(wstestOut, &indexJSON) - if err != nil { - t.Fatalf("failed to unmarshal index.json: %v", err) - } - - var failed bool - for _, tests := range indexJSON { - for test, result := range tests { - switch result.Behavior { - case "OK", "NON-STRICT", "INFORMATIONAL": - default: - failed = true - t.Errorf("test %v failed", test) - } - switch result.BehaviorClose { - case "OK", "INFORMATIONAL": - default: - failed = true - t.Errorf("bad close behaviour for test %v", test) - } - } - } - - if failed { - path = strings.Replace(path, ".json", ".html", 1) - if os.Getenv("CI") == "" { - t.Errorf("wstest found failure, see %q (output as an artifact in CI)", path) - } - } -} diff --git a/websocket_bench_test.go b/websocket_bench_test.go deleted file mode 100644 index ff2fd704..00000000 --- a/websocket_bench_test.go +++ /dev/null @@ -1,148 +0,0 @@ -// +build !js - -package websocket_test - -import ( - "context" - "io" - "io/ioutil" - "net/http" - "strconv" - "strings" - "testing" - "time" - - "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/wsecho" -) - -func BenchmarkConn(b *testing.B) { - sizes := []int{ - 2, - 16, - 32, - 512, - 4096, - 16384, - } - - b.Run("write", func(b *testing.B) { - for _, size := range sizes { - b.Run(strconv.Itoa(size), func(b *testing.B) { - b.Run("stream", func(b *testing.B) { - benchConn(b, false, true, size) - }) - b.Run("buffer", func(b *testing.B) { - benchConn(b, false, false, size) - }) - }) - } - }) - - b.Run("echo", func(b *testing.B) { - for _, size := range sizes { - b.Run(strconv.Itoa(size), func(b *testing.B) { - benchConn(b, true, true, size) - }) - } - }) -} - -func benchConn(b *testing.B, echo, stream bool, size int) { - s, closeFn := testServer(b, func(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, nil) - if err != nil { - return err - } - if echo { - wsecho.Loop(r.Context(), c) - } else { - discardLoop(r.Context(), c) - } - return nil - }, false) - defer closeFn() - - wsURL := strings.Replace(s.URL, "http", "ws", 1) - - ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) - defer cancel() - - c, _, err := websocket.Dial(ctx, wsURL, nil) - if err != nil { - b.Fatal(err) - } - defer c.Close(websocket.StatusInternalError, "") - - msg := []byte(strings.Repeat("2", size)) - readBuf := make([]byte, len(msg)) - b.SetBytes(int64(len(msg))) - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - if stream { - w, err := c.Writer(ctx, websocket.MessageText) - if err != nil { - b.Fatal(err) - } - - _, err = w.Write(msg) - if err != nil { - b.Fatal(err) - } - - err = w.Close() - if err != nil { - b.Fatal(err) - } - } else { - err = c.Write(ctx, websocket.MessageText, msg) - if err != nil { - b.Fatal(err) - } - } - - if echo { - _, r, err := c.Reader(ctx) - if err != nil { - b.Fatal(err) - } - - _, err = io.ReadFull(r, readBuf) - if err != nil { - b.Fatal(err) - } - } - } - b.StopTimer() - - c.Close(websocket.StatusNormalClosure, "") -} - -func discardLoop(ctx context.Context, c *websocket.Conn) { - defer c.Close(websocket.StatusInternalError, "") - - ctx, cancel := context.WithTimeout(ctx, time.Minute) - defer cancel() - - b := make([]byte, 32768) - echo := func() error { - _, r, err := c.Reader(ctx) - if err != nil { - return err - } - - _, err = io.CopyBuffer(ioutil.Discard, r, b) - if err != nil { - return err - } - return nil - } - - for { - err := echo() - if err != nil { - return - } - } -} diff --git a/websocket_js_test.go b/websocket_js_test.go index 1142190c..e68ba6f3 100644 --- a/websocket_js_test.go +++ b/websocket_js_test.go @@ -50,6 +50,4 @@ func TestConn(t *testing.T) { if err != nil { t.Fatal(err) } - - time.Sleep(time.Millisecond * 100) } diff --git a/xor.go b/xor.go deleted file mode 100644 index f9fe2051..00000000 --- a/xor.go +++ /dev/null @@ -1,127 +0,0 @@ -// +build !js - -package websocket - -import ( - "encoding/binary" -) - -// xor applies the WebSocket masking algorithm to p -// with the given key where the first 3 bits of pos -// are the starting position in the key. -// See https://tools.ietf.org/html/rfc6455#section-5.3 -// -// The returned value is the position of the next byte -// to be used for masking in the key. This is so that -// unmasking can be performed without the entire frame. -func fastXOR(key [4]byte, keyPos int, b []byte) int { - // If the payload is greater than or equal to 16 bytes, then it's worth - // masking 8 bytes at a time. - // Optimization from https://github.com/golang/go/issues/31586#issuecomment-485530859 - if len(b) >= 16 { - // We first create a key that is 8 bytes long - // and is aligned on the position correctly. - var alignedKey [8]byte - for i := range alignedKey { - alignedKey[i] = key[(i+keyPos)&3] - } - k := binary.LittleEndian.Uint64(alignedKey[:]) - - // At some point in the future we can clean these unrolled loops up. - // See https://github.com/golang/go/issues/31586#issuecomment-487436401 - - // Then we xor until b is less than 128 bytes. - for len(b) >= 128 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^k) - v = binary.LittleEndian.Uint64(b[8:]) - binary.LittleEndian.PutUint64(b[8:], v^k) - v = binary.LittleEndian.Uint64(b[16:]) - binary.LittleEndian.PutUint64(b[16:], v^k) - v = binary.LittleEndian.Uint64(b[24:]) - binary.LittleEndian.PutUint64(b[24:], v^k) - v = binary.LittleEndian.Uint64(b[32:]) - binary.LittleEndian.PutUint64(b[32:], v^k) - v = binary.LittleEndian.Uint64(b[40:]) - binary.LittleEndian.PutUint64(b[40:], v^k) - v = binary.LittleEndian.Uint64(b[48:]) - binary.LittleEndian.PutUint64(b[48:], v^k) - v = binary.LittleEndian.Uint64(b[56:]) - binary.LittleEndian.PutUint64(b[56:], v^k) - v = binary.LittleEndian.Uint64(b[64:]) - binary.LittleEndian.PutUint64(b[64:], v^k) - v = binary.LittleEndian.Uint64(b[72:]) - binary.LittleEndian.PutUint64(b[72:], v^k) - v = binary.LittleEndian.Uint64(b[80:]) - binary.LittleEndian.PutUint64(b[80:], v^k) - v = binary.LittleEndian.Uint64(b[88:]) - binary.LittleEndian.PutUint64(b[88:], v^k) - v = binary.LittleEndian.Uint64(b[96:]) - binary.LittleEndian.PutUint64(b[96:], v^k) - v = binary.LittleEndian.Uint64(b[104:]) - binary.LittleEndian.PutUint64(b[104:], v^k) - v = binary.LittleEndian.Uint64(b[112:]) - binary.LittleEndian.PutUint64(b[112:], v^k) - v = binary.LittleEndian.Uint64(b[120:]) - binary.LittleEndian.PutUint64(b[120:], v^k) - b = b[128:] - } - - // Then we xor until b is less than 64 bytes. - for len(b) >= 64 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^k) - v = binary.LittleEndian.Uint64(b[8:]) - binary.LittleEndian.PutUint64(b[8:], v^k) - v = binary.LittleEndian.Uint64(b[16:]) - binary.LittleEndian.PutUint64(b[16:], v^k) - v = binary.LittleEndian.Uint64(b[24:]) - binary.LittleEndian.PutUint64(b[24:], v^k) - v = binary.LittleEndian.Uint64(b[32:]) - binary.LittleEndian.PutUint64(b[32:], v^k) - v = binary.LittleEndian.Uint64(b[40:]) - binary.LittleEndian.PutUint64(b[40:], v^k) - v = binary.LittleEndian.Uint64(b[48:]) - binary.LittleEndian.PutUint64(b[48:], v^k) - v = binary.LittleEndian.Uint64(b[56:]) - binary.LittleEndian.PutUint64(b[56:], v^k) - b = b[64:] - } - - // Then we xor until b is less than 32 bytes. - for len(b) >= 32 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^k) - v = binary.LittleEndian.Uint64(b[8:]) - binary.LittleEndian.PutUint64(b[8:], v^k) - v = binary.LittleEndian.Uint64(b[16:]) - binary.LittleEndian.PutUint64(b[16:], v^k) - v = binary.LittleEndian.Uint64(b[24:]) - binary.LittleEndian.PutUint64(b[24:], v^k) - b = b[32:] - } - - // Then we xor until b is less than 16 bytes. - for len(b) >= 16 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^k) - v = binary.LittleEndian.Uint64(b[8:]) - binary.LittleEndian.PutUint64(b[8:], v^k) - b = b[16:] - } - - // Then we xor until b is less than 8 bytes. - for len(b) >= 8 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^k) - b = b[8:] - } - } - - // xor remaining bytes. - for i := range b { - b[i] ^= key[keyPos&3] - keyPos++ - } - return keyPos & 3 -} diff --git a/xor_test.go b/xor_test.go deleted file mode 100644 index 70047a9c..00000000 --- a/xor_test.go +++ /dev/null @@ -1,84 +0,0 @@ -// +build !js - -package websocket - -import ( - "crypto/rand" - "strconv" - "testing" - - "github.com/google/go-cmp/cmp" -) - -func Test_xor(t *testing.T) { - t.Parallel() - - key := [4]byte{0xa, 0xb, 0xc, 0xff} - p := []byte{0xa, 0xb, 0xc, 0xf2, 0xc} - pos := 0 - pos = fastXOR(key, pos, p) - - if exp := []byte{0, 0, 0, 0x0d, 0x6}; !cmp.Equal(exp, p) { - t.Fatalf("unexpected mask: %v", cmp.Diff(exp, p)) - } - - if exp := 1; !cmp.Equal(exp, pos) { - t.Fatalf("unexpected mask pos: %v", cmp.Diff(exp, pos)) - } -} - -func basixXOR(maskKey [4]byte, pos int, b []byte) int { - for i := range b { - b[i] ^= maskKey[pos&3] - pos++ - } - return pos & 3 -} - -func BenchmarkXOR(b *testing.B) { - sizes := []int{ - 2, - 16, - 32, - 512, - 4096, - 16384, - } - - fns := []struct { - name string - fn func([4]byte, int, []byte) int - }{ - { - "basic", - basixXOR, - }, - { - "fast", - fastXOR, - }, - } - - var maskKey [4]byte - _, err := rand.Read(maskKey[:]) - if err != nil { - b.Fatalf("failed to populate mask key: %v", err) - } - - for _, size := range sizes { - data := make([]byte, size) - - b.Run(strconv.Itoa(size), func(b *testing.B) { - for _, fn := range fns { - b.Run(fn.name, func(b *testing.B) { - b.ReportAllocs() - b.SetBytes(int64(size)) - - for i := 0; i < b.N; i++ { - fn.fn(maskKey, 0, data) - } - }) - } - }) - } -} From ece99d2aac09b2f5325a5514e86c857d6800ce2e Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 22 Sep 2019 14:14:05 -0500 Subject: [PATCH 145/519] Add *Conn.CloseRead for WASM --- conn.go | 21 --------------------- doc.go | 6 +++--- netconn.go | 25 +++++++++++++++++++++++++ websocket_js.go | 19 ++++++++++++++++--- 4 files changed, 44 insertions(+), 27 deletions(-) diff --git a/conn.go b/conn.go index bc115e38..e12e1443 100644 --- a/conn.go +++ b/conn.go @@ -406,27 +406,6 @@ func (c *Conn) reader(ctx context.Context) (MessageType, io.Reader, error) { return MessageType(h.opcode), r, nil } -// CloseRead will start a goroutine to read from the connection until it is closed or a data message -// is received. If a data message is received, the connection will be closed with StatusPolicyViolation. -// Since CloseRead reads from the connection, it will respond to ping, pong and close frames. -// After calling this method, you cannot read any data messages from the connection. -// The returned context will be cancelled when the connection is closed. -// -// Use this when you do not want to read data messages from the connection anymore but will -// want to write messages to it. -func (c *Conn) CloseRead(ctx context.Context) context.Context { - atomic.StoreInt64(&c.readClosed, 1) - - ctx, cancel := context.WithCancel(ctx) - go func() { - defer cancel() - // We use the unexported reader so that we don't get the read closed error. - c.reader(ctx) - c.Close(StatusPolicyViolation, "unexpected data message") - }() - return ctx -} - // messageReader enables reading a data frame from the WebSocket connection. type messageReader struct { c *Conn diff --git a/doc.go b/doc.go index da6f3222..2a5a0a1a 100644 --- a/doc.go +++ b/doc.go @@ -25,10 +25,10 @@ // // See https://developer.mozilla.org/en-US/docs/Web/API/WebSocket // -// Thus the unsupported features when compiling to WASM are: +// Thus the unsupported features (not compiled in) for WASM are: // - Accept and AcceptOptions -// - Conn's Reader, Writer, SetReadLimit, Ping methods -// - HTTPClient and HTTPHeader dial options +// - Conn's Reader, Writer, SetReadLimit and Ping methods +// - HTTPClient and HTTPHeader fields in DialOptions // // The *http.Response returned by Dial will always either be nil or &http.Response{} as // we do not have access to the handshake response in the browser. diff --git a/netconn.go b/netconn.go index 8efdade2..34f771c6 100644 --- a/netconn.go +++ b/netconn.go @@ -7,6 +7,7 @@ import ( "io" "math" "net" + "sync/atomic" "time" ) @@ -159,3 +160,27 @@ func (c *netConn) SetReadDeadline(t time.Time) error { } return nil } + +// CloseRead will start a goroutine to read from the connection until it is closed or a data message +// is received. If a data message is received, the connection will be closed with StatusPolicyViolation. +// Since CloseRead reads from the connection, it will respond to ping, pong and close frames. +// After calling this method, you cannot read any data messages from the connection. +// The returned context will be cancelled when the connection is closed. +// +// Use this when you do not want to read data messages from the connection anymore but will +// want to write messages to it. +func (c *Conn) CloseRead(ctx context.Context) context.Context { + atomic.StoreInt64(&c.readClosed, 1) + + ctx, cancel := context.WithCancel(ctx) + go func() { + defer cancel() + // We use the unexported reader method so that we don't get the read closed error. + c.reader(ctx) + // Either the connection is already closed since there was a read error + // or the context was cancelled or a message was read and we should close + // the connection. + c.Close(StatusPolicyViolation, "unexpected data message") + }() + return ctx +} diff --git a/websocket_js.go b/websocket_js.go index 14f198d1..123bc8f4 100644 --- a/websocket_js.go +++ b/websocket_js.go @@ -10,6 +10,7 @@ import ( "reflect" "runtime" "sync" + "sync/atomic" "syscall/js" "nhooyr.io/websocket/internal/wsjs" @@ -19,9 +20,10 @@ import ( type Conn struct { ws wsjs.WebSocket - closeOnce sync.Once - closed chan struct{} - closeErr error + readClosed int64 + closeOnce sync.Once + closed chan struct{} + closeErr error releaseOnClose func() releaseOnMessage func() @@ -67,6 +69,10 @@ func (c *Conn) init() { // Read attempts to read a message from the connection. // The maximum time spent waiting is bounded by the context. func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { + if atomic.LoadInt64(&c.readClosed) == 1 { + return 0, nil, fmt.Errorf("websocket connection read closed") + } + typ, p, err := c.read(ctx) if err != nil { return 0, nil, fmt.Errorf("failed to read: %w", err) @@ -78,6 +84,7 @@ func (c *Conn) read(ctx context.Context) (MessageType, []byte, error) { var me wsjs.MessageEvent select { case <-ctx.Done(): + c.Close(StatusPolicyViolation, "read timed out") return 0, nil, ctx.Err() case me = <-c.readch: case <-c.closed: @@ -198,6 +205,7 @@ func dial(ctx context.Context, url string, opts *DialOptions) (*Conn, *http.Resp select { case <-ctx.Done(): + c.Close(StatusPolicyViolation, "dial timed out") return nil, nil, ctx.Err() case <-opench: case <-c.closed: @@ -215,3 +223,8 @@ func (c *netConn) netConnReader(ctx context.Context) (MessageType, io.Reader, er } return typ, bytes.NewReader(p), nil } + +// Only implemented for use by *Conn.CloseRead in netconn.go +func (c *Conn) reader(ctx context.Context) { + c.read(ctx) +} From 8c54bd9b44d106dcb2aee2b8e7a9e4aae33d62dd Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 22 Sep 2019 15:12:11 -0500 Subject: [PATCH 146/519] Allow concurrent access NetConn wrapper reading Updates #88 --- netconn.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/netconn.go b/netconn.go index 34f771c6..c5c0e17b 100644 --- a/netconn.go +++ b/netconn.go @@ -7,6 +7,7 @@ import ( "io" "math" "net" + "sync" "sync/atomic" "time" ) @@ -69,8 +70,9 @@ type netConn struct { readTimer *time.Timer readContext context.Context - eofed bool + readMu sync.Mutex + eofed bool reader io.Reader } @@ -89,6 +91,9 @@ func (c *netConn) Write(p []byte) (int, error) { } func (c *netConn) Read(p []byte) (int, error) { + c.readMu.Lock() + defer c.readMu.Unlock() + if c.eofed { return 0, io.EOF } From c381928c9f8077c6b61efa8a297264e3e9c88ade Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 22 Sep 2019 23:28:41 -0500 Subject: [PATCH 147/519] Implement more of the API for WASM Realized I can at least make the Reader/Writer/SetReadLimit methods work as expected even if they're not perfect. --- conn.go | 22 +++--------- netconn.go => conn_common.go | 15 +++++++- doc.go | 12 ++++++- websocket_js.go | 66 +++++++++++++++++++++++++++++++++-- wsjson/wsjson.go | 6 +--- wsjson/wsjson_js.go | 58 ------------------------------- wspb/wspb.go | 6 +--- wspb/wspb_js.go | 67 ------------------------------------ 8 files changed, 95 insertions(+), 157 deletions(-) rename netconn.go => conn_common.go (91%) delete mode 100644 wsjson/wsjson_js.go delete mode 100644 wspb/wspb_js.go diff --git a/conn.go b/conn.go index e12e1443..20dbece2 100644 --- a/conn.go +++ b/conn.go @@ -59,7 +59,7 @@ type Conn struct { msgReadLimit int64 // Used to ensure a previous writer is not used after being closed. - activeWriter *messageWriter + activeWriter atomic.Value // messageWriter state. writeMsgOpcode opcode writeMsgCtx context.Context @@ -526,16 +526,6 @@ func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { return n, err } -// SetReadLimit sets the max number of bytes to read for a single message. -// It applies to the Reader and Read methods. -// -// By default, the connection has a message read limit of 32768 bytes. -// -// When the limit is hit, the connection will be closed with StatusMessageTooBig. -func (c *Conn) SetReadLimit(n int64) { - c.msgReadLimit = n -} - // Read is a convenience method to read a single message from the connection. // // See the Reader method if you want to be able to reuse buffers or want to stream a message. @@ -575,7 +565,7 @@ func (c *Conn) writer(ctx context.Context, typ MessageType) (io.WriteCloser, err w := &messageWriter{ c: c, } - c.activeWriter = w + c.activeWriter.Store(w) return w, nil } @@ -607,7 +597,7 @@ type messageWriter struct { } func (w *messageWriter) closed() bool { - return w != w.c.activeWriter + return w != w.c.activeWriter.Load() } // Write writes the given bytes to the WebSocket connection. @@ -645,7 +635,7 @@ func (w *messageWriter) close() error { if w.closed() { return fmt.Errorf("cannot use closed writer") } - w.c.activeWriter = nil + w.c.activeWriter.Store((*messageWriter)(nil)) _, err := w.c.writeFrame(w.c.writeMsgCtx, true, w.c.writeMsgOpcode, nil) if err != nil { @@ -925,7 +915,3 @@ func (c *Conn) extractBufioWriterBuf(w io.Writer) { c.bw.Reset(w) } - -func (c *netConn) netConnReader(ctx context.Context) (MessageType, io.Reader, error) { - return c.c.Reader(c.readContext) -} diff --git a/netconn.go b/conn_common.go similarity index 91% rename from netconn.go rename to conn_common.go index c5c0e17b..771db26b 100644 --- a/netconn.go +++ b/conn_common.go @@ -1,3 +1,6 @@ +// This file contains *Conn symbols relevant to both +// WASM and non WASM builds. + package websocket import ( @@ -99,7 +102,7 @@ func (c *netConn) Read(p []byte) (int, error) { } if c.reader == nil { - typ, r, err := c.netConnReader(c.readContext) + typ, r, err := c.c.Reader(c.readContext) if err != nil { var ce CloseError if errors.As(err, &ce) && (ce.Code == StatusNormalClosure) || (ce.Code == StatusGoingAway) { @@ -189,3 +192,13 @@ func (c *Conn) CloseRead(ctx context.Context) context.Context { }() return ctx } + +// SetReadLimit sets the max number of bytes to read for a single message. +// It applies to the Reader and Read methods. +// +// By default, the connection has a message read limit of 32768 bytes. +// +// When the limit is hit, the connection will be closed with StatusMessageTooBig. +func (c *Conn) SetReadLimit(n int64) { + c.msgReadLimit = n +} diff --git a/doc.go b/doc.go index 2a5a0a1a..7753afc7 100644 --- a/doc.go +++ b/doc.go @@ -26,13 +26,23 @@ // See https://developer.mozilla.org/en-US/docs/Web/API/WebSocket // // Thus the unsupported features (not compiled in) for WASM are: +// // - Accept and AcceptOptions -// - Conn's Reader, Writer, SetReadLimit and Ping methods +// - Conn.Ping // - HTTPClient and HTTPHeader fields in DialOptions // // The *http.Response returned by Dial will always either be nil or &http.Response{} as // we do not have access to the handshake response in the browser. // +// The Writer method on the Conn buffers everything in memory and then sends it as a message +// when the writer is closed. +// +// The Reader method also reads the entire response and then returns a reader that +// reads from the byte slice. +// +// SetReadLimit cannot actually limit the number of bytes read from the connection so instead +// when a message beyond the limit is fully read, it throws an error. +// // Writes are also always async so the passed context is no-op. // // Everything else is fully supported. This includes the wsjson and wspb helper packages. diff --git a/websocket_js.go b/websocket_js.go index 123bc8f4..4ed49d97 100644 --- a/websocket_js.go +++ b/websocket_js.go @@ -13,6 +13,7 @@ import ( "sync/atomic" "syscall/js" + "nhooyr.io/websocket/internal/bpool" "nhooyr.io/websocket/internal/wsjs" ) @@ -20,6 +21,8 @@ import ( type Conn struct { ws wsjs.WebSocket + msgReadLimit int64 + readClosed int64 closeOnce sync.Once closed chan struct{} @@ -43,6 +46,7 @@ func (c *Conn) close(err error) { func (c *Conn) init() { c.closed = make(chan struct{}) c.readch = make(chan wsjs.MessageEvent, 1) + c.msgReadLimit = 32768 c.releaseOnClose = c.ws.OnClose(func(e wsjs.CloseEvent) { cerr := CloseError{ @@ -77,6 +81,10 @@ func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { if err != nil { return 0, nil, fmt.Errorf("failed to read: %w", err) } + if int64(len(p)) > c.msgReadLimit { + c.Close(StatusMessageTooBig, fmt.Sprintf("read limited at %v bytes", c.msgReadLimit)) + return 0, nil, c.closeErr + } return typ, p, nil } @@ -106,6 +114,11 @@ func (c *Conn) read(ctx context.Context) (MessageType, []byte, error) { func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { err := c.write(ctx, typ, p) if err != nil { + // Have to ensure the WebSocket is closed after a write error + // to match the Go API. It can only error if the message type + // is unexpected or the passed bytes contain invalid UTF-8 for + // MessageText. + c.Close(StatusInternalError, "something went wrong") return fmt.Errorf("failed to write: %w", err) } return nil @@ -216,8 +229,10 @@ func dial(ctx context.Context, url string, opts *DialOptions) (*Conn, *http.Resp return c, &http.Response{}, nil } -func (c *netConn) netConnReader(ctx context.Context) (MessageType, io.Reader, error) { - typ, p, err := c.c.Read(ctx) +// Reader attempts to read a message from the connection. +// The maximum time spent waiting is bounded by the context. +func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) { + typ, p, err := c.Read(ctx) if err != nil { return 0, nil, err } @@ -228,3 +243,50 @@ func (c *netConn) netConnReader(ctx context.Context) (MessageType, io.Reader, er func (c *Conn) reader(ctx context.Context) { c.read(ctx) } + +// Writer returns a writer to write a WebSocket data message to the connection. +// It buffers the entire message in memory and then sends it when the writer +// is closed. +func (c *Conn) Writer(ctx context.Context, typ MessageType) (io.WriteCloser, error) { + return writer{ + c: c, + ctx: ctx, + typ: typ, + b: bpool.Get(), + }, nil +} + +type writer struct { + closed bool + + c *Conn + ctx context.Context + typ MessageType + + b *bytes.Buffer +} + +func (w writer) Write(p []byte) (int, error) { + if w.closed { + return 0, errors.New("cannot write to closed writer") + } + n, err := w.b.Write(p) + if err != nil { + return n, fmt.Errorf("failed to write message: %w", err) + } + return n, nil +} + +func (w writer) Close() error { + if w.closed { + return errors.New("cannot close closed writer") + } + w.closed = true + defer bpool.Put(w.b) + + err := w.c.Write(w.ctx, w.typ, w.b.Bytes()) + if err != nil { + return fmt.Errorf("failed to close writer: %w", err) + } + return nil +} diff --git a/wsjson/wsjson.go b/wsjson/wsjson.go index ffdd24ac..fe935fa1 100644 --- a/wsjson/wsjson.go +++ b/wsjson/wsjson.go @@ -1,5 +1,3 @@ -// +build !js - // Package wsjson provides websocket helpers for JSON messages. package wsjson // import "nhooyr.io/websocket/wsjson" @@ -34,9 +32,7 @@ func read(ctx context.Context, c *websocket.Conn, v interface{}) error { } b := bpool.Get() - defer func() { - bpool.Put(b) - }() + defer bpool.Put(b) _, err = b.ReadFrom(r) if err != nil { diff --git a/wsjson/wsjson_js.go b/wsjson/wsjson_js.go deleted file mode 100644 index 5b88ce3b..00000000 --- a/wsjson/wsjson_js.go +++ /dev/null @@ -1,58 +0,0 @@ -// +build js - -package wsjson - -import ( - "context" - "encoding/json" - "fmt" - - "nhooyr.io/websocket" -) - -// Read reads a json message from c into v. -func Read(ctx context.Context, c *websocket.Conn, v interface{}) error { - err := read(ctx, c, v) - if err != nil { - return fmt.Errorf("failed to read json: %w", err) - } - return nil -} - -func read(ctx context.Context, c *websocket.Conn, v interface{}) error { - typ, b, err := c.Read(ctx) - if err != nil { - return err - } - - if typ != websocket.MessageText { - c.Close(websocket.StatusUnsupportedData, "can only accept text messages") - return fmt.Errorf("unexpected frame type for json (expected %v): %v", websocket.MessageText, typ) - } - - err = json.Unmarshal(b, v) - if err != nil { - c.Close(websocket.StatusInvalidFramePayloadData, "failed to unmarshal JSON") - return fmt.Errorf("failed to unmarshal json: %w", err) - } - - return nil -} - -// Write writes the json message v to c. -func Write(ctx context.Context, c *websocket.Conn, v interface{}) error { - err := write(ctx, c, v) - if err != nil { - return fmt.Errorf("failed to write json: %w", err) - } - return nil -} - -func write(ctx context.Context, c *websocket.Conn, v interface{}) error { - b, err := json.Marshal(v) - if err != nil { - return err - } - - return c.Write(ctx, websocket.MessageText, b) -} diff --git a/wspb/wspb.go b/wspb/wspb.go index b32b0c1b..3c9e0f76 100644 --- a/wspb/wspb.go +++ b/wspb/wspb.go @@ -1,5 +1,3 @@ -// +build !js - // Package wspb provides websocket helpers for protobuf messages. package wspb // import "nhooyr.io/websocket/wspb" @@ -36,9 +34,7 @@ func read(ctx context.Context, c *websocket.Conn, v proto.Message) error { } b := bpool.Get() - defer func() { - bpool.Put(b) - }() + defer bpool.Put(b) _, err = b.ReadFrom(r) if err != nil { diff --git a/wspb/wspb_js.go b/wspb/wspb_js.go deleted file mode 100644 index 6f69eddd..00000000 --- a/wspb/wspb_js.go +++ /dev/null @@ -1,67 +0,0 @@ -// +build js - -package wspb // import "nhooyr.io/websocket/wspb" - -import ( - "bytes" - "context" - "fmt" - - "github.com/golang/protobuf/proto" - - "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/bpool" -) - -// Read reads a protobuf message from c into v. -func Read(ctx context.Context, c *websocket.Conn, v proto.Message) error { - err := read(ctx, c, v) - if err != nil { - return fmt.Errorf("failed to read protobuf: %w", err) - } - return nil -} - -func read(ctx context.Context, c *websocket.Conn, v proto.Message) error { - typ, p, err := c.Read(ctx) - if err != nil { - return err - } - - if typ != websocket.MessageBinary { - c.Close(websocket.StatusUnsupportedData, "can only accept binary messages") - return fmt.Errorf("unexpected frame type for protobuf (expected %v): %v", websocket.MessageBinary, typ) - } - - err = proto.Unmarshal(p, v) - if err != nil { - c.Close(websocket.StatusInvalidFramePayloadData, "failed to unmarshal protobuf") - return fmt.Errorf("failed to unmarshal protobuf: %w", err) - } - - return nil -} - -// Write writes the protobuf message v to c. -func Write(ctx context.Context, c *websocket.Conn, v proto.Message) error { - err := write(ctx, c, v) - if err != nil { - return fmt.Errorf("failed to write protobuf: %w", err) - } - return nil -} - -func write(ctx context.Context, c *websocket.Conn, v proto.Message) error { - b := bpool.Get() - pb := proto.NewBuffer(b.Bytes()) - defer func() { - bpool.Put(bytes.NewBuffer(pb.Bytes())) - }() - - err := pb.Marshal(v) - if err != nil { - return fmt.Errorf("failed to marshal protobuf: %w", err) - } - - return c.Write(ctx, websocket.MessageBinary, pb.Bytes()) -} From 676bb696307d6932c077a31fb3788aa78f117143 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 22 Sep 2019 23:46:46 -0500 Subject: [PATCH 148/519] Rename frame_string.go to frame_stringer.go --- frame.go | 2 +- frame_string.go => frame_stringer.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename frame_string.go => frame_stringer.go (98%) diff --git a/frame.go b/frame.go index 10cb9e38..84a18e02 100644 --- a/frame.go +++ b/frame.go @@ -7,7 +7,7 @@ import ( "math" ) -//go:generate go run golang.org/x/tools/cmd/stringer -type=opcode,MessageType,StatusCode -output=frame_string.go +//go:generate go run golang.org/x/tools/cmd/stringer -type=opcode,MessageType,StatusCode -output=frame_stringer.go // opcode represents a WebSocket Opcode. type opcode int diff --git a/frame_string.go b/frame_stringer.go similarity index 98% rename from frame_string.go rename to frame_stringer.go index 6b32672a..e60e53fa 100644 --- a/frame_string.go +++ b/frame_stringer.go @@ -1,4 +1,4 @@ -// Code generated by "stringer -type=opcode,MessageType,StatusCode -output=frame_string.go"; DO NOT EDIT. +// Code generated by "stringer -type=opcode,MessageType,StatusCode -output=frame_stringer.go"; DO NOT EDIT. package websocket From ac32d3f5d67af4c21650245eefee881170fa7919 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 22 Sep 2019 23:58:23 -0500 Subject: [PATCH 149/519] Cleanup README language --- README.md | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index ed22b1de..dc33e194 100644 --- a/README.md +++ b/README.md @@ -110,26 +110,25 @@ https://github.com/gorilla/websocket This package is the community standard but it is 6 years old and over time has accumulated cruft. There are too many ways to do the same thing. Just compare the godoc of -[nhooyr/websocket](https://godoc.org/nhooyr.io/websocket) side by side with -[gorilla/websocket](https://godoc.org/github.com/gorilla/websocket). +[nhooyr/websocket](https://godoc.org/nhooyr.io/websocket) with +[gorilla/websocket](https://godoc.org/github.com/gorilla/websocket) side by side. -The API for nhooyr/websocket has been designed such that there is only one way to do things -which makes it easy to use correctly. Not only is the API simpler, the implementation is +The API for nhooyr/websocket has been designed such that there is only one way to do things. +This makes it easy to use correctly. Not only is the API simpler, the implementation is only 2200 lines whereas gorilla/websocket is at 3500 lines. That's more code to maintain, more code to test, more code to document and more surface area for bugs. -Moreover, nhooyr/websocket has support for newer Go idioms such as context.Context and -also uses net/http's Client and ResponseWriter directly for WebSocket handshakes. -gorilla/websocket writes its handshakes to the underlying net.Conn which means -it has to reinvent hooks for TLS and proxies and prevents support of HTTP/2. +Moreover, nhooyr/websocket supports newer Go idioms such as context.Context. +It also uses net/http's Client and ResponseWriter directly for WebSocket handshakes. +gorilla/websocket writes its handshakes to the underlying net.Conn. +Thus it has to reinvent hooks for TLS and proxies and prevents support of HTTP/2. Some more advantages of nhooyr/websocket are that it supports concurrent writes and makes it very easy to close the connection with a status code and reason. The ping API is also nicer. gorilla/websocket requires registering a pong handler on the Conn which results in awkward control flow. With nhooyr/websocket you use the Ping method on the Conn -that sends a ping and also waits for the pong, though you must be reading from the connection -for the pong to be read. +that sends a ping and also waits for the pong. Additionally, nhooyr.io/websocket can compile to [WASM](https://godoc.org/nhooyr.io/websocket#hdr-WASM) for the browser. @@ -137,7 +136,7 @@ In terms of performance, the differences mostly depend on your application code. reuses message buffers out of the box if you use the wsjson and wspb subpackages. As mentioned above, nhooyr/websocket also supports concurrent writers. -The only performance con to nhooyr/websocket is that uses one extra goroutine to support +The only performance con to nhooyr/websocket is that it uses one extra goroutine to support cancellation with context.Context. This costs 2 KB of memory which is cheap compared to the benefits. @@ -161,9 +160,9 @@ effort to ensure its speed and I have applied as many of its optimizations as I could into nhooyr/websocket. Definitely check out his fantastic [blog post](https://medium.freecodecamp.org/million-websockets-and-go-cc58418460bb) about performant WebSocket servers. -If you want a library that gives you absolute control over everything, this is the library, -but for most users, the API provided by nhooyr/websocket will fit better as it is nearly just -as performant but much easier to use correctly and idiomatic. +If you want a library that gives you absolute control over everything, this is the library. +But for 99.9% of use cases, nhooyr/websocket will fit better. It's nearly as performant +but much easier to use. ## Contributing From 480d0eb5d908f97051f6369d9bdbcf22b794af0a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 23 Sep 2019 15:14:47 -0500 Subject: [PATCH 150/519] Fixes from @albrow's Wasm review --- README.md | 4 ++-- assert_test.go | 13 +++++++++++-- conn_common.go | 2 +- doc.go | 6 +++--- frame.go | 8 ++++---- internal/wsjs/{wsjs.go => wsjs_js.go} | 0 websocket_js_test.go | 4 ++-- 7 files changed, 23 insertions(+), 14 deletions(-) rename internal/wsjs/{wsjs.go => wsjs_js.go} (100%) diff --git a/README.md b/README.md index dc33e194..c5b8c907 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ go get nhooyr.io/websocket - JSON and ProtoBuf helpers in the [wsjson](https://godoc.org/nhooyr.io/websocket/wsjson) and [wspb](https://godoc.org/nhooyr.io/websocket/wspb) subpackages - Highly optimized by default - Concurrent writes out of the box -- [Complete WASM](https://godoc.org/nhooyr.io/websocket#hdr-WASM) support +- [Complete Wasm](https://godoc.org/nhooyr.io/websocket#hdr-Wasm) support ## Roadmap @@ -130,7 +130,7 @@ The ping API is also nicer. gorilla/websocket requires registering a pong handle which results in awkward control flow. With nhooyr/websocket you use the Ping method on the Conn that sends a ping and also waits for the pong. -Additionally, nhooyr.io/websocket can compile to [WASM](https://godoc.org/nhooyr.io/websocket#hdr-WASM) for the browser. +Additionally, nhooyr.io/websocket can compile to [Wasm](https://godoc.org/nhooyr.io/websocket#hdr-Wasm) for the browser. In terms of performance, the differences mostly depend on your application code. nhooyr/websocket reuses message buffers out of the box if you use the wsjson and wspb subpackages. diff --git a/assert_test.go b/assert_test.go index cddae99d..8970c543 100644 --- a/assert_test.go +++ b/assert_test.go @@ -2,10 +2,10 @@ package websocket_test import ( "context" - "encoding/hex" "fmt" "math/rand" "reflect" + "strings" "github.com/google/go-cmp/cmp" @@ -99,7 +99,16 @@ func randBytes(n int) []byte { } func randString(n int) string { - return hex.EncodeToString(randBytes(n))[:n] + s := strings.ToValidUTF8(string(randBytes(n)), "_") + if len(s) > n { + return s[:n] + } + if len(s) < n { + // Pad with = + extra := n - len(s) + return s + strings.Repeat("=", extra) + } + return s } func assertEcho(ctx context.Context, c *websocket.Conn, typ websocket.MessageType, n int) error { diff --git a/conn_common.go b/conn_common.go index 771db26b..1429b47d 100644 --- a/conn_common.go +++ b/conn_common.go @@ -1,5 +1,5 @@ // This file contains *Conn symbols relevant to both -// WASM and non WASM builds. +// Wasm and non Wasm builds. package websocket diff --git a/doc.go b/doc.go index 7753afc7..a17bfb05 100644 --- a/doc.go +++ b/doc.go @@ -18,14 +18,14 @@ // Use the errors.As function new in Go 1.13 to check for websocket.CloseError. // See the CloseError example. // -// WASM +// Wasm // -// The client side fully supports compiling to WASM. +// The client side fully supports compiling to Wasm. // It wraps the WebSocket browser API. // // See https://developer.mozilla.org/en-US/docs/Web/API/WebSocket // -// Thus the unsupported features (not compiled in) for WASM are: +// Thus the unsupported features (not compiled in) for Wasm are: // // - Accept and AcceptOptions // - Conn.Ping diff --git a/frame.go b/frame.go index 84a18e02..95061f5a 100644 --- a/frame.go +++ b/frame.go @@ -213,8 +213,8 @@ const ( StatusNoStatusRcvd - // This StatusCode is only exported for use with WASM. - // In non WASM Go, the returned error will indicate whether the connection was closed or not or what happened. + // This StatusCode is only exported for use with Wasm. + // In non Wasm Go, the returned error will indicate whether the connection was closed or not or what happened. StatusAbnormalClosure StatusInvalidFramePayloadData @@ -226,8 +226,8 @@ const ( StatusTryAgainLater StatusBadGateway - // This StatusCode is only exported for use with WASM. - // In non WASM Go, the returned error will indicate whether there was a TLS handshake failure. + // This StatusCode is only exported for use with Wasm. + // In non Wasm Go, the returned error will indicate whether there was a TLS handshake failure. StatusTLSHandshake ) diff --git a/internal/wsjs/wsjs.go b/internal/wsjs/wsjs_js.go similarity index 100% rename from internal/wsjs/wsjs.go rename to internal/wsjs/wsjs_js.go diff --git a/websocket_js_test.go b/websocket_js_test.go index e68ba6f3..ba9431d4 100644 --- a/websocket_js_test.go +++ b/websocket_js_test.go @@ -36,12 +36,12 @@ func TestConn(t *testing.T) { t.Fatal(err) } - err = assertJSONEcho(ctx, c, 16) + err = assertJSONEcho(ctx, c, 1024) if err != nil { t.Fatal(err) } - err = assertEcho(ctx, c, websocket.MessageBinary, 16) + err = assertEcho(ctx, c, websocket.MessageBinary, 1024) if err != nil { t.Fatal(err) } From 9fc9f7ab6742008fb936186272696c9933d9c51b Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 23 Sep 2019 16:53:37 -0500 Subject: [PATCH 151/519] Ensure message order with a buffer --- conn.go | 6 ----- conn_common.go | 6 +++++ websocket_js.go | 62 ++++++++++++++++++++++++++++++++++++++----------- 3 files changed, 54 insertions(+), 20 deletions(-) diff --git a/conn.go b/conn.go index 20dbece2..3d7d574e 100644 --- a/conn.go +++ b/conn.go @@ -120,12 +120,6 @@ func (c *Conn) Subprotocol() string { return c.subprotocol } -func (c *Conn) setCloseErr(err error) { - c.closeErrOnce.Do(func() { - c.closeErr = fmt.Errorf("websocket closed: %w", err) - }) -} - func (c *Conn) close(err error) { c.closeOnce.Do(func() { runtime.SetFinalizer(c, nil) diff --git a/conn_common.go b/conn_common.go index 1429b47d..ae0fe554 100644 --- a/conn_common.go +++ b/conn_common.go @@ -202,3 +202,9 @@ func (c *Conn) CloseRead(ctx context.Context) context.Context { func (c *Conn) SetReadLimit(n int64) { c.msgReadLimit = n } + +func (c *Conn) setCloseErr(err error) { + c.closeErrOnce.Do(func() { + c.closeErr = fmt.Errorf("websocket closed: %w", err) + }) +} diff --git a/websocket_js.go b/websocket_js.go index 4ed49d97..3822797b 100644 --- a/websocket_js.go +++ b/websocket_js.go @@ -23,29 +23,32 @@ type Conn struct { msgReadLimit int64 - readClosed int64 - closeOnce sync.Once - closed chan struct{} - closeErr error + readClosed int64 + closeOnce sync.Once + closed chan struct{} + closeErrOnce sync.Once + closeErr error releaseOnClose func() releaseOnMessage func() - readch chan wsjs.MessageEvent + readSignal chan struct{} + readBufMu sync.Mutex + readBuf []wsjs.MessageEvent } func (c *Conn) close(err error) { c.closeOnce.Do(func() { runtime.SetFinalizer(c, nil) - c.closeErr = fmt.Errorf("websocket closed: %w", err) + c.setCloseErr(err) close(c.closed) }) } func (c *Conn) init() { c.closed = make(chan struct{}) - c.readch = make(chan wsjs.MessageEvent, 1) + c.readSignal = make(chan struct{}, 1) c.msgReadLimit = 32768 c.releaseOnClose = c.ws.OnClose(func(e wsjs.CloseEvent) { @@ -61,15 +64,28 @@ func (c *Conn) init() { }) c.releaseOnMessage = c.ws.OnMessage(func(e wsjs.MessageEvent) { - c.readch <- e + c.readBufMu.Lock() + defer c.readBufMu.Unlock() + + c.readBuf = append(c.readBuf, e) + + // Lets the read goroutine know there is definitely something in readBuf. + select { + case c.readSignal <- struct{}{}: + default: + } }) runtime.SetFinalizer(c, func(c *Conn) { - c.ws.Close(int(StatusInternalError), "") - c.close(errors.New("connection garbage collected")) + c.setCloseErr(errors.New("connection garbage collected")) + c.closeWithInternal() }) } +func (c *Conn) closeWithInternal() { + c.Close(StatusInternalError, "something went wrong") +} + // Read attempts to read a message from the connection. // The maximum time spent waiting is bounded by the context. func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { @@ -89,16 +105,32 @@ func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { } func (c *Conn) read(ctx context.Context) (MessageType, []byte, error) { - var me wsjs.MessageEvent select { case <-ctx.Done(): c.Close(StatusPolicyViolation, "read timed out") return 0, nil, ctx.Err() - case me = <-c.readch: + case <-c.readSignal: case <-c.closed: return 0, nil, c.closeErr } + c.readBufMu.Lock() + defer c.readBufMu.Unlock() + + me := c.readBuf[0] + // We copy the messages forward and decrease the size + // of the slice to avoid reallocating. + copy(c.readBuf, c.readBuf[1:]) + c.readBuf = c.readBuf[:len(c.readBuf)-1] + + if len(c.readBuf) > 0 { + // Next time we read, we'll grab the message. + select { + case c.readSignal <- struct{}{}: + default: + } + } + switch p := me.Data.(type) { case string: return MessageText, []byte(p), nil @@ -118,8 +150,10 @@ func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { // to match the Go API. It can only error if the message type // is unexpected or the passed bytes contain invalid UTF-8 for // MessageText. - c.Close(StatusInternalError, "something went wrong") - return fmt.Errorf("failed to write: %w", err) + err := fmt.Errorf("failed to write: %w", err) + c.setCloseErr(err) + c.closeWithInternal() + return err } return nil } From 1fcb39cf49cd48b6a16dcbcaf5cce5aeb9fa0bb2 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 24 Sep 2019 13:56:23 -0500 Subject: [PATCH 152/519] Mention how mature gorilla/websocket is in comparison --- README.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index c5b8c907..a9149bff 100644 --- a/README.md +++ b/README.md @@ -107,9 +107,11 @@ production. https://github.com/gorilla/websocket -This package is the community standard but it is 6 years old and over time -has accumulated cruft. There are too many ways to do the same thing. -Just compare the godoc of +The implementation of gorilla/websocket is 6 years old. As such, it is +widely used and very mature compared to nhooyr.io/websocket. + +On the other hand, it has accumulated cruft over the years. There are too many ways to do +the same thing. Just compare the godoc of [nhooyr/websocket](https://godoc.org/nhooyr.io/websocket) with [gorilla/websocket](https://godoc.org/github.com/gorilla/websocket) side by side. From 5017dbf656557e89654234bbf52e60397e351ac9 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 24 Sep 2019 14:05:26 -0500 Subject: [PATCH 153/519] README.md: nhooyr/websocket -> nhooyr.io/websocket --- README.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index a9149bff..02cb726a 100644 --- a/README.md +++ b/README.md @@ -115,30 +115,30 @@ the same thing. Just compare the godoc of [nhooyr/websocket](https://godoc.org/nhooyr.io/websocket) with [gorilla/websocket](https://godoc.org/github.com/gorilla/websocket) side by side. -The API for nhooyr/websocket has been designed such that there is only one way to do things. +The API for nhooyr.io/websocket has been designed such that there is only one way to do things. This makes it easy to use correctly. Not only is the API simpler, the implementation is only 2200 lines whereas gorilla/websocket is at 3500 lines. That's more code to maintain, more code to test, more code to document and more surface area for bugs. -Moreover, nhooyr/websocket supports newer Go idioms such as context.Context. +Moreover, nhooyr.io/websocket supports newer Go idioms such as context.Context. It also uses net/http's Client and ResponseWriter directly for WebSocket handshakes. gorilla/websocket writes its handshakes to the underlying net.Conn. Thus it has to reinvent hooks for TLS and proxies and prevents support of HTTP/2. -Some more advantages of nhooyr/websocket are that it supports concurrent writes and +Some more advantages of nhooyr.io/websocket are that it supports concurrent writes and makes it very easy to close the connection with a status code and reason. The ping API is also nicer. gorilla/websocket requires registering a pong handler on the Conn -which results in awkward control flow. With nhooyr/websocket you use the Ping method on the Conn +which results in awkward control flow. With nhooyr.io/websocket you use the Ping method on the Conn that sends a ping and also waits for the pong. Additionally, nhooyr.io/websocket can compile to [Wasm](https://godoc.org/nhooyr.io/websocket#hdr-Wasm) for the browser. -In terms of performance, the differences mostly depend on your application code. nhooyr/websocket +In terms of performance, the differences mostly depend on your application code. nhooyr.io/websocket reuses message buffers out of the box if you use the wsjson and wspb subpackages. -As mentioned above, nhooyr/websocket also supports concurrent writers. +As mentioned above, nhooyr.io/websocket also supports concurrent writers. -The only performance con to nhooyr/websocket is that it uses one extra goroutine to support +The only performance con to nhooyr.io/websocket is that it uses one extra goroutine to support cancellation with context.Context. This costs 2 KB of memory which is cheap compared to the benefits. @@ -159,11 +159,11 @@ and clarity. This library is fantastic in terms of performance. The author put in significant effort to ensure its speed and I have applied as many of its optimizations as -I could into nhooyr/websocket. Definitely check out his fantastic [blog post](https://medium.freecodecamp.org/million-websockets-and-go-cc58418460bb) +I could into nhooyr.io/websocket. Definitely check out his fantastic [blog post](https://medium.freecodecamp.org/million-websockets-and-go-cc58418460bb) about performant WebSocket servers. If you want a library that gives you absolute control over everything, this is the library. -But for 99.9% of use cases, nhooyr/websocket will fit better. It's nearly as performant +But for 99.9% of use cases, nhooyr.io/websocket will fit better. It's nearly as performant but much easier to use. ## Contributing From 155bca6a2f74f0cf3ceb83325113c0e8b3e62cee Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 25 Sep 2019 10:29:25 -0500 Subject: [PATCH 154/519] Link to comparison table in gorilla/websocket --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 02cb726a..55d496e9 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,8 @@ authors of both. In particular, I made sure to go through the issue tracker of g to ensure I implemented details correctly and understood how people were using WebSockets in production. +Another comparison between Go WebSocket libraries is available at [gorilla/websocket](https://github.com/gorilla/websocket#gorilla-websocket-compared-with-other-packages). + ### gorilla/websocket https://github.com/gorilla/websocket From 0e007607e7d514188b3a7d86c336e2c3bf880fc0 Mon Sep 17 00:00:00 2001 From: Oleg Kovalov Date: Thu, 26 Sep 2019 08:33:09 +0200 Subject: [PATCH 155/519] Invert if to the more consistent style --- handshake.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/handshake.go b/handshake.go index c55dd463..0b078085 100644 --- a/handshake.go +++ b/handshake.go @@ -221,10 +221,10 @@ func authenticateOrigin(r *http.Request) error { if err != nil { return fmt.Errorf("failed to parse Origin header %q: %w", origin, err) } - if strings.EqualFold(u.Host, r.Host) { - return nil + if !strings.EqualFold(u.Host, r.Host) { + return fmt.Errorf("request Origin %q is not authorized for Host %q", origin, r.Host) } - return fmt.Errorf("request Origin %q is not authorized for Host %q", origin, r.Host) + return nil } // DialOptions represents the options available to pass to Dial. From ff63b191ded94e60736449335780d05025fd0dc7 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 27 Sep 2019 13:39:17 -0500 Subject: [PATCH 156/519] Fix unaligned 64 bit atomic loads on 32 bit platforms Closes #153 --- conn.go | 5 +++-- conn_common.go | 2 +- websocket_js.go | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/conn.go b/conn.go index 3d7d574e..2d0339e4 100644 --- a/conn.go +++ b/conn.go @@ -70,7 +70,8 @@ type Conn struct { activeReader *messageReader // readFrameLock is acquired to read from bw. readFrameLock chan struct{} - readClosed int64 + // Not int32 because of https://github.com/nhooyr/websocket/issues/153 + readClosed int32 readHeaderBuf []byte controlPayloadBuf []byte @@ -341,7 +342,7 @@ func (c *Conn) handleControl(ctx context.Context, h header) error { // See https://github.com/nhooyr/websocket/issues/87#issue-451703332 // Most users should not need this. func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) { - if atomic.LoadInt64(&c.readClosed) == 1 { + if atomic.LoadInt32(&c.readClosed) == 1 { return 0, nil, fmt.Errorf("websocket connection read closed") } diff --git a/conn_common.go b/conn_common.go index ae0fe554..9a4f9043 100644 --- a/conn_common.go +++ b/conn_common.go @@ -178,7 +178,7 @@ func (c *netConn) SetReadDeadline(t time.Time) error { // Use this when you do not want to read data messages from the connection anymore but will // want to write messages to it. func (c *Conn) CloseRead(ctx context.Context) context.Context { - atomic.StoreInt64(&c.readClosed, 1) + atomic.StoreInt32(&c.readClosed, 1) ctx, cancel := context.WithCancel(ctx) go func() { diff --git a/websocket_js.go b/websocket_js.go index 3822797b..dcb02061 100644 --- a/websocket_js.go +++ b/websocket_js.go @@ -89,7 +89,7 @@ func (c *Conn) closeWithInternal() { // Read attempts to read a message from the connection. // The maximum time spent waiting is bounded by the context. func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { - if atomic.LoadInt64(&c.readClosed) == 1 { + if atomic.LoadInt32(&c.readClosed) == 1 { return 0, nil, fmt.Errorf("websocket connection read closed") } From 4b51f4a8c995a1cf2bd4aaec3a54c519d9c3ea36 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 27 Sep 2019 14:05:11 -0500 Subject: [PATCH 157/519] Remove atomic integer loads and stores in favour of atomic.Value - Also allows SetReadLimit to be called concurrently which is a nice touch --- ci/wasm.sh | 2 ++ conn.go | 16 ++++++++-------- conn_common.go | 25 +++++++++++++++++++++++-- websocket_js.go | 16 ++++++++++------ 4 files changed, 43 insertions(+), 16 deletions(-) diff --git a/ci/wasm.sh b/ci/wasm.sh index 0290f188..1497ba24 100755 --- a/ci/wasm.sh +++ b/ci/wasm.sh @@ -26,5 +26,7 @@ GOOS=js GOARCH=wasm go test -exec=wasmbrowsertest ./... -args "$WS_ECHO_SERVER_U if ! wait "$wsjstestPID"; then echo "wsjstest exited unsuccessfully" + echo "output:" + cat "$wsjstestOut" exit 1 fi diff --git a/conn.go b/conn.go index 2d0339e4..2679edcc 100644 --- a/conn.go +++ b/conn.go @@ -20,8 +20,7 @@ import ( ) // Conn represents a WebSocket connection. -// All methods may be called concurrently except for Reader, Read -// and SetReadLimit. +// All methods may be called concurrently except for Reader and Read. // // You must always read from the connection. Otherwise control // frames will not be handled. See the docs on Reader and CloseRead. @@ -56,7 +55,7 @@ type Conn struct { writeHeaderBuf []byte writeHeader *header // read limit for a message in bytes. - msgReadLimit int64 + msgReadLimit *atomicInt64 // Used to ensure a previous writer is not used after being closed. activeWriter atomic.Value @@ -70,8 +69,7 @@ type Conn struct { activeReader *messageReader // readFrameLock is acquired to read from bw. readFrameLock chan struct{} - // Not int32 because of https://github.com/nhooyr/websocket/issues/153 - readClosed int32 + readClosed *atomicInt64 readHeaderBuf []byte controlPayloadBuf []byte @@ -91,7 +89,8 @@ type Conn struct { func (c *Conn) init() { c.closed = make(chan struct{}) - c.msgReadLimit = 32768 + c.msgReadLimit = &atomicInt64{} + c.msgReadLimit.Store(32768) c.writeMsgLock = make(chan struct{}, 1) c.writeFrameLock = make(chan struct{}, 1) @@ -106,6 +105,7 @@ func (c *Conn) init() { c.writeHeaderBuf = makeWriteHeaderBuf() c.writeHeader = &header{} c.readHeaderBuf = makeReadHeaderBuf() + c.readClosed = &atomicInt64{} c.controlPayloadBuf = make([]byte, maxControlFramePayload) runtime.SetFinalizer(c, func(c *Conn) { @@ -342,7 +342,7 @@ func (c *Conn) handleControl(ctx context.Context, h header) error { // See https://github.com/nhooyr/websocket/issues/87#issue-451703332 // Most users should not need this. func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) { - if atomic.LoadInt32(&c.readClosed) == 1 { + if c.readClosed.Load() == 1 { return 0, nil, fmt.Errorf("websocket connection read closed") } @@ -392,7 +392,7 @@ func (c *Conn) reader(ctx context.Context) (MessageType, io.Reader, error) { c.readerMsgHeader = h c.readerFrameEOF = false c.readerMaskPos = 0 - c.readMsgLeft = c.msgReadLimit + c.readMsgLeft = c.msgReadLimit.Load() r := &messageReader{ c: c, diff --git a/conn_common.go b/conn_common.go index 9a4f9043..47146110 100644 --- a/conn_common.go +++ b/conn_common.go @@ -178,7 +178,7 @@ func (c *netConn) SetReadDeadline(t time.Time) error { // Use this when you do not want to read data messages from the connection anymore but will // want to write messages to it. func (c *Conn) CloseRead(ctx context.Context) context.Context { - atomic.StoreInt32(&c.readClosed, 1) + c.readClosed.Store(1) ctx, cancel := context.WithCancel(ctx) go func() { @@ -200,7 +200,7 @@ func (c *Conn) CloseRead(ctx context.Context) context.Context { // // When the limit is hit, the connection will be closed with StatusMessageTooBig. func (c *Conn) SetReadLimit(n int64) { - c.msgReadLimit = n + c.msgReadLimit.Store(n) } func (c *Conn) setCloseErr(err error) { @@ -208,3 +208,24 @@ func (c *Conn) setCloseErr(err error) { c.closeErr = fmt.Errorf("websocket closed: %w", err) }) } + +// See https://github.com/nhooyr/websocket/issues/153 +type atomicInt64 struct { + v atomic.Value +} + +func (v *atomicInt64) Load() int64 { + i, ok := v.v.Load().(int64) + if !ok { + return 0 + } + return i +} + +func (v *atomicInt64) Store(i int64) { + v.v.Store(i) +} + +func (v *atomicInt64) String() string { + return fmt.Sprint(v.v.Load()) +} diff --git a/websocket_js.go b/websocket_js.go index dcb02061..2226d3a4 100644 --- a/websocket_js.go +++ b/websocket_js.go @@ -10,7 +10,6 @@ import ( "reflect" "runtime" "sync" - "sync/atomic" "syscall/js" "nhooyr.io/websocket/internal/bpool" @@ -21,9 +20,10 @@ import ( type Conn struct { ws wsjs.WebSocket - msgReadLimit int64 + // read limit for a message in bytes. + msgReadLimit *atomicInt64 - readClosed int64 + readClosed *atomicInt64 closeOnce sync.Once closed chan struct{} closeErrOnce sync.Once @@ -49,7 +49,11 @@ func (c *Conn) close(err error) { func (c *Conn) init() { c.closed = make(chan struct{}) c.readSignal = make(chan struct{}, 1) - c.msgReadLimit = 32768 + + c.msgReadLimit = &atomicInt64{} + c.msgReadLimit.Store(32768) + + c.readClosed = &atomicInt64{} c.releaseOnClose = c.ws.OnClose(func(e wsjs.CloseEvent) { cerr := CloseError{ @@ -89,7 +93,7 @@ func (c *Conn) closeWithInternal() { // Read attempts to read a message from the connection. // The maximum time spent waiting is bounded by the context. func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { - if atomic.LoadInt32(&c.readClosed) == 1 { + if c.readClosed.Load() == 1 { return 0, nil, fmt.Errorf("websocket connection read closed") } @@ -97,7 +101,7 @@ func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { if err != nil { return 0, nil, fmt.Errorf("failed to read: %w", err) } - if int64(len(p)) > c.msgReadLimit { + if int64(len(p)) > c.msgReadLimit.Load() { c.Close(StatusMessageTooBig, fmt.Sprintf("read limited at %v bytes", c.msgReadLimit)) return 0, nil, c.closeErr } From b371dcc009d83fa39d7358b41817904c4bbcc3b2 Mon Sep 17 00:00:00 2001 From: Oleg Kovalov Date: Fri, 27 Sep 2019 15:30:11 -0500 Subject: [PATCH 158/519] Rename readClosed to isReadClosed Closes #150 --- conn.go | 8 ++++---- conn_common.go | 2 +- websocket_js.go | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/conn.go b/conn.go index 2679edcc..37c4cac2 100644 --- a/conn.go +++ b/conn.go @@ -55,7 +55,7 @@ type Conn struct { writeHeaderBuf []byte writeHeader *header // read limit for a message in bytes. - msgReadLimit *atomicInt64 + msgReadLimit *atomicInt64 // Used to ensure a previous writer is not used after being closed. activeWriter atomic.Value @@ -69,7 +69,7 @@ type Conn struct { activeReader *messageReader // readFrameLock is acquired to read from bw. readFrameLock chan struct{} - readClosed *atomicInt64 + isReadClosed *atomicInt64 readHeaderBuf []byte controlPayloadBuf []byte @@ -105,7 +105,7 @@ func (c *Conn) init() { c.writeHeaderBuf = makeWriteHeaderBuf() c.writeHeader = &header{} c.readHeaderBuf = makeReadHeaderBuf() - c.readClosed = &atomicInt64{} + c.isReadClosed = &atomicInt64{} c.controlPayloadBuf = make([]byte, maxControlFramePayload) runtime.SetFinalizer(c, func(c *Conn) { @@ -342,7 +342,7 @@ func (c *Conn) handleControl(ctx context.Context, h header) error { // See https://github.com/nhooyr/websocket/issues/87#issue-451703332 // Most users should not need this. func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) { - if c.readClosed.Load() == 1 { + if c.isReadClosed.Load() == 1 { return 0, nil, fmt.Errorf("websocket connection read closed") } diff --git a/conn_common.go b/conn_common.go index 47146110..e7a01035 100644 --- a/conn_common.go +++ b/conn_common.go @@ -178,7 +178,7 @@ func (c *netConn) SetReadDeadline(t time.Time) error { // Use this when you do not want to read data messages from the connection anymore but will // want to write messages to it. func (c *Conn) CloseRead(ctx context.Context) context.Context { - c.readClosed.Store(1) + c.isReadClosed.Store(1) ctx, cancel := context.WithCancel(ctx) go func() { diff --git a/websocket_js.go b/websocket_js.go index 2226d3a4..d70ccfca 100644 --- a/websocket_js.go +++ b/websocket_js.go @@ -23,7 +23,7 @@ type Conn struct { // read limit for a message in bytes. msgReadLimit *atomicInt64 - readClosed *atomicInt64 + isReadClosed *atomicInt64 closeOnce sync.Once closed chan struct{} closeErrOnce sync.Once @@ -53,7 +53,7 @@ func (c *Conn) init() { c.msgReadLimit = &atomicInt64{} c.msgReadLimit.Store(32768) - c.readClosed = &atomicInt64{} + c.isReadClosed = &atomicInt64{} c.releaseOnClose = c.ws.OnClose(func(e wsjs.CloseEvent) { cerr := CloseError{ @@ -93,7 +93,7 @@ func (c *Conn) closeWithInternal() { // Read attempts to read a message from the connection. // The maximum time spent waiting is bounded by the context. func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { - if c.readClosed.Load() == 1 { + if c.isReadClosed.Load() == 1 { return 0, nil, fmt.Errorf("websocket connection read closed") } From 71d69debbd52697fc2704f8069e0a6b3ea32a6f4 Mon Sep 17 00:00:00 2001 From: Oleg Kovalov Date: Fri, 27 Sep 2019 15:34:39 -0500 Subject: [PATCH 159/519] Improve godoc for status codes Closes #151 --- frame.go | 47 ++++++++++++++++++++++++++--------------------- frame_stringer.go | 24 +++++++----------------- 2 files changed, 33 insertions(+), 38 deletions(-) diff --git a/frame.go b/frame.go index 95061f5a..f96b467b 100644 --- a/frame.go +++ b/frame.go @@ -203,32 +203,37 @@ type StatusCode int // These codes were retrieved from: // https://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number +// +// In addition to the defined constants, 4000-4999 may be used by applications. const ( - StatusNormalClosure StatusCode = 1000 + iota - StatusGoingAway - StatusProtocolError - StatusUnsupportedData + StatusNormalClosure StatusCode = 1000 + StatusGoingAway StatusCode = 1001 + StatusProtocolError StatusCode = 1002 + StatusUnsupportedData StatusCode = 1003 - _ // 1004 is reserved. + // 1004 is reserved and so not exported. + statusReserved StatusCode = 1004 - StatusNoStatusRcvd + // StatusNoStatusRcvd cannot be sent as reserved for when + // a close message is received without an explicit status. + StatusNoStatusRcvd StatusCode = 1005 - // This StatusCode is only exported for use with Wasm. + // StatusAbnormalClosure is only exported for use with Wasm. // In non Wasm Go, the returned error will indicate whether the connection was closed or not or what happened. - StatusAbnormalClosure - - StatusInvalidFramePayloadData - StatusPolicyViolation - StatusMessageTooBig - StatusMandatoryExtension - StatusInternalError - StatusServiceRestart - StatusTryAgainLater - StatusBadGateway - - // This StatusCode is only exported for use with Wasm. + StatusAbnormalClosure StatusCode = 1006 + + StatusInvalidFramePayloadData StatusCode = 1007 + StatusPolicyViolation StatusCode = 1008 + StatusMessageTooBig StatusCode = 1009 + StatusMandatoryExtension StatusCode = 1010 + StatusInternalError StatusCode = 1011 + StatusServiceRestart StatusCode = 1012 + StatusTryAgainLater StatusCode = 1013 + StatusBadGateway StatusCode = 1014 + + // StatusTLSHandshake is only exported for use with Wasm. // In non Wasm Go, the returned error will indicate whether there was a TLS handshake failure. - StatusTLSHandshake + StatusTLSHandshake StatusCode = 1015 ) // CloseError represents a WebSocket close frame. @@ -272,7 +277,7 @@ func parseClosePayload(p []byte) (CloseError, error) { // and https://tools.ietf.org/html/rfc6455#section-7.4.1 func validWireCloseCode(code StatusCode) bool { switch code { - case 1004, StatusNoStatusRcvd, StatusAbnormalClosure, StatusTLSHandshake: + case statusReserved, StatusNoStatusRcvd, StatusAbnormalClosure, StatusTLSHandshake: return false } diff --git a/frame_stringer.go b/frame_stringer.go index e60e53fa..72b865fc 100644 --- a/frame_stringer.go +++ b/frame_stringer.go @@ -64,6 +64,7 @@ func _() { _ = x[StatusGoingAway-1001] _ = x[StatusProtocolError-1002] _ = x[StatusUnsupportedData-1003] + _ = x[statusReserved-1004] _ = x[StatusNoStatusRcvd-1005] _ = x[StatusAbnormalClosure-1006] _ = x[StatusInvalidFramePayloadData-1007] @@ -77,25 +78,14 @@ func _() { _ = x[StatusTLSHandshake-1015] } -const ( - _StatusCode_name_0 = "StatusNormalClosureStatusGoingAwayStatusProtocolErrorStatusUnsupportedData" - _StatusCode_name_1 = "StatusNoStatusRcvdStatusAbnormalClosureStatusInvalidFramePayloadDataStatusPolicyViolationStatusMessageTooBigStatusMandatoryExtensionStatusInternalErrorStatusServiceRestartStatusTryAgainLaterStatusBadGatewayStatusTLSHandshake" -) +const _StatusCode_name = "StatusNormalClosureStatusGoingAwayStatusProtocolErrorStatusUnsupportedDatastatusReservedStatusNoStatusRcvdStatusAbnormalClosureStatusInvalidFramePayloadDataStatusPolicyViolationStatusMessageTooBigStatusMandatoryExtensionStatusInternalErrorStatusServiceRestartStatusTryAgainLaterStatusBadGatewayStatusTLSHandshake" -var ( - _StatusCode_index_0 = [...]uint8{0, 19, 34, 53, 74} - _StatusCode_index_1 = [...]uint8{0, 18, 39, 68, 89, 108, 132, 151, 171, 190, 206, 224} -) +var _StatusCode_index = [...]uint16{0, 19, 34, 53, 74, 88, 106, 127, 156, 177, 196, 220, 239, 259, 278, 294, 312} func (i StatusCode) String() string { - switch { - case 1000 <= i && i <= 1003: - i -= 1000 - return _StatusCode_name_0[_StatusCode_index_0[i]:_StatusCode_index_0[i+1]] - case 1005 <= i && i <= 1015: - i -= 1005 - return _StatusCode_name_1[_StatusCode_index_1[i]:_StatusCode_index_1[i+1]] - default: - return "StatusCode(" + strconv.FormatInt(int64(i), 10) + ")" + i -= 1000 + if i < 0 || i >= StatusCode(len(_StatusCode_index)-1) { + return "StatusCode(" + strconv.FormatInt(int64(i+1000), 10) + ")" } + return _StatusCode_name[_StatusCode_index[i]:_StatusCode_index[i+1]] } From a5e4e906581602f0dbac22ca67098a4f9671b815 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 27 Sep 2019 15:35:43 -0500 Subject: [PATCH 160/519] Improve StatusCode godoc --- frame.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/frame.go b/frame.go index f96b467b..f8f5ec54 100644 --- a/frame.go +++ b/frame.go @@ -204,7 +204,8 @@ type StatusCode int // These codes were retrieved from: // https://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number // -// In addition to the defined constants, 4000-4999 may be used by applications. +// In addition to the defined constants, 4000-4999 are reserved for arbitrary +// use by applications. const ( StatusNormalClosure StatusCode = 1000 StatusGoingAway StatusCode = 1001 From f8cc5a87320ad00c1b328f9cc9f802ad02040da0 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 27 Sep 2019 15:37:23 -0500 Subject: [PATCH 161/519] Improve status code godoc further --- frame.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frame.go b/frame.go index f8f5ec54..a12438ce 100644 --- a/frame.go +++ b/frame.go @@ -204,8 +204,8 @@ type StatusCode int // These codes were retrieved from: // https://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number // -// In addition to the defined constants, 4000-4999 are reserved for arbitrary -// use by applications. +// The defined constants only represent the status codes registered with IANA. +// The 4000-4999 range of status codes is reserved for arbitrary use by applications. const ( StatusNormalClosure StatusCode = 1000 StatusGoingAway StatusCode = 1001 From 181b36eb7eef7bf6681bddeb59bfed495d90e098 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 27 Sep 2019 15:40:54 -0500 Subject: [PATCH 162/519] More status code godoc improvements --- frame.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/frame.go b/frame.go index a12438ce..4b170c5f 100644 --- a/frame.go +++ b/frame.go @@ -215,8 +215,9 @@ const ( // 1004 is reserved and so not exported. statusReserved StatusCode = 1004 - // StatusNoStatusRcvd cannot be sent as reserved for when - // a close message is received without an explicit status. + // StatusNoStatusRcvd cannot be sent in a close message. + // It is reserved for when a close message is received without + // an explicit status. StatusNoStatusRcvd StatusCode = 1005 // StatusAbnormalClosure is only exported for use with Wasm. From 9b8320edf62c8b83fa8fd7a2cf7c4de4abf99184 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 27 Sep 2019 17:32:51 -0500 Subject: [PATCH 163/519] Remove fifo from wasm.sh Should prevent the sporadic failures in CI. --- ci/wasm.sh | 10 +++++----- internal/wsjstest/main.go | 1 + 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/ci/wasm.sh b/ci/wasm.sh index 1497ba24..d25a0750 100755 --- a/ci/wasm.sh +++ b/ci/wasm.sh @@ -9,13 +9,13 @@ GOOS=js GOARCH=wasm go vet ./... go install golang.org/x/lint/golint GOOS=js GOARCH=wasm golint -set_exit_status ./... -wsjstestOut="$(mktemp -d)/stdout" -mkfifo "$wsjstestOut" +wsjstestOut="$(mktemp)" go install ./internal/wsjstest -timeout 30s wsjstest > "$wsjstestOut" & +timeout 30s wsjstest >> "$wsjstestOut" & wsjstestPID=$! -WS_ECHO_SERVER_URL="$(timeout 10s head -n 1 "$wsjstestOut")" || true +# See https://superuser.com/a/900134 +WS_ECHO_SERVER_URL="$( (tail -f -n0 "$wsjstestOut" &) | timeout 10s head -n 1)" if [[ -z $WS_ECHO_SERVER_URL ]]; then echo "./internal/wsjstest failed to start in 10s" exit 1 @@ -25,7 +25,7 @@ go install github.com/agnivade/wasmbrowsertest GOOS=js GOARCH=wasm go test -exec=wasmbrowsertest ./... -args "$WS_ECHO_SERVER_URL" if ! wait "$wsjstestPID"; then - echo "wsjstest exited unsuccessfully" + echo "--- wsjstest exited unsuccessfully" echo "output:" cat "$wsjstestOut" exit 1 diff --git a/internal/wsjstest/main.go b/internal/wsjstest/main.go index a1ad1b02..9aeb9b14 100644 --- a/internal/wsjstest/main.go +++ b/internal/wsjstest/main.go @@ -36,6 +36,7 @@ func main() { os.Exit(0) })) + wsURL := strings.Replace(s.URL, "http", "ws", 1) fmt.Printf("%v\n", wsURL) From 0cda1c3479d39082e62cf950e5fe027a24bb2011 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 27 Sep 2019 17:51:38 -0500 Subject: [PATCH 164/519] Fix race in wasm test --- ci/wasm.sh | 3 ++- export_test.go => conn_export_test.go | 0 internal/wsjstest/main.go | 10 ++++++---- websocket_js.go | 7 +++++++ websocket_js_export_test.go | 17 +++++++++++++++++ websocket_js_test.go | 5 +++++ 6 files changed, 37 insertions(+), 5 deletions(-) rename export_test.go => conn_export_test.go (100%) create mode 100644 websocket_js_export_test.go diff --git a/ci/wasm.sh b/ci/wasm.sh index d25a0750..a780b63f 100755 --- a/ci/wasm.sh +++ b/ci/wasm.sh @@ -11,7 +11,7 @@ GOOS=js GOARCH=wasm golint -set_exit_status ./... wsjstestOut="$(mktemp)" go install ./internal/wsjstest -timeout 30s wsjstest >> "$wsjstestOut" & +timeout 30s wsjstest >> "$wsjstestOut" 2>&1 & wsjstestPID=$! # See https://superuser.com/a/900134 @@ -24,6 +24,7 @@ fi go install github.com/agnivade/wasmbrowsertest GOOS=js GOARCH=wasm go test -exec=wasmbrowsertest ./... -args "$WS_ECHO_SERVER_URL" +kill "$wsjstestPID" if ! wait "$wsjstestPID"; then echo "--- wsjstest exited unsuccessfully" echo "output:" diff --git a/export_test.go b/conn_export_test.go similarity index 100% rename from export_test.go rename to conn_export_test.go diff --git a/internal/wsjstest/main.go b/internal/wsjstest/main.go index 9aeb9b14..8145061b 100644 --- a/internal/wsjstest/main.go +++ b/internal/wsjstest/main.go @@ -9,8 +9,9 @@ import ( "net/http" "net/http/httptest" "os" - "runtime" + "os/signal" "strings" + "syscall" "nhooyr.io/websocket" "nhooyr.io/websocket/internal/wsecho" @@ -33,12 +34,13 @@ func main() { if !errors.As(err, &ce) || ce.Code != websocket.StatusNormalClosure { log.Fatalf("unexpected loop error: %+v", err) } - - os.Exit(0) })) wsURL := strings.Replace(s.URL, "http", "ws", 1) fmt.Printf("%v\n", wsURL) - runtime.Goexit() + sigs := make(chan os.Signal) + signal.Notify(sigs, syscall.SIGTERM) + + <-sigs } diff --git a/websocket_js.go b/websocket_js.go index d70ccfca..05fafe2a 100644 --- a/websocket_js.go +++ b/websocket_js.go @@ -35,6 +35,9 @@ type Conn struct { readSignal chan struct{} readBufMu sync.Mutex readBuf []wsjs.MessageEvent + + // Only used by tests + receivedCloseFrame chan struct{} } func (c *Conn) close(err error) { @@ -55,7 +58,11 @@ func (c *Conn) init() { c.isReadClosed = &atomicInt64{} + c.receivedCloseFrame = make(chan struct{}) + c.releaseOnClose = c.ws.OnClose(func(e wsjs.CloseEvent) { + close(c.receivedCloseFrame) + cerr := CloseError{ Code: StatusCode(e.Code), Reason: e.Reason, diff --git a/websocket_js_export_test.go b/websocket_js_export_test.go new file mode 100644 index 00000000..462c99d3 --- /dev/null +++ b/websocket_js_export_test.go @@ -0,0 +1,17 @@ +// +build js + +package websocket + +import ( + "context" + "fmt" +) + +func (c *Conn) WaitCloseFrame(ctx context.Context) error { + select { + case <-c.receivedCloseFrame: + return nil + case <-ctx.Done(): + return fmt.Errorf("failed to wait for close frame: %w", ctx.Err()) + } +} diff --git a/websocket_js_test.go b/websocket_js_test.go index ba9431d4..8e429969 100644 --- a/websocket_js_test.go +++ b/websocket_js_test.go @@ -50,4 +50,9 @@ func TestConn(t *testing.T) { if err != nil { t.Fatal(err) } + + err = c.WaitCloseFrame(ctx) + if err != nil { + t.Fatal(err) + } } From 58864f67038331e6bb7bd519263caaa01e07b039 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 28 Sep 2019 00:10:52 -0500 Subject: [PATCH 165/519] Allow codecov patch status to fail Very annoying. --- ci/.codecov.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/.codecov.yml b/ci/.codecov.yml index 5ede9ea1..fa7c5f0a 100644 --- a/ci/.codecov.yml +++ b/ci/.codecov.yml @@ -4,7 +4,7 @@ coverage: # Prevent small changes in coverage from failing CI. project: default: - threshold: 10 + threshold: 15% patch: default: - threshold: 50 + threshold: 100% From c755bc88609d040b249995d66b28ad514f7782ac Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 28 Sep 2019 19:10:50 -0500 Subject: [PATCH 166/519] Improve comparison to gorilla --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 55d496e9..c9770e4a 100644 --- a/README.md +++ b/README.md @@ -112,8 +112,8 @@ https://github.com/gorilla/websocket The implementation of gorilla/websocket is 6 years old. As such, it is widely used and very mature compared to nhooyr.io/websocket. -On the other hand, it has accumulated cruft over the years. There are too many ways to do -the same thing. Just compare the godoc of +On the other hand, it has grown organically and now there are too many ways to do +the same thing. Compare the godoc of [nhooyr/websocket](https://godoc.org/nhooyr.io/websocket) with [gorilla/websocket](https://godoc.org/github.com/gorilla/websocket) side by side. From a62514451dd0f14e1a3d544238fc8b4314cd0bb8 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 29 Sep 2019 12:32:26 -0500 Subject: [PATCH 167/519] Fix WASM Subprotocol selection Subprotocols didn't work with WASM at all before. --- README.md | 2 -- ci/wasm.sh | 3 ++- go.mod | 9 +++++++-- go.sum | 20 ++++++++++++++++---- internal/wsjs/wsjs_js.go | 13 +++++-------- websocket_js.go | 2 +- websocket_js_test.go | 8 +++----- 7 files changed, 34 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index c9770e4a..3ab395e6 100644 --- a/README.md +++ b/README.md @@ -103,8 +103,6 @@ authors of both. In particular, I made sure to go through the issue tracker of g to ensure I implemented details correctly and understood how people were using WebSockets in production. -Another comparison between Go WebSocket libraries is available at [gorilla/websocket](https://github.com/gorilla/websocket#gorilla-websocket-compared-with-other-packages). - ### gorilla/websocket https://github.com/gorilla/websocket diff --git a/ci/wasm.sh b/ci/wasm.sh index a780b63f..134b60b5 100755 --- a/ci/wasm.sh +++ b/ci/wasm.sh @@ -22,7 +22,8 @@ if [[ -z $WS_ECHO_SERVER_URL ]]; then fi go install github.com/agnivade/wasmbrowsertest -GOOS=js GOARCH=wasm go test -exec=wasmbrowsertest ./... -args "$WS_ECHO_SERVER_URL" +export WS_ECHO_SERVER_URL +GOOS=js GOARCH=wasm go test -exec=wasmbrowsertest ./... kill "$wsjstestPID" if ! wait "$wsjstestPID"; then diff --git a/go.mod b/go.mod index 86a9403b..8bd3b887 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,17 @@ module nhooyr.io/websocket go 1.13 require ( - github.com/agnivade/wasmbrowsertest v0.3.0 + github.com/agnivade/wasmbrowsertest v0.3.1 + github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6 // indirect + github.com/chromedp/chromedp v0.4.0 // indirect github.com/fatih/color v1.7.0 // indirect + github.com/go-interpreter/wagon v0.6.0 // indirect github.com/golang/protobuf v1.3.2 github.com/google/go-cmp v0.3.1 + github.com/google/pprof v0.0.0-20190908185732-236ed259b199 // indirect github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect github.com/kr/pretty v0.1.0 // indirect + github.com/mailru/easyjson v0.7.0 // indirect github.com/mattn/go-colorable v0.1.2 // indirect github.com/mattn/go-isatty v0.0.9 // indirect github.com/pkg/errors v0.8.1 // indirect @@ -20,7 +25,7 @@ require ( go.uber.org/multierr v1.1.0 golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect - golang.org/x/sys v0.0.0-20190919044723-0c1ff786ef13 // indirect + golang.org/x/sys v0.0.0-20190927073244-c990c680b611 // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect diff --git a/go.sum b/go.sum index 4af00946..0bcfc0b4 100644 --- a/go.sum +++ b/go.sum @@ -1,10 +1,15 @@ -github.com/agnivade/wasmbrowsertest v0.3.0 h1:5pAabhWzTVCLoVWqYejEbmWyzNGFR7K/Nu5lsmD1fVc= -github.com/agnivade/wasmbrowsertest v0.3.0/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI= +github.com/agnivade/wasmbrowsertest v0.3.1 h1:bA9aA+bcp7KuqGvmCuBdnMqy6PXxFjYP7FxsaT+JSqc= +github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI= github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw= github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0 h1:4Wocv9f+KWF4GtZudyrn8JSBTgHQbGp86mcsoH7j1iQ= github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw= +github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0= +github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6 h1:C/dvU7FH/BUbkgKS/eYDrja/D5idxH+FxaVmFRWW3E8= +github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0= github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901 h1:tg66ykM8VYqP9k4DFQwSMnYv84HNTruF+GR6kefFNg4= github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM= +github.com/chromedp/chromedp v0.4.0 h1:0AJC5ejETuh/6n7Tcsw4u4G0eKZkI9aVRwckWaImLUE= +github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -19,6 +24,8 @@ github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c h1:DLLAPVFrk9iNzljMKF512CUmrFImQ6WU3sDiUS4IRqk= github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc= +github.com/go-interpreter/wagon v0.6.0 h1:BBxDxjiJiHgw9EdkYXAWs8NHhwnazZ5P2EWBW5hFNWw= +github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= @@ -33,6 +40,8 @@ github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f h1:Jnx61latede7zDD3DiiP4gmNz33uK0U5HDUaF0a/HVQ= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190908185732-236ed259b199 h1:sEyCq3pOT7tNC+3gcLI7sZkBDgntZ6wQJNmr9lmIjIc= +github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= @@ -52,6 +61,9 @@ github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481 h1:IaSjLMT6WvkoZZjspGxy3rdaTEmWLoRm49WbtVUi9sA= github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= +github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM= +github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= @@ -118,8 +130,8 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190919044723-0c1ff786ef13 h1:/zi0zzlPHWXYXrO1LjNRByFu8sdGgCkj2JLDdBIB84k= -golang.org/x/sys v0.0.0-20190919044723-0c1ff786ef13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190927073244-c990c680b611 h1:q9u40nxWT5zRClI/uU9dHCiYGottAg6Nzz4YUQyHxdA= +golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= diff --git a/internal/wsjs/wsjs_js.go b/internal/wsjs/wsjs_js.go index 68078cf2..d48691d4 100644 --- a/internal/wsjs/wsjs_js.go +++ b/internal/wsjs/wsjs_js.go @@ -43,19 +43,11 @@ func New(url string, protocols []string) (c WebSocket, err error) { c.setBinaryType("arraybuffer") - c.Extensions = c.v.Get("extensions").String() - c.Protocol = c.v.Get("protocol").String() - c.URL = c.v.Get("url").String() - return c, nil } // WebSocket is a wrapper around a javascript WebSocket object. type WebSocket struct { - Extensions string - Protocol string - URL string - v js.Value } @@ -131,6 +123,11 @@ func (c WebSocket) OnMessage(fn func(m MessageEvent)) (remove func()) { }) } +// Subprotocol returns the WebSocket subprotocol in use. +func (c WebSocket) Subprotocol() string { + return c.v.Get("protocol").String() +} + // OnOpen registers a function to be called when the websocket is opened. func (c WebSocket) OnOpen(fn func(e js.Value)) (remove func()) { return c.addEventListener("open", fn) diff --git a/websocket_js.go b/websocket_js.go index 05fafe2a..4563a1bc 100644 --- a/websocket_js.go +++ b/websocket_js.go @@ -219,7 +219,7 @@ func (c *Conn) Close(code StatusCode, reason string) error { // Subprotocol returns the negotiated subprotocol. // An empty string means the default protocol. func (c *Conn) Subprotocol() string { - return c.ws.Protocol + return c.ws.Subprotocol() } // DialOptions represents the options available to pass to Dial. diff --git a/websocket_js_test.go b/websocket_js_test.go index 8e429969..a3bb7639 100644 --- a/websocket_js_test.go +++ b/websocket_js_test.go @@ -2,8 +2,8 @@ package websocket_test import ( "context" - "flag" "net/http" + "os" "testing" "time" @@ -13,12 +13,10 @@ import ( func TestConn(t *testing.T) { t.Parallel() - wsEchoServerURL := flag.Arg(0) - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() - c, resp, err := websocket.Dial(ctx, wsEchoServerURL, &websocket.DialOptions{ + c, resp, err := websocket.Dial(ctx, os.Getenv("WS_ECHO_SERVER_URL"), &websocket.DialOptions{ Subprotocols: []string{"echo"}, }) if err != nil { @@ -26,7 +24,7 @@ func TestConn(t *testing.T) { } defer c.Close(websocket.StatusInternalError, "") - assertSubprotocol(c, "echo") + err = assertSubprotocol(c, "echo") if err != nil { t.Fatal(err) } From e4ac86d30c44c0cf4ca2791bb75f9b7eb95418fb Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 29 Sep 2019 12:43:21 -0500 Subject: [PATCH 168/519] Remove autogenerated and testing files from coverage --- ci/test.sh | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/ci/test.sh b/ci/test.sh index 1f5b5102..d7abcd1e 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -27,8 +27,12 @@ fi mkdir -p ci/out/websocket "${argv[@]}" -# Removes coverage of generated files. -grep -v _string.go < ci/out/coverage.prof > ci/out/coverage2.prof +# Removes coverage of generated/test related files. +grep -v _stringer.go < ci/out/coverage.prof > ci/out/coverage2.prof +mv ci/out/coverage2.prof ci/out/coverage.prof +grep -v wsjstest < ci/out/coverage.prof > ci/out/coverage2.prof +mv ci/out/coverage2.prof ci/out/coverage.prof +grep -v wsecho < ci/out/coverage.prof > ci/out/coverage2.prof mv ci/out/coverage2.prof ci/out/coverage.prof go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html From 9ce769a2c8e2692025403c9cb0a469fe9c6674f7 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 29 Sep 2019 13:26:07 -0500 Subject: [PATCH 169/519] Simplify test.sh script --- ci/test.sh | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/ci/test.sh b/ci/test.sh index d7abcd1e..0b8d6835 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -28,12 +28,10 @@ mkdir -p ci/out/websocket "${argv[@]}" # Removes coverage of generated/test related files. -grep -v _stringer.go < ci/out/coverage.prof > ci/out/coverage2.prof -mv ci/out/coverage2.prof ci/out/coverage.prof -grep -v wsjstest < ci/out/coverage.prof > ci/out/coverage2.prof -mv ci/out/coverage2.prof ci/out/coverage.prof -grep -v wsecho < ci/out/coverage.prof > ci/out/coverage2.prof -mv ci/out/coverage2.prof ci/out/coverage.prof +sed -i.bak '/_stringer.go/d' ci/out/coverage.prof +sed -i.bak '/wsjstest/d' ci/out/coverage.prof +sed -i.bak '/wsecho/d' ci/out/coverage.prof +rm coverage.prof.bak go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html if [[ ${CI-} ]]; then From 4f91d7a5b85b35cd52b51ecb92f1859fc0941fff Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 29 Sep 2019 13:38:48 -0500 Subject: [PATCH 170/519] Fix broken rm in ci/test.sh --- ci/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/test.sh b/ci/test.sh index 0b8d6835..28c432b3 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -31,7 +31,7 @@ mkdir -p ci/out/websocket sed -i.bak '/_stringer.go/d' ci/out/coverage.prof sed -i.bak '/wsjstest/d' ci/out/coverage.prof sed -i.bak '/wsecho/d' ci/out/coverage.prof -rm coverage.prof.bak +rm ci/out/coverage.prof.bak go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html if [[ ${CI-} ]]; then From e476358de061353e5069f490ac09dd3815513e01 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 29 Sep 2019 18:21:31 -0500 Subject: [PATCH 171/519] Improve usage of math/rand versus crypto/rand math/rand was being used inappropiately and did not have a init function for every file it was used in. --- .github/workflows/ci.yml | 8 -------- assert_test.go | 5 +++++ ci/wasm.sh | 2 +- conn.go | 16 ++++++---------- conn_common.go | 17 +++++++++-------- conn_test.go | 8 ++++---- frame_test.go | 5 +++++ handshake.go | 17 ++++++++++++----- handshake_test.go | 7 +++++-- 9 files changed, 47 insertions(+), 38 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b07c54b8..a53a4697 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -7,24 +7,18 @@ jobs: container: docker://nhooyr/websocket-ci@sha256:b6331f8f64803c8b1bbd2a0ee9e2547317e0de2348bccd9c8dbcc1d88ff5747f steps: - uses: actions/checkout@v1 - with: - fetch-depth: 1 - run: ./ci/fmt.sh lint: runs-on: ubuntu-latest container: docker://nhooyr/websocket-ci@sha256:b6331f8f64803c8b1bbd2a0ee9e2547317e0de2348bccd9c8dbcc1d88ff5747f steps: - uses: actions/checkout@v1 - with: - fetch-depth: 1 - run: ./ci/lint.sh test: runs-on: ubuntu-latest container: docker://nhooyr/websocket-ci@sha256:b6331f8f64803c8b1bbd2a0ee9e2547317e0de2348bccd9c8dbcc1d88ff5747f steps: - uses: actions/checkout@v1 - with: - fetch-depth: 1 - run: ./ci/test.sh env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} @@ -33,6 +27,4 @@ jobs: container: docker://nhooyr/websocket-ci@sha256:b6331f8f64803c8b1bbd2a0ee9e2547317e0de2348bccd9c8dbcc1d88ff5747f steps: - uses: actions/checkout@v1 - with: - fetch-depth: 1 - run: ./ci/wasm.sh diff --git a/assert_test.go b/assert_test.go index 8970c543..e67ed539 100644 --- a/assert_test.go +++ b/assert_test.go @@ -6,6 +6,7 @@ import ( "math/rand" "reflect" "strings" + "time" "github.com/google/go-cmp/cmp" @@ -13,6 +14,10 @@ import ( "nhooyr.io/websocket/wsjson" ) +func init() { + rand.Seed(time.Now().UnixNano()) +} + // https://github.com/google/go-cmp/issues/40#issuecomment-328615283 func cmpDiff(exp, act interface{}) string { return cmp.Diff(exp, act, deepAllowUnexported(exp, act)) diff --git a/ci/wasm.sh b/ci/wasm.sh index 134b60b5..c1d9a404 100755 --- a/ci/wasm.sh +++ b/ci/wasm.sh @@ -25,7 +25,7 @@ go install github.com/agnivade/wasmbrowsertest export WS_ECHO_SERVER_URL GOOS=js GOARCH=wasm go test -exec=wasmbrowsertest ./... -kill "$wsjstestPID" +kill "$wsjstestPID" || true if ! wait "$wsjstestPID"; then echo "--- wsjstest exited unsuccessfully" echo "output:" diff --git a/conn.go b/conn.go index 37c4cac2..d74b8753 100644 --- a/conn.go +++ b/conn.go @@ -5,13 +5,12 @@ package websocket import ( "bufio" "context" - cryptorand "crypto/rand" + "crypto/rand" "errors" "fmt" "io" "io/ioutil" "log" - "math/rand" "runtime" "strconv" "sync" @@ -82,6 +81,7 @@ type Conn struct { setReadTimeout chan context.Context setWriteTimeout chan context.Context + pingCounter *atomicInt64 activePingsMu sync.Mutex activePings map[string]chan<- struct{} } @@ -100,6 +100,7 @@ func (c *Conn) init() { c.setReadTimeout = make(chan context.Context) c.setWriteTimeout = make(chan context.Context) + c.pingCounter = &atomicInt64{} c.activePings = make(map[string]chan<- struct{}) c.writeHeaderBuf = makeWriteHeaderBuf() @@ -669,7 +670,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, opcode opcode, p []byte c.writeHeader.payloadLength = int64(len(p)) if c.client { - _, err := io.ReadFull(cryptorand.Reader, c.writeHeader.maskKey[:]) + _, err := io.ReadFull(rand.Reader, c.writeHeader.maskKey[:]) if err != nil { return 0, fmt.Errorf("failed to generate masking key: %w", err) } @@ -839,10 +840,6 @@ func (c *Conn) writeClose(p []byte, cerr error) error { return nil } -func init() { - rand.Seed(time.Now().UnixNano()) -} - // Ping sends a ping to the peer and waits for a pong. // Use this to measure latency or ensure the peer is responsive. // Ping must be called concurrently with Reader as it does @@ -851,10 +848,9 @@ func init() { // // TCP Keepalives should suffice for most use cases. func (c *Conn) Ping(ctx context.Context) error { - id := rand.Uint64() - p := strconv.FormatUint(id, 10) + p := c.pingCounter.Increment(1) - err := c.ping(ctx, p) + err := c.ping(ctx, strconv.FormatInt(p, 10)) if err != nil { return fmt.Errorf("failed to ping: %w", err) } diff --git a/conn_common.go b/conn_common.go index e7a01035..8233e4a6 100644 --- a/conn_common.go +++ b/conn_common.go @@ -211,21 +211,22 @@ func (c *Conn) setCloseErr(err error) { // See https://github.com/nhooyr/websocket/issues/153 type atomicInt64 struct { - v atomic.Value + v int64 } func (v *atomicInt64) Load() int64 { - i, ok := v.v.Load().(int64) - if !ok { - return 0 - } - return i + return atomic.LoadInt64(&v.v) } func (v *atomicInt64) Store(i int64) { - v.v.Store(i) + atomic.StoreInt64(&v.v, i) } func (v *atomicInt64) String() string { - return fmt.Sprint(v.v.Load()) + return fmt.Sprint(v.Load()) +} + +// Increment increments the value and returns the new value. +func (v *atomicInt64) Increment(delta int64) int64 { + return atomic.AddInt64(&v.v, delta) } diff --git a/conn_test.go b/conn_test.go index 8846979d..c948c435 100644 --- a/conn_test.go +++ b/conn_test.go @@ -37,6 +37,10 @@ import ( "nhooyr.io/websocket/wspb" ) +func init() { + rand.Seed(time.Now().UnixNano()) +} + func TestHandshake(t *testing.T) { t.Parallel() @@ -911,10 +915,6 @@ func TestConn(t *testing.T) { } } -func init() { - rand.Seed(time.Now().UnixNano()) -} - func testServer(tb testing.TB, fn func(w http.ResponseWriter, r *http.Request) error, tls bool) (s *httptest.Server, closeFn func()) { var conns int64 h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/frame_test.go b/frame_test.go index 1a2054c1..7d2a5719 100644 --- a/frame_test.go +++ b/frame_test.go @@ -10,10 +10,15 @@ import ( "strconv" "strings" "testing" + "time" "github.com/google/go-cmp/cmp" ) +func init() { + rand.Seed(time.Now().UnixNano()) +} + func randBool() bool { return rand.Intn(1) == 0 } diff --git a/handshake.go b/handshake.go index 0b078085..d1a9fba4 100644 --- a/handshake.go +++ b/handshake.go @@ -6,13 +6,13 @@ import ( "bufio" "bytes" "context" + "crypto/rand" "crypto/sha1" "encoding/base64" "errors" "fmt" "io" "io/ioutil" - "math/rand" "net/http" "net/textproto" "net/url" @@ -299,7 +299,11 @@ func dial(ctx context.Context, u string, opts *DialOptions) (_ *Conn, _ *http.Re req.Header.Set("Connection", "Upgrade") req.Header.Set("Upgrade", "websocket") req.Header.Set("Sec-WebSocket-Version", "13") - req.Header.Set("Sec-WebSocket-Key", makeSecWebSocketKey()) + secWebSocketKey, err := makeSecWebSocketKey() + if err != nil { + return nil, nil, fmt.Errorf("failed to generate Sec-WebSocket-Key: %w", err) + } + req.Header.Set("Sec-WebSocket-Key", secWebSocketKey) if len(opts.Subprotocols) > 0 { req.Header.Set("Sec-WebSocket-Protocol", strings.Join(opts.Subprotocols, ",")) } @@ -403,8 +407,11 @@ func returnBufioWriter(bw *bufio.Writer) { bufioWriterPool.Put(bw) } -func makeSecWebSocketKey() string { +func makeSecWebSocketKey() (string, error) { b := make([]byte, 16) - rand.Read(b) - return base64.StdEncoding.EncodeToString(b) + _, err := io.ReadFull(rand.Reader, b) + if err != nil { + return "", fmt.Errorf("failed to read random data from rand.Reader: %w", err) + } + return base64.StdEncoding.EncodeToString(b), nil } diff --git a/handshake_test.go b/handshake_test.go index a3d98163..cb09353f 100644 --- a/handshake_test.go +++ b/handshake_test.go @@ -367,14 +367,17 @@ func Test_verifyServerHandshake(t *testing.T) { resp := w.Result() r := httptest.NewRequest("GET", "/", nil) - key := makeSecWebSocketKey() + key, err := makeSecWebSocketKey() + if err != nil { + t.Fatal(err) + } r.Header.Set("Sec-WebSocket-Key", key) if resp.Header.Get("Sec-WebSocket-Accept") == "" { resp.Header.Set("Sec-WebSocket-Accept", secWebSocketAccept(key)) } - err := verifyServerResponse(r, resp) + err = verifyServerResponse(r, resp) if (err == nil) != tc.success { t.Fatalf("unexpected error: %+v", err) } From 68b99a9fbab11d00079fcf3ac4c421917b771f7d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 29 Sep 2019 21:37:17 -0500 Subject: [PATCH 172/519] Upload coverage.html from test.sh as an artifact --- .github/CONTRIBUTING.md | 3 +-- .github/workflows/ci.yml | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 7338093a..08a09c97 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -29,8 +29,7 @@ CI must pass on your changes for them to be merged. CI will ensure your code is formatted correctly, passes linting and tests. It will collect coverage and report it to [codecov](https://codecov.io/gh/nhooyr/websocket) -and also upload a `out/coverage.html` artifact that you can click on to interactively -browse coverage. +and also upload a `coverage` artifact that you can download to inspect browse coverage. You can run CI locally. The various steps are located in [ci/\*.sh](../ci). diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a53a4697..b5fb9cbb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,11 @@ jobs: - run: ./ci/test.sh env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + - name: Upload coverage.html + uses: actions/upload-artifact@master + with: + name: coverage + path: ci/out/coverage.html wasm: runs-on: ubuntu-latest container: docker://nhooyr/websocket-ci@sha256:b6331f8f64803c8b1bbd2a0ee9e2547317e0de2348bccd9c8dbcc1d88ff5747f From 6fc9ea4dc7c9ca8f7c67c9e2ac3b32f2169aecac Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 1 Oct 2019 00:03:19 -0500 Subject: [PATCH 173/519] Switch CI to Typescript Significantly faster locally since **everything** runs in parallel. Also much easier to maintain than bash. --- .github/workflows/ci.yml | 16 +- ci/.eslintrc.yaml | 23 + ci/.gitignore | 1 - ci/all.ts | 20 + ci/fmt.sh | 49 -- ci/fmt.ts | 37 + ci/image/Dockerfile | 24 +- ci/image/dockerignore | 1 + ci/image/gitignore | 5 + ci/image/push.sh | 8 - ci/image/push.ts | 26 + ci/lib.ts | 117 +++ ci/lint.sh | 10 - ci/lint.ts | 18 + ci/out/.gitignore | 1 + ci/run.sh | 19 - ci/test.sh | 39 - ci/test.ts | 57 ++ ci/tsconfig.json | 62 ++ ci/wasm.sh | 34 - ci/wasm.ts | 67 ++ conn_test.go | 1 + package.json | 27 + yarn.lock | 1476 ++++++++++++++++++++++++++++++++++++++ 24 files changed, 1958 insertions(+), 180 deletions(-) create mode 100644 ci/.eslintrc.yaml delete mode 100644 ci/.gitignore create mode 100755 ci/all.ts delete mode 100755 ci/fmt.sh create mode 100755 ci/fmt.ts create mode 100644 ci/image/dockerignore create mode 100644 ci/image/gitignore delete mode 100755 ci/image/push.sh create mode 100755 ci/image/push.ts create mode 100644 ci/lib.ts delete mode 100755 ci/lint.sh create mode 100755 ci/lint.ts create mode 100644 ci/out/.gitignore delete mode 100755 ci/run.sh delete mode 100755 ci/test.sh create mode 100755 ci/test.ts create mode 100644 ci/tsconfig.json delete mode 100755 ci/wasm.sh create mode 100755 ci/wasm.ts create mode 100644 package.json create mode 100644 yarn.lock diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5fb9cbb..38884369 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,22 +4,22 @@ on: [push] jobs: fmt: runs-on: ubuntu-latest - container: docker://nhooyr/websocket-ci@sha256:b6331f8f64803c8b1bbd2a0ee9e2547317e0de2348bccd9c8dbcc1d88ff5747f + container: nhooyr/websocket-ci@sha256:7f5513545dcbaa3ed06a2919acfd1cfbff1e6e0decc1602c98672a4aad2f68ab steps: - uses: actions/checkout@v1 - - run: ./ci/fmt.sh + - run: yarn --frozen-lockfile && yarn fmt lint: runs-on: ubuntu-latest - container: docker://nhooyr/websocket-ci@sha256:b6331f8f64803c8b1bbd2a0ee9e2547317e0de2348bccd9c8dbcc1d88ff5747f + container: nhooyr/websocket-ci@sha256:7f5513545dcbaa3ed06a2919acfd1cfbff1e6e0decc1602c98672a4aad2f68ab steps: - uses: actions/checkout@v1 - - run: ./ci/lint.sh + - run: yarn --frozen-lockfile && yarn lint test: runs-on: ubuntu-latest - container: docker://nhooyr/websocket-ci@sha256:b6331f8f64803c8b1bbd2a0ee9e2547317e0de2348bccd9c8dbcc1d88ff5747f + container: nhooyr/websocket-ci@sha256:7f5513545dcbaa3ed06a2919acfd1cfbff1e6e0decc1602c98672a4aad2f68ab steps: - uses: actions/checkout@v1 - - run: ./ci/test.sh + - run: yarn --frozen-lockfile && yarn test env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - name: Upload coverage.html @@ -29,7 +29,7 @@ jobs: path: ci/out/coverage.html wasm: runs-on: ubuntu-latest - container: docker://nhooyr/websocket-ci@sha256:b6331f8f64803c8b1bbd2a0ee9e2547317e0de2348bccd9c8dbcc1d88ff5747f + container: nhooyr/websocket-ci@sha256:7f5513545dcbaa3ed06a2919acfd1cfbff1e6e0decc1602c98672a4aad2f68ab steps: - uses: actions/checkout@v1 - - run: ./ci/wasm.sh + - run: yarn --frozen-lockfile && yarn wasm diff --git a/ci/.eslintrc.yaml b/ci/.eslintrc.yaml new file mode 100644 index 00000000..c6d53e23 --- /dev/null +++ b/ci/.eslintrc.yaml @@ -0,0 +1,23 @@ +parser: "@typescript-eslint/parser" +env: + node: true + +parserOptions: + ecmaVersion: 2018 + sourceType: module + +extends: + # https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#usage + - eslint:recommended + - plugin:@typescript-eslint/eslint-recommended + - plugin:@typescript-eslint/recommended + # https://www.npmjs.com/package/eslint-plugin-import#typescript + - plugin:import/recommended + - plugin:import/typescript + # https://dev.to/robertcoopercode/using-eslint-and-prettier-in-a-typescript-project-53jb + - prettier/@typescript-eslint + +rules: + "@typescript-eslint/no-use-before-define": off + "@typescript-eslint/explicit-function-return-type": off + "@typescript-eslint/no-non-null-assertion": off diff --git a/ci/.gitignore b/ci/.gitignore deleted file mode 100644 index 1fcb1529..00000000 --- a/ci/.gitignore +++ /dev/null @@ -1 +0,0 @@ -out diff --git a/ci/all.ts b/ci/all.ts new file mode 100755 index 00000000..3b798e93 --- /dev/null +++ b/ci/all.ts @@ -0,0 +1,20 @@ +#!/usr/bin/env -S npx ts-node -P ci/tsconfig.json + +import { fmt, gen } from "./fmt" +import { main } from "./lib" +import { lint } from "./lint" +import { test } from "./test" +import { wasm } from "./wasm" + +main(run) + +async function run(ctx: Promise) { + await gen(ctx) + + await Promise.all([ + fmt(ctx), + lint(ctx), + test(ctx), + wasm(ctx), + ]) +} diff --git a/ci/fmt.sh b/ci/fmt.sh deleted file mode 100755 index d6251e05..00000000 --- a/ci/fmt.sh +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail -cd "$(dirname "${0}")" -cd "$(git rev-parse --show-toplevel)" - -gen() { - # Unfortunately, this is the only way to ensure go.mod and go.sum are correct. - # See https://github.com/golang/go/issues/27005 - go list ./... > /dev/null - go mod tidy - - go generate ./... -} - -fmt() { - gofmt -w -s . - go run go.coder.com/go-tools/cmd/goimports -w "-local=$(go list -m)" . - go run mvdan.cc/sh/cmd/shfmt -i 2 -w -s -sr . - # shellcheck disable=SC2046 - npx -q prettier \ - --write \ - --print-width 120 \ - --no-semi \ - --trailing-comma all \ - --loglevel silent \ - $(git ls-files "*.yaml" "*.yml" "*.md") -} - -unstaged_files() { - git ls-files --other --modified --exclude-standard -} - -check() { - if [[ ${CI-} && $(unstaged_files) != "" ]]; then - echo - echo "Files need generation or are formatted incorrectly." - echo "Run:" - echo "./ci/fmt.sh" - echo - git status - git diff - exit 1 - fi -} - -gen -fmt -check diff --git a/ci/fmt.ts b/ci/fmt.ts new file mode 100755 index 00000000..ba76f54b --- /dev/null +++ b/ci/fmt.ts @@ -0,0 +1,37 @@ +#!/usr/bin/env -S npx ts-node -P ci/tsconfig.json + +import { exec, main } from "./lib" + +if (process.argv[1] === __filename) { + main(async (ctx: Promise) => { + await gen(ctx) + await fmt(ctx) + }) +} + +export async function fmt(ctx: Promise) { + await Promise.all([ + exec(ctx, "go mod tidy"), + exec(ctx, "gofmt -w -s ."), + exec(ctx, `go run go.coder.com/go-tools/cmd/goimports -w "-local=$(go list -m)" .`), + exec(ctx, `npx prettier --write --print-width=120 --no-semi --trailing-comma=all --loglevel=silent $(git ls-files "*.yaml" "*.yml" "*.md")`), + ], + ) + + if (process.env.CI) { + const r = await exec(ctx, "git ls-files --other --modified --exclude-standard") + const files = r.stdout.toString().trim() + if (files.length) { + console.log(`files need generation or are formatted incorrectly: +${files} + +please run: + ./ci/fmt.js`) + process.exit(1) + } + } +} + +export async function gen(ctx: Promise) { + await exec(ctx, "go generate ./...") +} diff --git a/ci/image/Dockerfile b/ci/image/Dockerfile index 8d792159..e17fe446 100644 --- a/ci/image/Dockerfile +++ b/ci/image/Dockerfile @@ -1,24 +1,24 @@ FROM golang:1 -ENV DEBIAN_FRONTEND=noninteractive +RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ + echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list + +RUN curl -sL https://deb.nodesource.com/setup_12.x | bash - +RUN apt-get install -y nodejs chromium yarn + +RUN git config --global color.ui always + ENV GOPATH=/root/gopath ENV PATH=$GOPATH/bin:$PATH ENV GOFLAGS="-mod=readonly" ENV PAGER=cat ENV CI=true -RUN apt-get update && \ - apt-get install -y shellcheck npm chromium && \ - npm install -g prettier - -# https://github.com/golang/go/wiki/WebAssembly#running-tests-in-the-browser -RUN go get github.com/agnivade/wasmbrowsertest && \ - mv $GOPATH/bin/wasmbrowsertest $GOPATH/bin/go_js_wasm_exec - -RUN git config --global color.ui always +RUN mkdir -p ~/.config/git +COPY ./ci/image/gitignore ~/.config/git/ignore -# Cache go modules and build cache. +# Cache go modules, build cache and yarn cache. COPY . /tmp/websocket RUN cd /tmp/websocket && \ - CI= ./ci/run.sh && \ + yarn && CI= yarn ci && \ rm -rf /tmp/websocket diff --git a/ci/image/dockerignore b/ci/image/dockerignore new file mode 100644 index 00000000..3c3629e6 --- /dev/null +++ b/ci/image/dockerignore @@ -0,0 +1 @@ +node_modules diff --git a/ci/image/gitignore b/ci/image/gitignore new file mode 100644 index 00000000..3917f38e --- /dev/null +++ b/ci/image/gitignore @@ -0,0 +1,5 @@ +node_modules +.DS_Store +.idea +.gitignore +.dockerignore diff --git a/ci/image/push.sh b/ci/image/push.sh deleted file mode 100755 index 1cbae979..00000000 --- a/ci/image/push.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail -cd "$(dirname "${0}")" -cd "$(git rev-parse --show-toplevel)" - -docker build -f ./ci/image/Dockerfile -t nhooyr/websocket-ci . -docker push nhooyr/websocket-ci diff --git a/ci/image/push.ts b/ci/image/push.ts new file mode 100755 index 00000000..15740fc1 --- /dev/null +++ b/ci/image/push.ts @@ -0,0 +1,26 @@ +#!/usr/bin/env -S npx ts-node -P ci/tsconfig.json + +import fs from "fs" +import { promisify } from "util" +import { main, spawn } from "../lib" + +main(run, { + timeout: 10 * 60_000, +}) + +async function run(ctx: Promise) { + await promisify(fs.copyFile)("./ci/image/dockerignore", ".dockerignore") + + try { + await spawn(ctx, "docker build -f ./ci/image/Dockerfile -t nhooyr/websocket-ci .", [], { + timeout: 180_000, + stdio: "inherit", + }) + await spawn(ctx, "docker push nhooyr/websocket-ci", [], { + timeout: 30_000, + stdio: "inherit", + }) + } finally { + await promisify(fs.unlink)(".dockerignore") + } +} diff --git a/ci/lib.ts b/ci/lib.ts new file mode 100644 index 00000000..43e15a16 --- /dev/null +++ b/ci/lib.ts @@ -0,0 +1,117 @@ +import Timeout from "await-timeout" +import cp from "child-process-promise" +import { ExecOptions, SpawnOptions } from "child_process" + +export async function main(fn: (ctx: Promise) => void, opts: { + timeout: number +} = { + timeout: 180_000, +}) { + + const timer = new Timeout(); + let ctx: Promise = timer.set(opts.timeout, "context timed out") + + const interrupted = new Promise((res, rej) => { + let int = 0 + process.on("SIGINT", () => { + int++ + if (int === 2) { + console.log("force exited") + process.exit(1) + } + rej("") + }) + }) + + ctx = Promise.race([ctx, interrupted]) + const {res, rej, p} = withCancel(ctx) + ctx = p + + try { + await init(ctx) + await fn(ctx) + res!() + } catch (e) { + console.log(e) + rej!() + process.on("beforeExit", () => { + process.exit(1) + }) + } finally { + timer.clear() + } +} + +// TODO promisify native versions +export async function exec(ctx: Promise, cmd: string, opts?: ExecOptions) { + opts = { + timeout: 60_000, + ...opts, + } + const p = cp.exec(cmd, opts) + + try { + return await selectCtx(ctx, p) + } finally { + p.childProcess.kill() + } +} + +export async function spawn(ctx: Promise, cmd: string, args: string[], opts?: SpawnOptions) { + if (args === undefined) { + args = [] + } + opts = { + timeout: 60_000, + shell: true, + ...opts, + } + const p = cp.spawn(cmd, args, opts) + + try { + return await selectCtx(ctx, p) + } finally { + p.childProcess.kill() + } +} + +async function init(ctx: Promise) { + const r = await exec(ctx, "git rev-parse --show-toplevel", { + cwd: __dirname, + }) + + process.chdir(r.stdout.toString().trim()) +} + +export async function selectCtx(ctx: Promise, p: Promise): Promise { + return await Promise.race([ctx, p]) as Promise +} + +const cancelSymbol = Symbol() + +export function withCancel(p: Promise) { + let rej: () => void; + let res: () => void; + const p2 = new Promise((res2, rej2) => { + res = res2 + rej = () => { + rej2(cancelSymbol) + } + }) + + p = Promise.race([p, p2]) + p = p.catch(e => { + // We need this catch to prevent node from complaining about it being unhandled. + // Look into why more later. + if (e === cancelSymbol) { + return + } + throw e + }) as Promise + + return { + res: res!, + rej: rej!, + p: p, + } +} diff --git a/ci/lint.sh b/ci/lint.sh deleted file mode 100755 index b7268c55..00000000 --- a/ci/lint.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail -cd "$(dirname "${0}")" -cd "$(git rev-parse --show-toplevel)" - -# shellcheck disable=SC2046 -shellcheck -x $(git ls-files "*.sh") -go vet ./... -go run golang.org/x/lint/golint -set_exit_status ./... diff --git a/ci/lint.ts b/ci/lint.ts new file mode 100755 index 00000000..a411e588 --- /dev/null +++ b/ci/lint.ts @@ -0,0 +1,18 @@ +#!/usr/bin/env -S npx ts-node -P ci/tsconfig.json + +import { exec, main } from "./lib" + +if (process.argv[1] === __filename) { + main(lint) +} + +export async function lint(ctx: Promise) { + await Promise.all([ + exec(ctx, "go vet ./..."), + exec(ctx, "go run golang.org/x/lint/golint -set_exit_status ./..."), + exec(ctx, "git ls-files '*.ts' | xargs npx eslint --max-warnings 0 --fix", { + cwd: "ci", + }), + ], + ) +} diff --git a/ci/out/.gitignore b/ci/out/.gitignore new file mode 100644 index 00000000..72e8ffc0 --- /dev/null +++ b/ci/out/.gitignore @@ -0,0 +1 @@ +* diff --git a/ci/run.sh b/ci/run.sh deleted file mode 100755 index 1e386ff1..00000000 --- a/ci/run.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash - -# This script is for local testing. See .github/workflows/ci.yml for CI. - -set -euo pipefail -cd "$(dirname "${0}")" -cd "$(git rev-parse --show-toplevel)" - -echo "--- fmt" -./ci/fmt.sh - -echo "--- lint" -./ci/lint.sh - -echo "--- test" -./ci/test.sh - -echo "--- wasm" -./ci/wasm.sh diff --git a/ci/test.sh b/ci/test.sh deleted file mode 100755 index 28c432b3..00000000 --- a/ci/test.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail -cd "$(dirname "${0}")" -cd "$(git rev-parse --show-toplevel)" - -argv+=(go test - "-parallel=1024" - "-coverprofile=ci/out/coverage.prof" - "-coverpkg=./..." -) - -if [[ ${CI-} ]]; then - argv+=( - "-race" - ) -fi - -if [[ $# -gt 0 ]]; then - argv+=( - "$@" - ) -else - argv+=(./...) -fi - -mkdir -p ci/out/websocket -"${argv[@]}" - -# Removes coverage of generated/test related files. -sed -i.bak '/_stringer.go/d' ci/out/coverage.prof -sed -i.bak '/wsjstest/d' ci/out/coverage.prof -sed -i.bak '/wsecho/d' ci/out/coverage.prof -rm ci/out/coverage.prof.bak - -go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html -if [[ ${CI-} ]]; then - bash <(curl -s https://codecov.io/bash) -Z -R . -f ci/out/coverage.prof -fi diff --git a/ci/test.ts b/ci/test.ts new file mode 100755 index 00000000..a2063d8d --- /dev/null +++ b/ci/test.ts @@ -0,0 +1,57 @@ +#!/usr/bin/env -S npx ts-node -P ci/tsconfig.json + +import * as https from "https" +import replaceInFile from "replace-in-file" +import { exec, main, selectCtx, spawn } from "./lib" + +if (process.argv[1] === __filename) { + main(test) +} + +export async function test(ctx: Promise) { + const args = [ + "-parallel=1024", + "-coverprofile=ci/out/coverage.prof", + "-coverpkg=./...", + ] + + if (process.env.CI) { + args.push("-race") + } + + const cliArgs = process.argv.splice(2) + if (cliArgs.length > 0) { + args.push(...cliArgs) + } else { + args.push("./...") + } + + await spawn(ctx, "go", ["test", ...args], { + timeout: 60_000, + stdio: "inherit", + }) + + // Depending on the code tested, we may not have replaced anything so we do not + // check whether anything was replaced. + await selectCtx(ctx, replaceInFile({ + files: "./ci/out/coverage.prof", + from: [ + /.+frame_stringer.go:.+\n/g, + /.+wsjstest:.+\n/g, + /.+wsecho:.+\n/g, + ], + to: "", + })) + + let p: Promise = exec(ctx, "go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html") + + if (process.env.CI) { + const script = https.get("https://codecov.io/bash") + const p2 = spawn(ctx, "bash -Z -R . -f ci/out/coverage.prof", [], { + stdio: [script], + }) + p = Promise.all([p, p2]) + } + + await p +} diff --git a/ci/tsconfig.json b/ci/tsconfig.json new file mode 100644 index 00000000..cdf51985 --- /dev/null +++ b/ci/tsconfig.json @@ -0,0 +1,62 @@ +{ + "compilerOptions": { + /* Basic Options */ + "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + "rootDir": "./ci", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "incremental": true, /* Enable incremental compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + } +} diff --git a/ci/wasm.sh b/ci/wasm.sh deleted file mode 100755 index c1d9a404..00000000 --- a/ci/wasm.sh +++ /dev/null @@ -1,34 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail -cd "$(dirname "${0}")" -cd "$(git rev-parse --show-toplevel)" - -GOOS=js GOARCH=wasm go vet ./... - -go install golang.org/x/lint/golint -GOOS=js GOARCH=wasm golint -set_exit_status ./... - -wsjstestOut="$(mktemp)" -go install ./internal/wsjstest -timeout 30s wsjstest >> "$wsjstestOut" 2>&1 & -wsjstestPID=$! - -# See https://superuser.com/a/900134 -WS_ECHO_SERVER_URL="$( (tail -f -n0 "$wsjstestOut" &) | timeout 10s head -n 1)" -if [[ -z $WS_ECHO_SERVER_URL ]]; then - echo "./internal/wsjstest failed to start in 10s" - exit 1 -fi - -go install github.com/agnivade/wasmbrowsertest -export WS_ECHO_SERVER_URL -GOOS=js GOARCH=wasm go test -exec=wasmbrowsertest ./... - -kill "$wsjstestPID" || true -if ! wait "$wsjstestPID"; then - echo "--- wsjstest exited unsuccessfully" - echo "output:" - cat "$wsjstestOut" - exit 1 -fi diff --git a/ci/wasm.ts b/ci/wasm.ts new file mode 100755 index 00000000..29698c72 --- /dev/null +++ b/ci/wasm.ts @@ -0,0 +1,67 @@ +#!/usr/bin/env -S npx ts-node -P ci/tsconfig.json + +import cp from "child_process" +import * as events from "events" +import * as readline from "readline" +import { exec, main, selectCtx } from "./lib" + +if (process.argv[1] === __filename) { + main(wasm) +} + +const wasmEnv = { + ...process.env, + GOOS: "js", + GOARCH: "wasm", +} + +export async function wasm(ctx: Promise) { + await Promise.all([ + exec(ctx, "go vet ./...", { + env: wasmEnv, + }), + goLint(ctx), + wasmTest(ctx), + ], + ) +} + +async function goLint(ctx: Promise) { + await exec(ctx, "go install golang.org/x/lint/golint") + await exec(ctx, "golint -set_exit_status ./...", { + env: wasmEnv, + }) +} + +async function wasmTest(ctx: Promise) { + await Promise.all([ + exec(ctx, "go install ./internal/wsjstest"), + exec(ctx, "go install github.com/agnivade/wasmbrowsertest"), + ]) + + const url = await startServer(ctx) + + await exec(ctx, "go test -exec=wasmbrowsertest ./...", { + env: { + ...wasmEnv, + WS_ECHO_SERVER_URL: url, + }, + }) +} + +async function startServer(ctx: Promise): Promise { + const wsjstest = cp.spawn("wsjstest") + ctx.finally(wsjstest.kill.bind(wsjstest)) + + const rl = readline.createInterface({ + input: wsjstest.stdout!, + }) + + try { + const p = events.once(rl, "line") + const a = await selectCtx(ctx, p) + return a[0] + } finally { + rl.close() + } +} diff --git a/conn_test.go b/conn_test.go index c948c435..12788c30 100644 --- a/conn_test.go +++ b/conn_test.go @@ -947,6 +947,7 @@ func testServer(tb testing.TB, fn func(w http.ResponseWriter, r *http.Request) e tb.Fatalf("waiting for server to come down timed out: %v", ctx.Err()) } } + } } diff --git a/package.json b/package.json new file mode 100644 index 00000000..85f2735e --- /dev/null +++ b/package.json @@ -0,0 +1,27 @@ +{ + "private": true, + "devDependencies": { + "@types/await-timeout": "^0.3.1", + "@types/child-process-promise": "^2.2.1", + "@types/node": "^12.7.9", + "@typescript-eslint/eslint-plugin": "^2.3.2", + "@typescript-eslint/parser": "^2.3.2", + "await-timeout": "^0.6.0", + "child-process-promise": "^2.2.1", + "eslint": "^6.5.1", + "eslint-config-prettier": "^6.3.0", + "eslint-plugin-import": "^2.18.2", + "prettier": "^1.18.2", + "replace-in-file": "^4.1.3", + "ts-node": "^8.4.1", + "typescript": "^3.6.3" + }, + "scripts": { + "fmt": "./ci/fmt.ts", + "lint": "./ci/lint.ts", + "test": "./ci/test.ts", + "wasm": "./ci/wasm.ts", + "ci": "./ci/all.ts", + "push-ci-image": "ci/image/push.ts" + } +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 00000000..6fa6eeec --- /dev/null +++ b/yarn.lock @@ -0,0 +1,1476 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.0.0": + version "7.5.5" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d" + integrity sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw== + dependencies: + "@babel/highlight" "^7.0.0" + +"@babel/highlight@^7.0.0": + version "7.5.0" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.5.0.tgz#56d11312bd9248fa619591d02472be6e8cb32540" + integrity sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ== + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^4.0.0" + +"@types/await-timeout@^0.3.1": + version "0.3.1" + resolved "https://registry.yarnpkg.com/@types/await-timeout/-/await-timeout-0.3.1.tgz#3a0baafc3a96c7a14447a4dcfdcc76b21ce97c3b" + integrity sha512-H5PzROT4KuP7XQDua13Iw8did//OCKAZ/3TL15DjvMzDonrk4HvhH1+tLko96f2guU6XaD3AoqRa49ZOwbwNig== + +"@types/child-process-promise@^2.2.1": + version "2.2.1" + resolved "https://registry.yarnpkg.com/@types/child-process-promise/-/child-process-promise-2.2.1.tgz#049033bef102f77a1719b38672cc86a2c4710ab1" + integrity sha512-xZ4kkF82YkmqPCERqV9Tj0bVQj3Tk36BqGlNgxv5XhifgDRhwAqp+of+sccksdpZRbbPsNwMOkmUqOnLgxKtGw== + dependencies: + "@types/node" "*" + +"@types/eslint-visitor-keys@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" + integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== + +"@types/json-schema@^7.0.3": + version "7.0.3" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636" + integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A== + +"@types/node@*", "@types/node@^12.7.9": + version "12.7.9" + resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.9.tgz#da0210f91096aa67138cf5afd04c4d629f8a406a" + integrity sha512-P57oKTJ/vYivL2BCfxCC5tQjlS8qW31pbOL6qt99Yrjm95YdHgNZwjrTTjMBh+C2/y6PXIX4oz253+jUzxKKfQ== + +"@typescript-eslint/eslint-plugin@^2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.3.2.tgz#7e112ca0bb29044d915baf10163a8199a20f7c69" + integrity sha512-tcnpksq1bXzcIRbYLeXkgp6l+ggEMXXUcl1wsSvL807fRtmvVQKygElwEUf4hBA76dNag3VAK1q2m3vd7qJaZA== + dependencies: + "@typescript-eslint/experimental-utils" "2.3.2" + eslint-utils "^1.4.2" + functional-red-black-tree "^1.0.1" + regexpp "^2.0.1" + tsutils "^3.17.1" + +"@typescript-eslint/experimental-utils@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.3.2.tgz#e50f31264507e6fec7b33840bb6af260c24f4ea8" + integrity sha512-t+JGdTT6dRbmvKDlhlVkEueoZa0fhJNfG6z2cpnRPLwm3VwYr2BjR//acJGC1Yza0I9ZNcDfRY7ubQEvvfG6Jg== + dependencies: + "@types/json-schema" "^7.0.3" + "@typescript-eslint/typescript-estree" "2.3.2" + eslint-scope "^5.0.0" + +"@typescript-eslint/parser@^2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.3.2.tgz#e9b742e191cd1209930da469cde379591ad0af5b" + integrity sha512-nq1UQeNGdKdqdgF6Ww+Ov2OidWgiL96+JYdXXZ2rkP/OWyc6KMNSbs6MpRCpI8q+PmDa7hBnHNQIo7w/drYccA== + dependencies: + "@types/eslint-visitor-keys" "^1.0.0" + "@typescript-eslint/experimental-utils" "2.3.2" + "@typescript-eslint/typescript-estree" "2.3.2" + eslint-visitor-keys "^1.1.0" + +"@typescript-eslint/typescript-estree@2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.3.2.tgz#107414aa04e689fe6f7251eb63fb500217f2b7f4" + integrity sha512-eZNEAai16nwyhIVIEaWQlaUgAU3S9CkQ58qvK0+3IuSdLJD3W1PNuehQFMIhW/mTP1oFR9GNoTcLg7gtXz6lzA== + dependencies: + glob "^7.1.4" + is-glob "^4.0.1" + lodash.unescape "4.0.1" + semver "^6.3.0" + +acorn-jsx@^5.0.2: + version "5.0.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.2.tgz#84b68ea44b373c4f8686023a551f61a21b7c4a4f" + integrity sha512-tiNTrP1MP0QrChmD2DdupCr6HWSFeKVw5d/dHTu4Y7rkAkRhU/Dt7dphAfIUyxtHpl/eBVip5uTNSpQJHylpAw== + +acorn@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c" + integrity sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ== + +ajv@^6.10.0, ajv@^6.10.2: + version "6.10.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" + integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw== + dependencies: + fast-deep-equal "^2.0.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-escapes@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" + integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== + +ansi-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" + integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= + +ansi-regex@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" + integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== + +ansi-styles@^3.2.0, ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +arg@^4.1.0: + version "4.1.1" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.1.tgz#485f8e7c390ce4c5f78257dbea80d4be11feda4c" + integrity sha512-SlmP3fEA88MBv0PypnXZ8ZfJhwmDeIE3SP71j37AiXQBXYosPV0x6uISAaHYSlSVhmHOVkomen0tbGk6Anlebw== + +argparse@^1.0.7: + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" + integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== + dependencies: + sprintf-js "~1.0.2" + +array-includes@^3.0.3: + version "3.0.3" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d" + integrity sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0= + dependencies: + define-properties "^1.1.2" + es-abstract "^1.7.0" + +astral-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" + integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== + +await-timeout@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/await-timeout/-/await-timeout-0.6.0.tgz#efb52f5dba4d5fea6cff043705b09c97e7c403dc" + integrity sha512-eMGnZxbqnmYTxCPqSYk5paIbPgcX8auC0UyVMScYMPY8pYmdk6o2wqQGw+SyN2hrhaDVZIQHVZX8DaHuBoRZcg== + +balanced-match@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" + integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +buffer-from@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" + integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +camelcase@^5.0.0: + version "5.3.1" + resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" + integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== + +chalk@^2.0.0, chalk@^2.1.0, chalk@^2.4.2: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + +child-process-promise@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/child-process-promise/-/child-process-promise-2.2.1.tgz#4730a11ef610fad450b8f223c79d31d7bdad8074" + integrity sha1-RzChHvYQ+tRQuPIjx50x172tgHQ= + dependencies: + cross-spawn "^4.0.2" + node-version "^1.0.0" + promise-polyfill "^6.0.1" + +cli-cursor@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" + integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= + dependencies: + restore-cursor "^2.0.0" + +cli-width@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" + integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk= + +cliui@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" + integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== + dependencies: + string-width "^3.1.0" + strip-ansi "^5.2.0" + wrap-ansi "^5.1.0" + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +contains-path@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" + integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo= + +cross-spawn@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" + integrity sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE= + dependencies: + lru-cache "^4.0.1" + which "^1.2.9" + +cross-spawn@^6.0.5: + version "6.0.5" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" + integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== + dependencies: + nice-try "^1.0.4" + path-key "^2.0.1" + semver "^5.5.0" + shebang-command "^1.2.0" + which "^1.2.9" + +debug@^2.6.8, debug@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^4.0.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" + integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== + dependencies: + ms "^2.1.1" + +decamelize@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" + integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= + +deep-is@~0.1.3: + version "0.1.3" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" + integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= + +define-properties@^1.1.2, define-properties@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + dependencies: + object-keys "^1.0.12" + +diff@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.1.tgz#0c667cb467ebbb5cea7f14f135cc2dba7780a8ff" + integrity sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q== + +doctrine@1.5.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" + integrity sha1-N53Ocw9hZvds76TmcHoVmwLFpvo= + dependencies: + esutils "^2.0.2" + isarray "^1.0.0" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +emoji-regex@^7.0.1: + version "7.0.3" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" + integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== + +error-ex@^1.2.0: + version "1.3.2" + resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" + integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== + dependencies: + is-arrayish "^0.2.1" + +es-abstract@^1.12.0, es-abstract@^1.7.0: + version "1.15.0" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.15.0.tgz#8884928ec7e40a79e3c9bc812d37d10c8b24cc57" + integrity sha512-bhkEqWJ2t2lMeaJDuk7okMkJWI/yqgH/EoGwpcvv0XW9RWQsRspI4wt6xuyuvMvvQE3gg/D9HXppgk21w78GyQ== + dependencies: + es-to-primitive "^1.2.0" + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.0" + is-callable "^1.1.4" + is-regex "^1.0.4" + object-inspect "^1.6.0" + object-keys "^1.1.1" + string.prototype.trimleft "^2.1.0" + string.prototype.trimright "^2.1.0" + +es-to-primitive@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" + integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +eslint-config-prettier@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.3.0.tgz#e73b48e59dc49d950843f3eb96d519e2248286a3" + integrity sha512-EWaGjlDAZRzVFveh2Jsglcere2KK5CJBhkNSa1xs3KfMUGdRiT7lG089eqPdvlzWHpAqaekubOsOMu8W8Yk71A== + dependencies: + get-stdin "^6.0.0" + +eslint-import-resolver-node@^0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a" + integrity sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q== + dependencies: + debug "^2.6.9" + resolve "^1.5.0" + +eslint-module-utils@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.4.1.tgz#7b4675875bf96b0dbf1b21977456e5bb1f5e018c" + integrity sha512-H6DOj+ejw7Tesdgbfs4jeS4YMFrT8uI8xwd1gtQqXssaR0EQ26L+2O/w6wkYFy2MymON0fTwHmXBvvfLNZVZEw== + dependencies: + debug "^2.6.8" + pkg-dir "^2.0.0" + +eslint-plugin-import@^2.18.2: + version "2.18.2" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz#02f1180b90b077b33d447a17a2326ceb400aceb6" + integrity sha512-5ohpsHAiUBRNaBWAF08izwUGlbrJoJJ+W9/TBwsGoR1MnlgfwMIKrFeSjWbt6moabiXW9xNvtFz+97KHRfI4HQ== + dependencies: + array-includes "^3.0.3" + contains-path "^0.1.0" + debug "^2.6.9" + doctrine "1.5.0" + eslint-import-resolver-node "^0.3.2" + eslint-module-utils "^2.4.0" + has "^1.0.3" + minimatch "^3.0.4" + object.values "^1.1.0" + read-pkg-up "^2.0.0" + resolve "^1.11.0" + +eslint-scope@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9" + integrity sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw== + dependencies: + esrecurse "^4.1.0" + estraverse "^4.1.1" + +eslint-utils@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.2.tgz#166a5180ef6ab7eb462f162fd0e6f2463d7309ab" + integrity sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q== + dependencies: + eslint-visitor-keys "^1.0.0" + +eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" + integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== + +eslint@^6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.5.1.tgz#828e4c469697d43bb586144be152198b91e96ed6" + integrity sha512-32h99BoLYStT1iq1v2P9uwpyznQ4M2jRiFB6acitKz52Gqn+vPaMDUTB1bYi1WN4Nquj2w+t+bimYUG83DC55A== + dependencies: + "@babel/code-frame" "^7.0.0" + ajv "^6.10.0" + chalk "^2.1.0" + cross-spawn "^6.0.5" + debug "^4.0.1" + doctrine "^3.0.0" + eslint-scope "^5.0.0" + eslint-utils "^1.4.2" + eslint-visitor-keys "^1.1.0" + espree "^6.1.1" + esquery "^1.0.1" + esutils "^2.0.2" + file-entry-cache "^5.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^5.0.0" + globals "^11.7.0" + ignore "^4.0.6" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + inquirer "^6.4.1" + is-glob "^4.0.0" + js-yaml "^3.13.1" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.3.0" + lodash "^4.17.14" + minimatch "^3.0.4" + mkdirp "^0.5.1" + natural-compare "^1.4.0" + optionator "^0.8.2" + progress "^2.0.0" + regexpp "^2.0.1" + semver "^6.1.2" + strip-ansi "^5.2.0" + strip-json-comments "^3.0.1" + table "^5.2.3" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +espree@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.1.tgz#7f80e5f7257fc47db450022d723e356daeb1e5de" + integrity sha512-EYbr8XZUhWbYCqQRW0duU5LxzL5bETN6AjKBGy1302qqzPaCH10QbRg3Wvco79Z8x9WbiE8HYB4e75xl6qUYvQ== + dependencies: + acorn "^7.0.0" + acorn-jsx "^5.0.2" + eslint-visitor-keys "^1.1.0" + +esprima@^4.0.0: + version "4.0.1" + resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" + integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== + +esquery@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708" + integrity sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA== + dependencies: + estraverse "^4.0.0" + +esrecurse@^4.1.0: + version "4.2.1" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" + integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ== + dependencies: + estraverse "^4.1.0" + +estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +external-editor@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + +fast-deep-equal@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" + integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= + +fast-json-stable-stringify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" + integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= + +fast-levenshtein@~2.0.4: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +figures@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" + integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= + dependencies: + escape-string-regexp "^1.0.5" + +file-entry-cache@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" + integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== + dependencies: + flat-cache "^2.0.1" + +find-up@^2.0.0, find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= + dependencies: + locate-path "^2.0.0" + +find-up@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" + integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== + dependencies: + locate-path "^3.0.0" + +flat-cache@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" + integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== + dependencies: + flatted "^2.0.0" + rimraf "2.6.3" + write "1.0.3" + +flatted@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08" + integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg== + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + +get-caller-file@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-stdin@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" + integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g== + +glob-parent@^5.0.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.0.tgz#5f4c1d1e748d30cd73ad2944b3577a81b081e8c2" + integrity sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw== + dependencies: + is-glob "^4.0.1" + +glob@^7.1.3, glob@^7.1.4: + version "7.1.4" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" + integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.7.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +graceful-fs@^4.1.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.2.tgz#6f0952605d0140c1cfdb138ed005775b92d67b02" + integrity sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-symbols@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" + integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= + +has@^1.0.1, has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +hosted-git-info@^2.1.4: + version "2.8.4" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.4.tgz#44119abaf4bc64692a16ace34700fed9c03e2546" + integrity sha512-pzXIvANXEFrc5oFFXRMkbLPQ2rXRoDERwDLyrcUxGhaZhgP54BBSl9Oheh7Vv0T090cszWBxPjkQQ5Sq1PbBRQ== + +iconv-lite@^0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ignore@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" + integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== + +import-fresh@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.1.0.tgz#6d33fa1dcef6df930fae003446f33415af905118" + integrity sha512-PpuksHKGt8rXfWEr9m9EHIpgyyaltBy8+eF6GJM0QCAxMgxCfucMF3mjecK2QsJr0amJW7gTqh5/wht0z2UhEQ== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inquirer@^6.4.1: + version "6.5.2" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca" + integrity sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ== + dependencies: + ansi-escapes "^3.2.0" + chalk "^2.4.2" + cli-cursor "^2.1.0" + cli-width "^2.0.0" + external-editor "^3.0.3" + figures "^2.0.0" + lodash "^4.17.12" + mute-stream "0.0.7" + run-async "^2.2.0" + rxjs "^6.4.0" + string-width "^2.1.0" + strip-ansi "^5.1.0" + through "^2.3.6" + +is-arrayish@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" + integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= + +is-callable@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" + integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA== + +is-date-object@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" + integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY= + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-fullwidth-code-point@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" + integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= + +is-glob@^4.0.0, is-glob@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" + integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== + dependencies: + is-extglob "^2.1.1" + +is-promise@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= + +is-regex@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" + integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE= + dependencies: + has "^1.0.1" + +is-symbol@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38" + integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw== + dependencies: + has-symbols "^1.0.0" + +isarray@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" + integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^3.13.1: + version "3.13.1" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" + integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== + dependencies: + argparse "^1.0.7" + esprima "^4.0.0" + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + +levn@^0.3.0, levn@~0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" + integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= + dependencies: + prelude-ls "~1.1.2" + type-check "~0.3.2" + +load-json-file@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" + integrity sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg= + dependencies: + graceful-fs "^4.1.2" + parse-json "^2.2.0" + pify "^2.0.0" + strip-bom "^3.0.0" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +locate-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" + integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== + dependencies: + p-locate "^3.0.0" + path-exists "^3.0.0" + +lodash.unescape@4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c" + integrity sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw= + +lodash@^4.17.12, lodash@^4.17.14: + version "4.17.15" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" + integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== + +lru-cache@^4.0.1: + version "4.1.5" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" + integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== + dependencies: + pseudomap "^1.0.2" + yallist "^2.1.2" + +make-error@^1.1.1: + version "1.3.5" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" + integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g== + +mimic-fn@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" + integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== + +minimatch@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" + integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== + dependencies: + brace-expansion "^1.1.7" + +minimist@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" + integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= + +mkdirp@^0.5.1: + version "0.5.1" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" + integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= + dependencies: + minimist "0.0.8" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@^2.1.1: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +mute-stream@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" + integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + +nice-try@^1.0.4: + version "1.0.5" + resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" + integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== + +node-version@^1.0.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/node-version/-/node-version-1.2.0.tgz#34fde3ffa8e1149bd323983479dda620e1b5060d" + integrity sha512-ma6oU4Sk0qOoKEAymVoTvk8EdXEobdS7m/mAGhDJ8Rouugho48crHBORAmy5BoOcv8wraPM6xumapQp5hl4iIQ== + +normalize-package-data@^2.3.2: + version "2.5.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" + integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== + dependencies: + hosted-git-info "^2.1.4" + resolve "^1.10.0" + semver "2 || 3 || 4 || 5" + validate-npm-package-license "^3.0.1" + +object-inspect@^1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b" + integrity sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ== + +object-keys@^1.0.12, object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.values@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.0.tgz#bf6810ef5da3e5325790eaaa2be213ea84624da9" + integrity sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg== + dependencies: + define-properties "^1.1.3" + es-abstract "^1.12.0" + function-bind "^1.1.1" + has "^1.0.3" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +onetime@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" + integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ= + dependencies: + mimic-fn "^1.0.0" + +optionator@^0.8.2: + version "0.8.2" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" + integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q= + dependencies: + deep-is "~0.1.3" + fast-levenshtein "~2.0.4" + levn "~0.3.0" + prelude-ls "~1.1.2" + type-check "~0.3.2" + wordwrap "~1.0.0" + +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + +p-limit@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" + integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== + dependencies: + p-try "^1.0.0" + +p-limit@^2.0.0: + version "2.2.1" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.1.tgz#aa07a788cc3151c939b5131f63570f0dd2009537" + integrity sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg== + dependencies: + p-try "^2.0.0" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= + dependencies: + p-limit "^1.1.0" + +p-locate@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" + integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== + dependencies: + p-limit "^2.0.0" + +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= + +p-try@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" + integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +parse-json@^2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" + integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= + dependencies: + error-ex "^1.2.0" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-key@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" + integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= + +path-parse@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" + integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== + +path-type@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" + integrity sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM= + dependencies: + pify "^2.0.0" + +pify@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" + integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= + +pkg-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" + integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s= + dependencies: + find-up "^2.1.0" + +prelude-ls@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" + integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= + +prettier@^1.18.2: + version "1.18.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.18.2.tgz#6823e7c5900017b4bd3acf46fe9ac4b4d7bda9ea" + integrity sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw== + +progress@^2.0.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" + integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== + +promise-polyfill@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-6.1.0.tgz#dfa96943ea9c121fca4de9b5868cb39d3472e057" + integrity sha1-36lpQ+qcEh/KTem1hoyznTRy4Fc= + +pseudomap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" + integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +read-pkg-up@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" + integrity sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4= + dependencies: + find-up "^2.0.0" + read-pkg "^2.0.0" + +read-pkg@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" + integrity sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg= + dependencies: + load-json-file "^2.0.0" + normalize-package-data "^2.3.2" + path-type "^2.0.0" + +regexpp@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" + integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== + +replace-in-file@^4.1.3: + version "4.1.3" + resolved "https://registry.yarnpkg.com/replace-in-file/-/replace-in-file-4.1.3.tgz#0692332304c2b535e2323fffb39a1dfe59a8aac6" + integrity sha512-x8uSfKKK/5YiJ8JYsNMwa1PJYvHfPdUABVXd21ro09Nh5BRZ5ATuACwrqCNpktwVqVbFTk/cIGFepeMqY0oX3g== + dependencies: + chalk "^2.4.2" + glob "^7.1.4" + yargs "^13.3.0" + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +require-main-filename@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" + integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve@^1.10.0, resolve@^1.11.0, resolve@^1.5.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6" + integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w== + dependencies: + path-parse "^1.0.6" + +restore-cursor@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" + integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368= + dependencies: + onetime "^2.0.0" + signal-exit "^3.0.2" + +rimraf@2.6.3: + version "2.6.3" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" + integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== + dependencies: + glob "^7.1.3" + +run-async@^2.2.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" + integrity sha1-A3GrSuC91yDUFm19/aZP96RFpsA= + dependencies: + is-promise "^2.1.0" + +rxjs@^6.4.0: + version "6.5.3" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.3.tgz#510e26317f4db91a7eb1de77d9dd9ba0a4899a3a" + integrity sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA== + dependencies: + tslib "^1.9.0" + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +"semver@2 || 3 || 4 || 5", semver@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +semver@^6.1.2, semver@^6.3.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +set-blocking@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" + integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= + +shebang-command@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" + integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= + dependencies: + shebang-regex "^1.0.0" + +shebang-regex@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" + integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= + +signal-exit@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" + integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= + +slice-ansi@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" + integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== + dependencies: + ansi-styles "^3.2.0" + astral-regex "^1.0.0" + is-fullwidth-code-point "^2.0.0" + +source-map-support@^0.5.6: + version "0.5.13" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" + integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +spdx-correct@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" + integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== + dependencies: + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" + +spdx-exceptions@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" + integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== + +spdx-expression-parse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" + integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.5" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" + integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== + +sprintf-js@~1.0.2: + version "1.0.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" + integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= + +string-width@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" + integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== + dependencies: + is-fullwidth-code-point "^2.0.0" + strip-ansi "^4.0.0" + +string-width@^3.0.0, string-width@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" + integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== + dependencies: + emoji-regex "^7.0.1" + is-fullwidth-code-point "^2.0.0" + strip-ansi "^5.1.0" + +string.prototype.trimleft@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz#6cc47f0d7eb8d62b0f3701611715a3954591d634" + integrity sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + +string.prototype.trimright@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz#669d164be9df9b6f7559fa8e89945b168a5a6c58" + integrity sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg== + dependencies: + define-properties "^1.1.3" + function-bind "^1.1.1" + +strip-ansi@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" + integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= + dependencies: + ansi-regex "^3.0.0" + +strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" + integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== + dependencies: + ansi-regex "^4.1.0" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + +strip-json-comments@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" + integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +table@^5.2.3: + version "5.4.6" + resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" + integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== + dependencies: + ajv "^6.10.2" + lodash "^4.17.14" + slice-ansi "^2.1.0" + string-width "^3.0.0" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + +ts-node@^8.4.1: + version "8.4.1" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.4.1.tgz#270b0dba16e8723c9fa4f9b4775d3810fd994b4f" + integrity sha512-5LpRN+mTiCs7lI5EtbXmF/HfMeCjzt7DH9CZwtkr6SywStrNQC723wG+aOWFiLNn7zT3kD/RnFqi3ZUfr4l5Qw== + dependencies: + arg "^4.1.0" + diff "^4.0.1" + make-error "^1.1.1" + source-map-support "^0.5.6" + yn "^3.0.0" + +tslib@^1.8.1, tslib@^1.9.0: + version "1.10.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" + integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== + +tsutils@^3.17.1: + version "3.17.1" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" + integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g== + dependencies: + tslib "^1.8.1" + +type-check@~0.3.2: + version "0.3.2" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" + integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= + dependencies: + prelude-ls "~1.1.2" + +typescript@^3.6.3: + version "3.6.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.3.tgz#fea942fabb20f7e1ca7164ff626f1a9f3f70b4da" + integrity sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw== + +uri-js@^4.2.2: + version "4.2.2" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" + integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== + dependencies: + punycode "^2.1.0" + +v8-compile-cache@^2.0.3: + version "2.1.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e" + integrity sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g== + +validate-npm-package-license@^3.0.1: + version "3.0.4" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" + integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== + dependencies: + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" + +which-module@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" + integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= + +which@^1.2.9: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + +wordwrap@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" + integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= + +wrap-ansi@^5.1.0: + version "5.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" + integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== + dependencies: + ansi-styles "^3.2.0" + string-width "^3.0.0" + strip-ansi "^5.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +write@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" + integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== + dependencies: + mkdirp "^0.5.1" + +y18n@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" + integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== + +yallist@^2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" + integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= + +yargs-parser@^13.1.1: + version "13.1.1" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0" + integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ== + dependencies: + camelcase "^5.0.0" + decamelize "^1.2.0" + +yargs@^13.3.0: + version "13.3.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83" + integrity sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA== + dependencies: + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.1" + +yn@^3.0.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== From 862b00ff99a20fdfd7d12c6dbe7643135605e82f Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 4 Oct 2019 15:07:55 -0500 Subject: [PATCH 174/519] Update docs and remove separate wasm CI step Integrated into the appropriate existing steps. --- .github/CONTRIBUTING.md | 26 +++++++++------- .github/workflows/ci.yml | 12 ++----- ci/all.ts | 2 -- ci/fmt.ts | 2 +- ci/image/Dockerfile | 8 ++--- ci/image/push.ts | 2 -- ci/lib.ts | 11 +++++-- ci/lint.ts | 13 ++++++-- ci/test.ts | 63 +++++++++++++++++++++++++++++-------- ci/wasm.ts | 67 ---------------------------------------- package.json | 2 +- yarn.lock | 27 ++++++++++++++++ 12 files changed, 120 insertions(+), 115 deletions(-) delete mode 100755 ci/wasm.ts diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 08a09c97..edf2f839 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -27,25 +27,29 @@ CI must pass on your changes for them to be merged. ### CI -CI will ensure your code is formatted correctly, passes linting and tests. +CI will ensure your code is formatted, lints and passes tests. It will collect coverage and report it to [codecov](https://codecov.io/gh/nhooyr/websocket) -and also upload a `coverage` artifact that you can download to inspect browse coverage. +and also upload a html `coverage` artifact that you can download to browse coverage. -You can run CI locally. The various steps are located in [ci/\*.sh](../ci). +You can run CI locally. You only need [Go](https://golang.org), [nodejs](https://nodejs.org/en/) and [yarn](https://yarnpkg.com). -1. [fmt.sh](../ci/fmt.sh) which requires node (specifically prettier). -1. [lint.sh](../ci/lint.sh) which requires [shellcheck](https://github.com/koalaman/shellcheck#installing). -1. [test.sh](../ci/test.sh) -1. [run.sh](../ci/run.sh) which runs the above scripts in order. +See the scripts in [package.json](../package.json). -For coverage details locally, see `ci/out/coverage.html` after running [test.sh](../ci/test.sh). +1. `yarn fmt` performs code generation and formatting. +1. `yarn lint` performs linting. +1. `yarn test` runs tests. +1. `yarn all` runs the above scripts in parallel. + +For coverage details locally, see `ci/out/coverage.html` after running `yarn test`. + +CI is written with nodejs to enable running as much as possible concurrently. See [ci/image/Dockerfile](../ci/image/Dockerfile) for the installation of the CI dependencies on Ubuntu. -You can also run tests normally with `go test`. [test.sh](../ci/test.sh) just passes a default set of flags to -`go test` to collect coverage and also prettify the output. +You can also run tests normally with `go test`. `yarn test` just passes a default set of flags to +`go test` to collect coverage and runs the WASM tests. -You can pass flags to [test.sh](../ci/test.sh) if you want to run a specific test or otherwise +You can pass flags to `yarn test` if you want to run a specific test or otherwise control the behaviour of `go test` but also get coverage. Coverage percentage from codecov and the CI scripts will be different because they are calculated differently. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 38884369..774775d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,19 +4,19 @@ on: [push] jobs: fmt: runs-on: ubuntu-latest - container: nhooyr/websocket-ci@sha256:7f5513545dcbaa3ed06a2919acfd1cfbff1e6e0decc1602c98672a4aad2f68ab + container: nhooyr/websocket-ci@sha256:f8b6e53a9fd256bcf6c90029276385b9ec730b76a0d7ccf3ff19084bce210c50 steps: - uses: actions/checkout@v1 - run: yarn --frozen-lockfile && yarn fmt lint: runs-on: ubuntu-latest - container: nhooyr/websocket-ci@sha256:7f5513545dcbaa3ed06a2919acfd1cfbff1e6e0decc1602c98672a4aad2f68ab + container: nhooyr/websocket-ci@sha256:f8b6e53a9fd256bcf6c90029276385b9ec730b76a0d7ccf3ff19084bce210c50 steps: - uses: actions/checkout@v1 - run: yarn --frozen-lockfile && yarn lint test: runs-on: ubuntu-latest - container: nhooyr/websocket-ci@sha256:7f5513545dcbaa3ed06a2919acfd1cfbff1e6e0decc1602c98672a4aad2f68ab + container: nhooyr/websocket-ci@sha256:f8b6e53a9fd256bcf6c90029276385b9ec730b76a0d7ccf3ff19084bce210c50 steps: - uses: actions/checkout@v1 - run: yarn --frozen-lockfile && yarn test @@ -27,9 +27,3 @@ jobs: with: name: coverage path: ci/out/coverage.html - wasm: - runs-on: ubuntu-latest - container: nhooyr/websocket-ci@sha256:7f5513545dcbaa3ed06a2919acfd1cfbff1e6e0decc1602c98672a4aad2f68ab - steps: - - uses: actions/checkout@v1 - - run: yarn --frozen-lockfile && yarn wasm diff --git a/ci/all.ts b/ci/all.ts index 3b798e93..d40d3994 100755 --- a/ci/all.ts +++ b/ci/all.ts @@ -4,7 +4,6 @@ import { fmt, gen } from "./fmt" import { main } from "./lib" import { lint } from "./lint" import { test } from "./test" -import { wasm } from "./wasm" main(run) @@ -15,6 +14,5 @@ async function run(ctx: Promise) { fmt(ctx), lint(ctx), test(ctx), - wasm(ctx), ]) } diff --git a/ci/fmt.ts b/ci/fmt.ts index ba76f54b..ec39f065 100755 --- a/ci/fmt.ts +++ b/ci/fmt.ts @@ -2,7 +2,7 @@ import { exec, main } from "./lib" -if (process.argv[1] === __filename) { +if (require.main === module) { main(async (ctx: Promise) => { await gen(ctx) await fmt(ctx) diff --git a/ci/image/Dockerfile b/ci/image/Dockerfile index e17fe446..44c058d1 100644 --- a/ci/image/Dockerfile +++ b/ci/image/Dockerfile @@ -6,7 +6,10 @@ RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ RUN curl -sL https://deb.nodesource.com/setup_12.x | bash - RUN apt-get install -y nodejs chromium yarn -RUN git config --global color.ui always +COPY ./ci/image/gitignore /etc/git/ignore +RUN git config --system color.ui always +# Need to set set this explicitly for the system since github uses HOME=/home/github. +RUN git config --system core.excludesfile /etc/git/ignore ENV GOPATH=/root/gopath ENV PATH=$GOPATH/bin:$PATH @@ -14,9 +17,6 @@ ENV GOFLAGS="-mod=readonly" ENV PAGER=cat ENV CI=true -RUN mkdir -p ~/.config/git -COPY ./ci/image/gitignore ~/.config/git/ignore - # Cache go modules, build cache and yarn cache. COPY . /tmp/websocket RUN cd /tmp/websocket && \ diff --git a/ci/image/push.ts b/ci/image/push.ts index 15740fc1..ee2a50dc 100755 --- a/ci/image/push.ts +++ b/ci/image/push.ts @@ -13,11 +13,9 @@ async function run(ctx: Promise) { try { await spawn(ctx, "docker build -f ./ci/image/Dockerfile -t nhooyr/websocket-ci .", [], { - timeout: 180_000, stdio: "inherit", }) await spawn(ctx, "docker push nhooyr/websocket-ci", [], { - timeout: 30_000, stdio: "inherit", }) } finally { diff --git a/ci/lib.ts b/ci/lib.ts index 43e15a16..8a666447 100644 --- a/ci/lib.ts +++ b/ci/lib.ts @@ -5,7 +5,7 @@ import { ExecOptions, SpawnOptions } from "child_process" export async function main(fn: (ctx: Promise) => void, opts: { timeout: number } = { - timeout: 180_000, + timeout: 3 * 60_000, }) { const timer = new Timeout(); @@ -45,7 +45,6 @@ export async function main(fn: (ctx: Promise) => void, opts: { // TODO promisify native versions export async function exec(ctx: Promise, cmd: string, opts?: ExecOptions) { opts = { - timeout: 60_000, ...opts, } const p = cp.exec(cmd, opts) @@ -62,7 +61,6 @@ export async function spawn(ctx: Promise, cmd: string, args: string[], args = [] } opts = { - timeout: 60_000, shell: true, ...opts, } @@ -115,3 +113,10 @@ export function withCancel(p: Promise) { p: p, } } + + +export const wasmEnv = { + ...process.env, + GOOS: "js", + GOARCH: "wasm", +} diff --git a/ci/lint.ts b/ci/lint.ts index a411e588..ab02a72b 100755 --- a/ci/lint.ts +++ b/ci/lint.ts @@ -1,8 +1,8 @@ #!/usr/bin/env -S npx ts-node -P ci/tsconfig.json -import { exec, main } from "./lib" +import { exec, main, wasmEnv } from "./lib" -if (process.argv[1] === __filename) { +if (require.main === module) { main(lint) } @@ -13,6 +13,15 @@ export async function lint(ctx: Promise) { exec(ctx, "git ls-files '*.ts' | xargs npx eslint --max-warnings 0 --fix", { cwd: "ci", }), + wasmLint(ctx), ], ) } + + +async function wasmLint(ctx: Promise) { + await exec(ctx, "go install golang.org/x/lint/golint") + await exec(ctx, "golint -set_exit_status ./...", { + env: wasmEnv, + }) +} diff --git a/ci/test.ts b/ci/test.ts index a2063d8d..b64310dd 100755 --- a/ci/test.ts +++ b/ci/test.ts @@ -1,10 +1,12 @@ #!/usr/bin/env -S npx ts-node -P ci/tsconfig.json -import * as https from "https" +import cp from "child_process" +import * as events from "events" +import * as readline from "readline" import replaceInFile from "replace-in-file" -import { exec, main, selectCtx, spawn } from "./lib" +import { exec, main, selectCtx, spawn, wasmEnv } from "./lib" -if (process.argv[1] === __filename) { +if (require.main === module) { main(test) } @@ -26,19 +28,20 @@ export async function test(ctx: Promise) { args.push("./...") } - await spawn(ctx, "go", ["test", ...args], { - timeout: 60_000, + const p1 = spawn(ctx, "go", ["test", ...args], { stdio: "inherit", }) + const p2 = wasmTest(ctx) + await Promise.all([p1, p2]) // Depending on the code tested, we may not have replaced anything so we do not // check whether anything was replaced. await selectCtx(ctx, replaceInFile({ files: "./ci/out/coverage.prof", from: [ - /.+frame_stringer.go:.+\n/g, - /.+wsjstest:.+\n/g, - /.+wsecho:.+\n/g, + /.+frame_stringer.go.+\n/g, + /.+wsjstest.+\n/g, + /.+wsecho.+\n/g, ], to: "", })) @@ -46,12 +49,46 @@ export async function test(ctx: Promise) { let p: Promise = exec(ctx, "go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html") if (process.env.CI) { - const script = https.get("https://codecov.io/bash") - const p2 = spawn(ctx, "bash -Z -R . -f ci/out/coverage.prof", [], { - stdio: [script], - }) - p = Promise.all([p, p2]) + p = Promise.all([p, codecov(ctx)]) } await p } + + +async function wasmTest(ctx: Promise) { + await Promise.all([ + exec(ctx, "go install ./internal/wsjstest"), + exec(ctx, "go install github.com/agnivade/wasmbrowsertest"), + ]) + + const url = await startWasmTestServer(ctx) + + await exec(ctx, "go test -exec=wasmbrowsertest ./...", { + env: { + ...wasmEnv, + WS_ECHO_SERVER_URL: url, + }, + }) +} + +async function startWasmTestServer(ctx: Promise): Promise { + const wsjstest = cp.spawn("wsjstest") + ctx.finally(wsjstest.kill.bind(wsjstest)) + + const rl = readline.createInterface({ + input: wsjstest.stdout!, + }) + + try { + const p = events.once(rl, "line") + const a = await selectCtx(ctx, p) + return a[0] + } finally { + rl.close() + } +} + +function codecov(ctx: Promise) { + return exec(ctx, "curl -s https://codecov.io/bash | bash -s -- -Z -f ci/out/coverage.prof") +} diff --git a/ci/wasm.ts b/ci/wasm.ts deleted file mode 100755 index 29698c72..00000000 --- a/ci/wasm.ts +++ /dev/null @@ -1,67 +0,0 @@ -#!/usr/bin/env -S npx ts-node -P ci/tsconfig.json - -import cp from "child_process" -import * as events from "events" -import * as readline from "readline" -import { exec, main, selectCtx } from "./lib" - -if (process.argv[1] === __filename) { - main(wasm) -} - -const wasmEnv = { - ...process.env, - GOOS: "js", - GOARCH: "wasm", -} - -export async function wasm(ctx: Promise) { - await Promise.all([ - exec(ctx, "go vet ./...", { - env: wasmEnv, - }), - goLint(ctx), - wasmTest(ctx), - ], - ) -} - -async function goLint(ctx: Promise) { - await exec(ctx, "go install golang.org/x/lint/golint") - await exec(ctx, "golint -set_exit_status ./...", { - env: wasmEnv, - }) -} - -async function wasmTest(ctx: Promise) { - await Promise.all([ - exec(ctx, "go install ./internal/wsjstest"), - exec(ctx, "go install github.com/agnivade/wasmbrowsertest"), - ]) - - const url = await startServer(ctx) - - await exec(ctx, "go test -exec=wasmbrowsertest ./...", { - env: { - ...wasmEnv, - WS_ECHO_SERVER_URL: url, - }, - }) -} - -async function startServer(ctx: Promise): Promise { - const wsjstest = cp.spawn("wsjstest") - ctx.finally(wsjstest.kill.bind(wsjstest)) - - const rl = readline.createInterface({ - input: wsjstest.stdout!, - }) - - try { - const p = events.once(rl, "line") - const a = await selectCtx(ctx, p) - return a[0] - } finally { - rl.close() - } -} diff --git a/package.json b/package.json index 85f2735e..a8f436b0 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "@typescript-eslint/eslint-plugin": "^2.3.2", "@typescript-eslint/parser": "^2.3.2", "await-timeout": "^0.6.0", + "axios": "^0.19.0", "child-process-promise": "^2.2.1", "eslint": "^6.5.1", "eslint-config-prettier": "^6.3.0", @@ -20,7 +21,6 @@ "fmt": "./ci/fmt.ts", "lint": "./ci/lint.ts", "test": "./ci/test.ts", - "wasm": "./ci/wasm.ts", "ci": "./ci/all.ts", "push-ci-image": "ci/image/push.ts" } diff --git a/yarn.lock b/yarn.lock index 6fa6eeec..a8690d81 100644 --- a/yarn.lock +++ b/yarn.lock @@ -157,6 +157,14 @@ await-timeout@^0.6.0: resolved "https://registry.yarnpkg.com/await-timeout/-/await-timeout-0.6.0.tgz#efb52f5dba4d5fea6cff043705b09c97e7c403dc" integrity sha512-eMGnZxbqnmYTxCPqSYk5paIbPgcX8auC0UyVMScYMPY8pYmdk6o2wqQGw+SyN2hrhaDVZIQHVZX8DaHuBoRZcg== +axios@^0.19.0: + version "0.19.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.0.tgz#8e09bff3d9122e133f7b8101c8fbdd00ed3d2ab8" + integrity sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ== + dependencies: + follow-redirects "1.5.10" + is-buffer "^2.0.2" + balanced-match@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" @@ -270,6 +278,13 @@ cross-spawn@^6.0.5: shebang-command "^1.2.0" which "^1.2.9" +debug@=3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + debug@^2.6.8, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -570,6 +585,13 @@ flatted@^2.0.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08" integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg== +follow-redirects@1.5.10: + version "1.5.10" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" + integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== + dependencies: + debug "=3.1.0" + fs.realpath@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" @@ -708,6 +730,11 @@ is-arrayish@^0.2.1: resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= +is-buffer@^2.0.2: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" + integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A== + is-callable@^1.1.4: version "1.1.4" resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" From a78b6d4b975866b6b5d5d6150b648aacfb6e39f9 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 4 Oct 2019 22:00:42 -0400 Subject: [PATCH 175/519] Add CloseStatus Will make it much more convenient and less error prone to access the StatusCode of a *CloseError. --- assert_test.go | 68 ++++----------------------------------- conn_test.go | 39 +++++++++++----------- frame.go | 12 +++++++ frame_test.go | 42 ++++++++++++++++++++++++ internal/assert/assert.go | 63 ++++++++++++++++++++++++++++++++++++ websocket_js_test.go | 3 +- 6 files changed, 145 insertions(+), 82 deletions(-) create mode 100644 internal/assert/assert.go diff --git a/assert_test.go b/assert_test.go index e67ed539..26fd1d48 100644 --- a/assert_test.go +++ b/assert_test.go @@ -2,15 +2,12 @@ package websocket_test import ( "context" - "fmt" "math/rand" - "reflect" "strings" "time" - "github.com/google/go-cmp/cmp" - "nhooyr.io/websocket" + "nhooyr.io/websocket/internal/assert" "nhooyr.io/websocket/wsjson" ) @@ -18,59 +15,6 @@ func init() { rand.Seed(time.Now().UnixNano()) } -// https://github.com/google/go-cmp/issues/40#issuecomment-328615283 -func cmpDiff(exp, act interface{}) string { - return cmp.Diff(exp, act, deepAllowUnexported(exp, act)) -} - -func deepAllowUnexported(vs ...interface{}) cmp.Option { - m := make(map[reflect.Type]struct{}) - for _, v := range vs { - structTypes(reflect.ValueOf(v), m) - } - var typs []interface{} - for t := range m { - typs = append(typs, reflect.New(t).Elem().Interface()) - } - return cmp.AllowUnexported(typs...) -} - -func structTypes(v reflect.Value, m map[reflect.Type]struct{}) { - if !v.IsValid() { - return - } - switch v.Kind() { - case reflect.Ptr: - if !v.IsNil() { - structTypes(v.Elem(), m) - } - case reflect.Interface: - if !v.IsNil() { - structTypes(v.Elem(), m) - } - case reflect.Slice, reflect.Array: - for i := 0; i < v.Len(); i++ { - structTypes(v.Index(i), m) - } - case reflect.Map: - for _, k := range v.MapKeys() { - structTypes(v.MapIndex(k), m) - } - case reflect.Struct: - m[v.Type()] = struct{}{} - for i := 0; i < v.NumField(); i++ { - structTypes(v.Field(i), m) - } - } -} - -func assertEqualf(exp, act interface{}, f string, v ...interface{}) error { - if diff := cmpDiff(exp, act); diff != "" { - return fmt.Errorf(f+": %v", append(v, diff)...) - } - return nil -} - func assertJSONEcho(ctx context.Context, c *websocket.Conn, n int) error { exp := randString(n) err := wsjson.Write(ctx, c, exp) @@ -84,7 +28,7 @@ func assertJSONEcho(ctx context.Context, c *websocket.Conn, n int) error { return err } - return assertEqualf(exp, act, "unexpected JSON") + return assert.Equalf(exp, act, "unexpected JSON") } func assertJSONRead(ctx context.Context, c *websocket.Conn, exp interface{}) error { @@ -94,7 +38,7 @@ func assertJSONRead(ctx context.Context, c *websocket.Conn, exp interface{}) err return err } - return assertEqualf(exp, act, "unexpected JSON") + return assert.Equalf(exp, act, "unexpected JSON") } func randBytes(n int) []byte { @@ -126,13 +70,13 @@ func assertEcho(ctx context.Context, c *websocket.Conn, typ websocket.MessageTyp if err != nil { return err } - err = assertEqualf(typ, typ2, "unexpected data type") + err = assert.Equalf(typ, typ2, "unexpected data type") if err != nil { return err } - return assertEqualf(p, p2, "unexpected payload") + return assert.Equalf(p, p2, "unexpected payload") } func assertSubprotocol(c *websocket.Conn, exp string) error { - return assertEqualf(exp, c.Subprotocol(), "unexpected subprotocol") + return assert.Equalf(exp, c.Subprotocol(), "unexpected subprotocol") } diff --git a/conn_test.go b/conn_test.go index 12788c30..6ef77829 100644 --- a/conn_test.go +++ b/conn_test.go @@ -32,6 +32,7 @@ import ( "go.uber.org/multierr" "nhooyr.io/websocket" + "nhooyr.io/websocket/internal/assert" "nhooyr.io/websocket/internal/wsecho" "nhooyr.io/websocket/wsjson" "nhooyr.io/websocket/wspb" @@ -127,7 +128,7 @@ func TestHandshake(t *testing.T) { if err != nil { return fmt.Errorf("request is missing mycookie: %w", err) } - err = assertEqualf("myvalue", cookie.Value, "unexpected cookie value") + err = assert.Equalf("myvalue", cookie.Value, "unexpected cookie value") if err != nil { return err } @@ -219,7 +220,7 @@ func TestConn(t *testing.T) { } for h, exp := range headers { value := resp.Header.Get(h) - err := assertEqualf(exp, value, "unexpected value for header %v", h) + err := assert.Equalf(exp, value, "unexpected value for header %v", h) if err != nil { return err } @@ -276,11 +277,11 @@ func TestConn(t *testing.T) { time.Sleep(1) nc.SetWriteDeadline(time.Now().Add(time.Second * 15)) - err := assertEqualf(websocket.Addr{}, nc.LocalAddr(), "net conn local address is not equal to websocket.Addr") + err := assert.Equalf(websocket.Addr{}, nc.LocalAddr(), "net conn local address is not equal to websocket.Addr") if err != nil { return err } - err = assertEqualf(websocket.Addr{}, nc.RemoteAddr(), "net conn remote address is not equal to websocket.Addr") + err = assert.Equalf(websocket.Addr{}, nc.RemoteAddr(), "net conn remote address is not equal to websocket.Addr") if err != nil { return err } @@ -310,13 +311,13 @@ func TestConn(t *testing.T) { // Ensure the close frame is converted to an EOF and multiple read's after all return EOF. err2 := assertNetConnRead(nc, "hello") - err := assertEqualf(io.EOF, err2, "unexpected error") + err := assert.Equalf(io.EOF, err2, "unexpected error") if err != nil { return err } err2 = assertNetConnRead(nc, "hello") - return assertEqualf(io.EOF, err2, "unexpected error") + return assert.Equalf(io.EOF, err2, "unexpected error") }, }, { @@ -772,7 +773,7 @@ func TestConn(t *testing.T) { if err != nil { return err } - err = assertEqualf("hi", v, "unexpected JSON") + err = assert.Equalf("hi", v, "unexpected JSON") if err != nil { return err } @@ -780,7 +781,7 @@ func TestConn(t *testing.T) { if err != nil { return err } - return assertEqualf("hi", string(b), "unexpected JSON") + return assert.Equalf("hi", string(b), "unexpected JSON") }, client: func(ctx context.Context, c *websocket.Conn) error { err := wsjson.Write(ctx, c, "hi") @@ -1079,11 +1080,11 @@ func TestAutobahn(t *testing.T) { if err != nil { return err } - err = assertEqualf(typ, actTyp, "unexpected message type") + err = assert.Equalf(typ, actTyp, "unexpected message type") if err != nil { return err } - return assertEqualf(p, p2, "unexpected message") + return assert.Equalf(p, p2, "unexpected message") }) } } @@ -1859,7 +1860,7 @@ func assertCloseStatus(err error, code websocket.StatusCode) error { if !errors.As(err, &cerr) { return fmt.Errorf("no websocket close error in error chain: %+v", err) } - return assertEqualf(code, cerr.Code, "unexpected status code") + return assert.Equalf(code, cerr.Code, "unexpected status code") } func assertProtobufRead(ctx context.Context, c *websocket.Conn, exp interface{}) error { @@ -1871,7 +1872,7 @@ func assertProtobufRead(ctx context.Context, c *websocket.Conn, exp interface{}) return err } - return assertEqualf(exp, act, "unexpected protobuf") + return assert.Equalf(exp, act, "unexpected protobuf") } func assertNetConnRead(r io.Reader, exp string) error { @@ -1880,7 +1881,7 @@ func assertNetConnRead(r io.Reader, exp string) error { if err != nil { return err } - return assertEqualf(exp, string(act), "unexpected net conn read") + return assert.Equalf(exp, string(act), "unexpected net conn read") } func assertErrorContains(err error, exp string) error { @@ -1902,11 +1903,11 @@ func assertReadFrame(ctx context.Context, c *websocket.Conn, opcode websocket.Op if err != nil { return err } - err = assertEqualf(opcode, actOpcode, "unexpected frame opcode with payload %q", actP) + err = assert.Equalf(opcode, actOpcode, "unexpected frame opcode with payload %q", actP) if err != nil { return err } - return assertEqualf(p, actP, "unexpected frame %v payload", opcode) + return assert.Equalf(p, actP, "unexpected frame %v payload", opcode) } func assertReadCloseFrame(ctx context.Context, c *websocket.Conn, code websocket.StatusCode) error { @@ -1914,7 +1915,7 @@ func assertReadCloseFrame(ctx context.Context, c *websocket.Conn, code websocket if err != nil { return err } - err = assertEqualf(websocket.OpClose, actOpcode, "unexpected frame opcode with payload %q", actP) + err = assert.Equalf(websocket.OpClose, actOpcode, "unexpected frame opcode with payload %q", actP) if err != nil { return err } @@ -1922,7 +1923,7 @@ func assertReadCloseFrame(ctx context.Context, c *websocket.Conn, code websocket if err != nil { return fmt.Errorf("failed to parse close frame payload: %w", err) } - return assertEqualf(ce.Code, code, "unexpected frame close frame code with payload %q", actP) + return assert.Equalf(ce.Code, code, "unexpected frame close frame code with payload %q", actP) } func assertCloseHandshake(ctx context.Context, c *websocket.Conn, code websocket.StatusCode, reason string) error { @@ -1960,11 +1961,11 @@ func assertReadMessage(ctx context.Context, c *websocket.Conn, typ websocket.Mes if err != nil { return err } - err = assertEqualf(websocket.MessageText, actTyp, "unexpected frame opcode with payload %q", actP) + err = assert.Equalf(websocket.MessageText, actTyp, "unexpected frame opcode with payload %q", actP) if err != nil { return err } - return assertEqualf(p, actP, "unexpected frame %v payload", actTyp) + return assert.Equalf(p, actP, "unexpected frame %v payload", actTyp) } func BenchmarkConn(b *testing.B) { diff --git a/frame.go b/frame.go index 4b170c5f..796c1c85 100644 --- a/frame.go +++ b/frame.go @@ -2,6 +2,7 @@ package websocket import ( "encoding/binary" + "errors" "fmt" "io" "math" @@ -252,6 +253,17 @@ func (ce CloseError) Error() string { return fmt.Sprintf("status = %v and reason = %q", ce.Code, ce.Reason) } +// CloseStatus is a convenience wrapper around xerrors.As to grab +// the status code from a *CloseError. If the passed error is nil +// or not a *CloseError, the returned StatusCode will be -1. +func CloseStatus(err error) StatusCode { + var ce *CloseError + if errors.As(err, &ce) { + return ce.Code + } + return -1 +} + func parseClosePayload(p []byte) (CloseError, error) { if len(p) == 0 { return CloseError{ diff --git a/frame_test.go b/frame_test.go index 7d2a5719..a4fead49 100644 --- a/frame_test.go +++ b/frame_test.go @@ -13,6 +13,8 @@ import ( "time" "github.com/google/go-cmp/cmp" + + "nhooyr.io/websocket/internal/assert" ) func init() { @@ -376,3 +378,43 @@ func BenchmarkXOR(b *testing.B) { }) } } + +func TestCloseStatus(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + in error + exp StatusCode + }{ + { + name: "nil", + in: nil, + exp: -1, + }, + { + name: "io.EOF", + in: io.EOF, + exp: -1, + }, + { + name: "StatusInternalError", + in: &CloseError{ + Code: StatusInternalError, + }, + exp: StatusInternalError, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + err := assert.Equalf(tc.exp, CloseStatus(tc.in), "unexpected close status") + if err != nil { + t.Fatal(err) + } + }) + } +} diff --git a/internal/assert/assert.go b/internal/assert/assert.go new file mode 100644 index 00000000..e57abfd9 --- /dev/null +++ b/internal/assert/assert.go @@ -0,0 +1,63 @@ +package assert + +import ( + "fmt" + "reflect" + + "github.com/google/go-cmp/cmp" +) + +// https://github.com/google/go-cmp/issues/40#issuecomment-328615283 +func cmpDiff(exp, act interface{}) string { + return cmp.Diff(exp, act, deepAllowUnexported(exp, act)) +} + +func deepAllowUnexported(vs ...interface{}) cmp.Option { + m := make(map[reflect.Type]struct{}) + for _, v := range vs { + structTypes(reflect.ValueOf(v), m) + } + var typs []interface{} + for t := range m { + typs = append(typs, reflect.New(t).Elem().Interface()) + } + return cmp.AllowUnexported(typs...) +} + +func structTypes(v reflect.Value, m map[reflect.Type]struct{}) { + if !v.IsValid() { + return + } + switch v.Kind() { + case reflect.Ptr: + if !v.IsNil() { + structTypes(v.Elem(), m) + } + case reflect.Interface: + if !v.IsNil() { + structTypes(v.Elem(), m) + } + case reflect.Slice, reflect.Array: + for i := 0; i < v.Len(); i++ { + structTypes(v.Index(i), m) + } + case reflect.Map: + for _, k := range v.MapKeys() { + structTypes(v.MapIndex(k), m) + } + case reflect.Struct: + m[v.Type()] = struct{}{} + for i := 0; i < v.NumField(); i++ { + structTypes(v.Field(i), m) + } + } +} + +// Equalf compares exp to act and if they are not equal, returns +// an error describing an error. +func Equalf(exp, act interface{}, f string, v ...interface{}) error { + if diff := cmpDiff(exp, act); diff != "" { + return fmt.Errorf(f+": %v", append(v, diff)...) + } + return nil +} diff --git a/websocket_js_test.go b/websocket_js_test.go index a3bb7639..9808e708 100644 --- a/websocket_js_test.go +++ b/websocket_js_test.go @@ -8,6 +8,7 @@ import ( "time" "nhooyr.io/websocket" + "nhooyr.io/websocket/internal/assert" ) func TestConn(t *testing.T) { @@ -29,7 +30,7 @@ func TestConn(t *testing.T) { t.Fatal(err) } - err = assertEqualf(&http.Response{}, resp, "unexpected http response") + err = assert.Equalf(&http.Response{}, resp, "unexpected http response") if err != nil { t.Fatal(err) } From 0919bdbf52368af78a7a2b04c3944ac55ea68efc Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 5 Oct 2019 22:23:48 -0400 Subject: [PATCH 176/519] Update docs for CloseStatus --- README.md | 4 +++- conn_test.go | 4 ++-- doc.go | 1 + example_test.go | 4 +--- frame.go | 4 ++-- frame_test.go | 2 +- 6 files changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 3ab395e6..f6afbd8c 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,9 @@ go get nhooyr.io/websocket For a production quality example that shows off the full API, see the [echo example on the godoc](https://godoc.org/nhooyr.io/websocket#example-package--Echo). On github, the example is at [example_echo_test.go](./example_echo_test.go). -Use the [errors.As](https://golang.org/pkg/errors/#As) function [new in Go 1.13](https://golang.org/doc/go1.13#error_wrapping) to check for [websocket.CloseError](https://godoc.org/nhooyr.io/websocket#CloseError). See the [CloseError godoc example](https://godoc.org/nhooyr.io/websocket#example-CloseError). +Use the [errors.As](https://golang.org/pkg/errors/#As) function [new in Go 1.13](https://golang.org/doc/go1.13#error_wrapping) to check for [websocket.CloseError](https://godoc.org/nhooyr.io/websocket#CloseError). +There is also [websocket.CloseStatus](https://godoc.org/nhooyr.io/websocket#CloseStatus) to quickly grab the close status code out of a [websocket.CloseError](https://godoc.org/nhooyr.io/websocket#CloseError). +See the [CloseError godoc example](https://godoc.org/nhooyr.io/websocket#example-CloseError). ### Server diff --git a/conn_test.go b/conn_test.go index 6ef77829..0e012bf7 100644 --- a/conn_test.go +++ b/conn_test.go @@ -586,8 +586,8 @@ func TestConn(t *testing.T) { return err } _, _, err = c.Read(ctx) - cerr := &websocket.CloseError{} - if !errors.As(err, cerr) || cerr.Code != websocket.StatusProtocolError { + var cerr websocket.CloseError + if !errors.As(err, &cerr) || cerr.Code != websocket.StatusProtocolError { return fmt.Errorf("expected close error with StatusProtocolError: %+v", err) } return nil diff --git a/doc.go b/doc.go index a17bfb05..1610eed1 100644 --- a/doc.go +++ b/doc.go @@ -16,6 +16,7 @@ // comparison with existing implementations. // // Use the errors.As function new in Go 1.13 to check for websocket.CloseError. +// Or use the CloseStatus function to grab the StatusCode out of a websocket.CloseError // See the CloseError example. // // Wasm diff --git a/example_test.go b/example_test.go index 2cedddf3..1cb3d799 100644 --- a/example_test.go +++ b/example_test.go @@ -4,7 +4,6 @@ package websocket_test import ( "context" - "errors" "log" "net/http" "time" @@ -76,8 +75,7 @@ func ExampleCloseError() { defer c.Close(websocket.StatusInternalError, "the sky is falling") _, _, err = c.Reader(ctx) - var cerr websocket.CloseError - if !errors.As(err, &cerr) || cerr.Code != websocket.StatusNormalClosure { + if websocket.CloseStatus(err) != websocket.StatusNormalClosure { log.Fatalf("expected to be disconnected with StatusNormalClosure but got: %+v", err) return } diff --git a/frame.go b/frame.go index 796c1c85..b5301d78 100644 --- a/frame.go +++ b/frame.go @@ -253,11 +253,11 @@ func (ce CloseError) Error() string { return fmt.Sprintf("status = %v and reason = %q", ce.Code, ce.Reason) } -// CloseStatus is a convenience wrapper around xerrors.As to grab +// CloseStatus is a convenience wrapper around errors.As to grab // the status code from a *CloseError. If the passed error is nil // or not a *CloseError, the returned StatusCode will be -1. func CloseStatus(err error) StatusCode { - var ce *CloseError + var ce CloseError if errors.As(err, &ce) { return ce.Code } diff --git a/frame_test.go b/frame_test.go index a4fead49..8c5760e8 100644 --- a/frame_test.go +++ b/frame_test.go @@ -399,7 +399,7 @@ func TestCloseStatus(t *testing.T) { }, { name: "StatusInternalError", - in: &CloseError{ + in: CloseError{ Code: StatusInternalError, }, exp: StatusInternalError, From 90515999931e33b47cbfc605dee850a9fb56a496 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 6 Oct 2019 12:24:51 -0400 Subject: [PATCH 177/519] Ignore coverage from assert pkg --- ci/test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ci/test.ts b/ci/test.ts index b64310dd..e3fb5a7f 100755 --- a/ci/test.ts +++ b/ci/test.ts @@ -40,8 +40,9 @@ export async function test(ctx: Promise) { files: "./ci/out/coverage.prof", from: [ /.+frame_stringer.go.+\n/g, - /.+wsjstest.+\n/g, - /.+wsecho.+\n/g, + /.+wsjstest\/.+\n/g, + /.+wsecho\/.+\n/g, + /.+assert\/.+\n/g, ], to: "", })) From e795e467cbeb0959b84477febeafbf5867b850da Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 8 Oct 2019 01:49:21 -0400 Subject: [PATCH 178/519] Cleanup CI --- ci/all.ts | 6 +---- ci/fmt.ts | 14 ++++++----- ci/lib.ts | 25 +++++++++---------- ci/lint.ts | 16 ++++++------- ci/test.ts | 25 +++++++------------ ci/tools.go | 2 -- go.mod | 11 +-------- go.sum | 69 ----------------------------------------------------- 8 files changed, 39 insertions(+), 129 deletions(-) diff --git a/ci/all.ts b/ci/all.ts index d40d3994..c0d16a8b 100755 --- a/ci/all.ts +++ b/ci/all.ts @@ -10,9 +10,5 @@ main(run) async function run(ctx: Promise) { await gen(ctx) - await Promise.all([ - fmt(ctx), - lint(ctx), - test(ctx), - ]) + await Promise.all([fmt(ctx), lint(ctx), test(ctx)]) } diff --git a/ci/fmt.ts b/ci/fmt.ts index ec39f065..6d7543ed 100755 --- a/ci/fmt.ts +++ b/ci/fmt.ts @@ -11,12 +11,14 @@ if (require.main === module) { export async function fmt(ctx: Promise) { await Promise.all([ - exec(ctx, "go mod tidy"), - exec(ctx, "gofmt -w -s ."), - exec(ctx, `go run go.coder.com/go-tools/cmd/goimports -w "-local=$(go list -m)" .`), - exec(ctx, `npx prettier --write --print-width=120 --no-semi --trailing-comma=all --loglevel=silent $(git ls-files "*.yaml" "*.yml" "*.md")`), - ], - ) + exec(ctx, "go mod tidy"), + exec(ctx, "gofmt -w -s ."), + exec(ctx, `go run go.coder.com/go-tools/cmd/goimports -w "-local=$(go list -m)" .`), + exec( + ctx, + `npx prettier --write --print-width=120 --no-semi --trailing-comma=all --loglevel=silent $(git ls-files "*.yaml" "*.yml" "*.md" "*.ts")`, + ), + ]) if (process.env.CI) { const r = await exec(ctx, "git ls-files --other --modified --exclude-standard") diff --git a/ci/lib.ts b/ci/lib.ts index 8a666447..b6e5d0b4 100644 --- a/ci/lib.ts +++ b/ci/lib.ts @@ -2,13 +2,15 @@ import Timeout from "await-timeout" import cp from "child-process-promise" import { ExecOptions, SpawnOptions } from "child_process" -export async function main(fn: (ctx: Promise) => void, opts: { - timeout: number -} = { - timeout: 3 * 60_000, -}) { - - const timer = new Timeout(); +export async function main( + fn: (ctx: Promise) => void, + opts: { + timeout: number + } = { + timeout: 3 * 60_000, + }, +) { + const timer = new Timeout() let ctx: Promise = timer.set(opts.timeout, "context timed out") const interrupted = new Promise((res, rej) => { @@ -24,7 +26,7 @@ export async function main(fn: (ctx: Promise) => void, opts: { }) ctx = Promise.race([ctx, interrupted]) - const {res, rej, p} = withCancel(ctx) + const { res, rej, p } = withCancel(ctx) ctx = p try { @@ -82,14 +84,14 @@ async function init(ctx: Promise) { } export async function selectCtx(ctx: Promise, p: Promise): Promise { - return await Promise.race([ctx, p]) as Promise + return (await Promise.race([ctx, p])) as Promise } const cancelSymbol = Symbol() export function withCancel(p: Promise) { - let rej: () => void; - let res: () => void; + let rej: () => void + let res: () => void const p2 = new Promise((res2, rej2) => { res = res2 rej = () => { @@ -114,7 +116,6 @@ export function withCancel(p: Promise) { } } - export const wasmEnv = { ...process.env, GOOS: "js", diff --git a/ci/lint.ts b/ci/lint.ts index ab02a72b..22a47df3 100755 --- a/ci/lint.ts +++ b/ci/lint.ts @@ -8,17 +8,15 @@ if (require.main === module) { export async function lint(ctx: Promise) { await Promise.all([ - exec(ctx, "go vet ./..."), - exec(ctx, "go run golang.org/x/lint/golint -set_exit_status ./..."), - exec(ctx, "git ls-files '*.ts' | xargs npx eslint --max-warnings 0 --fix", { - cwd: "ci", - }), - wasmLint(ctx), - ], - ) + exec(ctx, "go vet ./..."), + exec(ctx, "go run golang.org/x/lint/golint -set_exit_status ./..."), + exec(ctx, "git ls-files '*.ts' | xargs npx eslint --max-warnings 0 --fix", { + cwd: "ci", + }), + wasmLint(ctx), + ]) } - async function wasmLint(ctx: Promise) { await exec(ctx, "go install golang.org/x/lint/golint") await exec(ctx, "golint -set_exit_status ./...", { diff --git a/ci/test.ts b/ci/test.ts index e3fb5a7f..aa1a0029 100755 --- a/ci/test.ts +++ b/ci/test.ts @@ -11,11 +11,7 @@ if (require.main === module) { } export async function test(ctx: Promise) { - const args = [ - "-parallel=1024", - "-coverprofile=ci/out/coverage.prof", - "-coverpkg=./...", - ] + const args = ["-parallel=1024", "-coverprofile=ci/out/coverage.prof", "-coverpkg=./..."] if (process.env.CI) { args.push("-race") @@ -36,16 +32,14 @@ export async function test(ctx: Promise) { // Depending on the code tested, we may not have replaced anything so we do not // check whether anything was replaced. - await selectCtx(ctx, replaceInFile({ - files: "./ci/out/coverage.prof", - from: [ - /.+frame_stringer.go.+\n/g, - /.+wsjstest\/.+\n/g, - /.+wsecho\/.+\n/g, - /.+assert\/.+\n/g, - ], - to: "", - })) + await selectCtx( + ctx, + replaceInFile({ + files: "./ci/out/coverage.prof", + from: [/.+frame_stringer.go.+\n/g, /.+wsjstest\/.+\n/g, /.+wsecho\/.+\n/g, /.+assert\/.+\n/g], + to: "", + }), + ) let p: Promise = exec(ctx, "go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html") @@ -56,7 +50,6 @@ export async function test(ctx: Promise) { await p } - async function wasmTest(ctx: Promise) { await Promise.all([ exec(ctx, "go install ./internal/wsjstest"), diff --git a/ci/tools.go b/ci/tools.go index 1ec11eb4..4cbd71cf 100644 --- a/ci/tools.go +++ b/ci/tools.go @@ -8,6 +8,4 @@ import ( _ "go.coder.com/go-tools/cmd/goimports" _ "golang.org/x/lint/golint" _ "golang.org/x/tools/cmd/stringer" - _ "gotest.tools/gotestsum" - _ "mvdan.cc/sh/cmd/shfmt" ) diff --git a/go.mod b/go.mod index 8bd3b887..0e398366 100644 --- a/go.mod +++ b/go.mod @@ -6,29 +6,20 @@ require ( github.com/agnivade/wasmbrowsertest v0.3.1 github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6 // indirect github.com/chromedp/chromedp v0.4.0 // indirect - github.com/fatih/color v1.7.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/go-interpreter/wagon v0.6.0 // indirect github.com/golang/protobuf v1.3.2 github.com/google/go-cmp v0.3.1 github.com/google/pprof v0.0.0-20190908185732-236ed259b199 // indirect - github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect github.com/kr/pretty v0.1.0 // indirect github.com/mailru/easyjson v0.7.0 // indirect - github.com/mattn/go-colorable v0.1.2 // indirect - github.com/mattn/go-isatty v0.0.9 // indirect - github.com/pkg/errors v0.8.1 // indirect - github.com/sirupsen/logrus v1.4.2 // indirect - github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/testify v1.4.0 // indirect go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16 go.uber.org/atomic v1.4.0 // indirect go.uber.org/multierr v1.1.0 golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac - golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect golang.org/x/sys v0.0.0-20190927073244-c990c680b611 // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect - gotest.tools/gotestsum v0.3.5 - mvdan.cc/sh v2.6.4+incompatible ) diff --git a/go.sum b/go.sum index 0bcfc0b4..afd13b74 100644 --- a/go.sum +++ b/go.sum @@ -16,12 +16,6 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -github.com/fatih/color v1.6.0 h1:66qjqZk8kalYAvDRtM1AdAJQI0tj4Wrue3Eq3B3pmFU= -github.com/fatih/color v1.6.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c h1:DLLAPVFrk9iNzljMKF512CUmrFImQ6WU3sDiUS4IRqk= github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc= github.com/go-interpreter/wagon v0.6.0 h1:BBxDxjiJiHgw9EdkYXAWs8NHhwnazZ5P2EWBW5hFNWw= @@ -32,26 +26,16 @@ github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f h1:Jnx61latede7zDD3DiiP4gmNz33uK0U5HDUaF0a/HVQ= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190908185732-236ed259b199 h1:sEyCq3pOT7tNC+3gcLI7sZkBDgntZ6wQJNmr9lmIjIc= github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/jonboulle/clockwork v0.1.0 h1:VKV+ZcuP6l3yW9doeqz6ziZGgcynBVQO+obU0+0hcPo= -github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307 h1:vl4eIlySbjertFaNwiMjXsGrFVK25aOWLq7n+3gh2ls= github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -64,38 +48,9 @@ github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= -github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= -github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI= -github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.9 h1:d5US/mDsogSGW37IV293h//ZFaeajb69h+EHFsv2xGg= -github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.8.0 h1:VkHVNpR4iVnU8XQR6DBm8BqYjN7CRzw+xKUbVVbbW9w= -github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sirupsen/logrus v1.0.5 h1:8c8b5uO0zS4X6RPl/sd1ENwSkIc0/H2PaHxE3udaE8I= -github.com/sirupsen/logrus v1.0.5/go.mod h1:pMByvHTf9Beacp5x1UXfOR9xyW/9antXMhjMPG0dEzc= -github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= -github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= -github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4= -github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc h1:RTUQlKzoZZVG3umWNzOYeFecQLIh+dbxXvJp1zPQJTI= @@ -106,27 +61,18 @@ go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -golang.org/x/crypto v0.0.0-20180426230345-b49d69b5da94/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac h1:8R1esu+8QioDxo4E4mX6bFztO+dMTM49DNAaWfO5OeY= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -141,24 +87,9 @@ golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72 h1:bw9doJza/SFBEweII/rHQh3 golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/airbrake/gobrake.v2 v2.0.9 h1:7z2uVWwn7oVeeugY1DtlPAy5H+KYgB1KeKTnqjNatLo= -gopkg.in/airbrake/gobrake.v2 v2.0.9/go.mod h1:/h5ZAUhDkGaJfjzjKLSjv6zCL6O0LLBxU4K+aSYdM/U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2 h1:OAj3g0cR6Dx/R07QgQe8wkA9RNjB2u4i700xBkIT4e0= -gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gotest.tools v2.1.0+incompatible h1:5USw7CrJBYKqjg9R7QlA6jzqZKEAtvW82aNmsxxGPxw= -gotest.tools v2.1.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -gotest.tools/gotestsum v0.3.5 h1:VePOWRsuWFYpfp/G8mbmOZKxO5T3501SEGQRUdvq7h0= -gotest.tools/gotestsum v0.3.5/go.mod h1:Mnf3e5FUzXbkCfynWBGOwLssY7gTQgCHObK9tMpAriY= -mvdan.cc/sh v2.6.4+incompatible h1:eD6tDeh0pw+/TOTI1BBEryZ02rD2nMcFsgcvde7jffM= -mvdan.cc/sh v2.6.4+incompatible/go.mod h1:IeeQbZq+x2SUGBensq/jge5lLQbS3XT2ktyp3wrt4x8= From d432e6ba5283b1599e2592c4da24a63ef6951580 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 7 Oct 2019 01:11:51 -0400 Subject: [PATCH 179/519] Implement complete close handshake I changed my mind after #103 as browsers include a wasClean event to indicate whether the connection was closed cleanly. From my tests, if a server using this library prior to this commit initiates the close handshake, wasClean will be false for the browser as the connection was closed before it could respond with a close frame. Thus, I believe it's necessary to fully implement the close handshake. @stephenyama You'll enjoy this. --- conn.go | 104 ++++++++++++++++++++++++++++-------- conn_common.go | 4 ++ conn_test.go | 9 ++++ go.mod | 1 + websocket_js.go | 48 ++++++++++++----- websocket_js_export_test.go | 17 ------ websocket_js_test.go | 5 -- 7 files changed, 130 insertions(+), 58 deletions(-) delete mode 100644 websocket_js_export_test.go diff --git a/conn.go b/conn.go index d74b8753..73d64904 100644 --- a/conn.go +++ b/conn.go @@ -16,6 +16,10 @@ import ( "sync" "sync/atomic" "time" + + "golang.org/x/xerrors" + + "nhooyr.io/websocket/internal/bpool" ) // Conn represents a WebSocket connection. @@ -62,6 +66,7 @@ type Conn struct { writeMsgOpcode opcode writeMsgCtx context.Context readMsgLeft int64 + readCloseFrame CloseError // Used to ensure the previous reader is read till EOF before allowing // a new one. @@ -69,6 +74,7 @@ type Conn struct { // readFrameLock is acquired to read from bw. readFrameLock chan struct{} isReadClosed *atomicInt64 + isCloseHandshake *atomicInt64 readHeaderBuf []byte controlPayloadBuf []byte @@ -96,6 +102,7 @@ func (c *Conn) init() { c.writeFrameLock = make(chan struct{}, 1) c.readFrameLock = make(chan struct{}, 1) + c.isCloseHandshake = &atomicInt64{} c.setReadTimeout = make(chan context.Context) c.setWriteTimeout = make(chan context.Context) @@ -230,7 +237,7 @@ func (c *Conn) readTillMsg(ctx context.Context) (header, error) { } func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { - err := c.acquireLock(context.Background(), c.readFrameLock) + err := c.acquireLock(ctx, c.readFrameLock) if err != nil { return header{}, err } @@ -308,11 +315,22 @@ func (c *Conn) handleControl(ctx context.Context, h header) error { c.Close(StatusProtocolError, err.Error()) return c.closeErr } + // This ensures the closeErr of the Conn is always the received CloseError // in case the echo close frame write fails. // See https://github.com/nhooyr/websocket/issues/109 c.setCloseErr(fmt.Errorf("received close frame: %w", ce)) - c.writeClose(b, nil) + + c.readCloseFrame = ce + + func() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + c.writeControl(ctx, opClose, b) + }() + + // We close with nil since the error is already set above. + c.close(nil) return c.closeErr default: panic(fmt.Sprintf("websocket: unexpected control opcode: %#v", h)) @@ -347,6 +365,15 @@ func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) { return 0, nil, fmt.Errorf("websocket connection read closed") } + if c.isCloseHandshake.Load() == 1 { + select { + case <-ctx.Done(): + return 0, nil, fmt.Errorf("failed to get reader: %w", ctx.Err()) + case <-c.closed: + return 0, nil, fmt.Errorf("failed to get reader: %w", c.closeErr) + } + } + typ, r, err := c.reader(ctx) if err != nil { return 0, nil, fmt.Errorf("failed to get reader: %w", err) @@ -772,27 +799,28 @@ func (c *Conn) writePong(p []byte) error { // Close closes the WebSocket connection with the given status code and reason. // -// It will write a WebSocket close frame with a timeout of 5 seconds. +// It will write a WebSocket close frame and then wait for the peer to respond +// with its own close frame. The entire process must complete within 10 seconds. +// Thus, it implements the full WebSocket close handshake. +// // The connection can only be closed once. Additional calls to Close // are no-ops. // -// This does not perform a WebSocket close handshake. -// See https://github.com/nhooyr/websocket/issues/103 for details on why. -// // The maximum length of reason must be 125 bytes otherwise an internal // error will be sent to the peer. For this reason, you should avoid // sending a dynamic reason. // -// Close will unblock all goroutines interacting with the connection. +// Close will unblock all goroutines interacting with the connection once +// complete. func (c *Conn) Close(code StatusCode, reason string) error { - err := c.exportedClose(code, reason) + err := c.closeHandshake(code, reason) if err != nil { return fmt.Errorf("failed to close websocket connection: %w", err) } return nil } -func (c *Conn) exportedClose(code StatusCode, reason string) error { +func (c *Conn) closeHandshake(code StatusCode, reason string) error { ce := CloseError{ Code: code, Reason: reason, @@ -810,34 +838,64 @@ func (c *Conn) exportedClose(code StatusCode, reason string) error { p, _ = ce.bytes() } + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + // Ensures the connection is closed if everything below succeeds. + // Up here because we must release the read lock first. + // nil because of the setCloseErr call below. + defer c.close(nil) + // CloseErrors sent are made opaque to prevent applications from thinking // they received a given status. sentErr := fmt.Errorf("sent close frame: %v", ce) - err = c.writeClose(p, sentErr) + // Other connections should only see this error. + c.setCloseErr(sentErr) + + err = c.writeControl(ctx, opClose, p) if err != nil { return err } - if !errors.Is(c.closeErr, sentErr) { - return c.closeErr + // Wait for close frame from peer. + err = c.waitClose(ctx) + // We didn't read a close frame. + if c.readCloseFrame == (CloseError{}) { + if ctx.Err() != nil { + return xerrors.Errorf("failed to wait for peer close frame: %w", ctx.Err()) + } + // We need to make the err returned from c.waitClose accurate. + return xerrors.Errorf("failed to read peer close frame for unknown reason") } - return nil } -func (c *Conn) writeClose(p []byte, cerr error) error { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - defer cancel() +func (c *Conn) waitClose(ctx context.Context) error { + b := bpool.Get() + buf := b.Bytes() + buf = buf[:cap(buf)] + defer bpool.Put(b) - // If this fails, the connection had to have died. - err := c.writeControl(ctx, opClose, p) - if err != nil { - return err - } + // Prevent reads from user code as we are going to be + // discarding all messages so they cannot rely on any ordering. + c.isCloseHandshake.Store(1) - c.close(cerr) + // From this point forward, any reader we receive means we are + // now the sole readers of the connection and so it is safe + // to discard all payloads. - return nil + for { + _, r, err := c.reader(ctx) + if err != nil { + return err + } + + // Discard all payloads. + _, err = io.CopyBuffer(ioutil.Discard, r, buf) + if err != nil { + return err + } + } } // Ping sends a ping to the peer and waits for a pong. diff --git a/conn_common.go b/conn_common.go index 8233e4a6..162dc80d 100644 --- a/conn_common.go +++ b/conn_common.go @@ -230,3 +230,7 @@ func (v *atomicInt64) String() string { func (v *atomicInt64) Increment(delta int64) int64 { return atomic.AddInt64(&v.v, delta) } + +func (v *atomicInt64) CAS(old, new int64) (swapped bool) { + return atomic.CompareAndSwapInt64(&v.v, old, new) +} diff --git a/conn_test.go b/conn_test.go index 0e012bf7..970d2350 100644 --- a/conn_test.go +++ b/conn_test.go @@ -856,6 +856,15 @@ func TestConn(t *testing.T) { return nil }, }, + { + name: "closeHandshake", + server: func(ctx context.Context, c *websocket.Conn) error { + return c.Close(websocket.StatusNormalClosure, "") + }, + client: func(ctx context.Context, c *websocket.Conn) error { + return c.Close(websocket.StatusNormalClosure, "") + }, + }, } for _, tc := range testCases { tc := tc diff --git a/go.mod b/go.mod index 0e398366..2a5bbae3 100644 --- a/go.mod +++ b/go.mod @@ -21,5 +21,6 @@ require ( golang.org/x/sys v0.0.0-20190927073244-c990c680b611 // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72 + golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect ) diff --git a/websocket_js.go b/websocket_js.go index 4563a1bc..27b83715 100644 --- a/websocket_js.go +++ b/websocket_js.go @@ -36,8 +36,7 @@ type Conn struct { readBufMu sync.Mutex readBuf []wsjs.MessageEvent - // Only used by tests - receivedCloseFrame chan struct{} + closeEventCh chan wsjs.CloseEvent } func (c *Conn) close(err error) { @@ -58,10 +57,11 @@ func (c *Conn) init() { c.isReadClosed = &atomicInt64{} - c.receivedCloseFrame = make(chan struct{}) + c.closeEventCh = make(chan wsjs.CloseEvent, 1) c.releaseOnClose = c.ws.OnClose(func(e wsjs.CloseEvent) { - close(c.receivedCloseFrame) + c.closeEventCh <- e + close(c.closeEventCh) cerr := CloseError{ Code: StatusCode(e.Code), @@ -193,24 +193,46 @@ func (c *Conn) isClosed() bool { } // Close closes the websocket with the given code and reason. +// It will wait until the peer responds with a close frame +// or the connection is closed. +// It thus performs the full WebSocket close handshake. func (c *Conn) Close(code StatusCode, reason string) error { + err := c.exportedClose(code, reason) + if err != nil { + return fmt.Errorf("failed to close websocket: %w", err) + } + return nil +} + +func (c *Conn) exportedClose(code StatusCode, reason string) error { if c.isClosed() { return fmt.Errorf("already closed: %w", c.closeErr) } - err := fmt.Errorf("sent close frame: %v", CloseError{ + cerr := CloseError{ Code: code, Reason: reason, - }) - - err2 := c.ws.Close(int(code), reason) - if err2 != nil { - err = err2 } - c.close(err) + closeErr := fmt.Errorf("sent close frame: %v", cerr) + c.close(closeErr) + if !errors.Is(c.closeErr, closeErr) { + return c.closeErr + } - if !errors.Is(c.closeErr, err) { - return fmt.Errorf("failed to close websocket: %w", err) + // We're the only goroutine allowed to get this far. + // The only possible error from closing the connection here + // is that the connection is already closed in which case, + // we do not really care. + c.ws.Close(int(code), reason) + + // Guaranteed for this channel receive to succeed since the above + // if statement means we are the goroutine that closed this connection. + ev := <-c.closeEventCh + if !ev.WasClean { + return fmt.Errorf("unclean connection close: %v", CloseError{ + Code: StatusCode(ev.Code), + Reason: ev.Reason, + }) } return nil diff --git a/websocket_js_export_test.go b/websocket_js_export_test.go deleted file mode 100644 index 462c99d3..00000000 --- a/websocket_js_export_test.go +++ /dev/null @@ -1,17 +0,0 @@ -// +build js - -package websocket - -import ( - "context" - "fmt" -) - -func (c *Conn) WaitCloseFrame(ctx context.Context) error { - select { - case <-c.receivedCloseFrame: - return nil - case <-ctx.Done(): - return fmt.Errorf("failed to wait for close frame: %w", ctx.Err()) - } -} diff --git a/websocket_js_test.go b/websocket_js_test.go index 9808e708..9b7bb813 100644 --- a/websocket_js_test.go +++ b/websocket_js_test.go @@ -49,9 +49,4 @@ func TestConn(t *testing.T) { if err != nil { t.Fatal(err) } - - err = c.WaitCloseFrame(ctx) - if err != nil { - t.Fatal(err) - } } From a5af693572651bf11ec1d09adc8c60836db5dd87 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 7 Oct 2019 01:17:03 -0400 Subject: [PATCH 180/519] Update docs to reflect close handshake support --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f6afbd8c..47165b0c 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ go get nhooyr.io/websocket - Highly optimized by default - Concurrent writes out of the box - [Complete Wasm](https://godoc.org/nhooyr.io/websocket#hdr-Wasm) support +- [WebSocket close handshake](https://godoc.org/nhooyr.io/websocket#Conn.Close) ## Roadmap @@ -128,7 +129,9 @@ gorilla/websocket writes its handshakes to the underlying net.Conn. Thus it has to reinvent hooks for TLS and proxies and prevents support of HTTP/2. Some more advantages of nhooyr.io/websocket are that it supports concurrent writes and -makes it very easy to close the connection with a status code and reason. +makes it very easy to close the connection with a status code and reason. In fact, +nhooyr.io/websocket even implements the complete WebSocket close handshake for you whereas +with gorilla/websocket you have to perform it manually. See [gorilla/websocket#448](https://github.com/gorilla/websocket/issues/448). The ping API is also nicer. gorilla/websocket requires registering a pong handler on the Conn which results in awkward control flow. With nhooyr.io/websocket you use the Ping method on the Conn From 224ef23799cb71fd8fabc27a20503e978a18e048 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 7 Oct 2019 01:20:55 -0400 Subject: [PATCH 181/519] Cleanup close handshake implementation --- conn.go | 157 ++++++++++++++++++++------------------------ conn_export_test.go | 4 +- go.mod | 1 - websocket_js.go | 54 +++++---------- 4 files changed, 92 insertions(+), 124 deletions(-) diff --git a/conn.go b/conn.go index 73d64904..b162a42a 100644 --- a/conn.go +++ b/conn.go @@ -17,8 +17,6 @@ import ( "sync/atomic" "time" - "golang.org/x/xerrors" - "nhooyr.io/websocket/internal/bpool" ) @@ -66,7 +64,6 @@ type Conn struct { writeMsgOpcode opcode writeMsgCtx context.Context readMsgLeft int64 - readCloseFrame CloseError // Used to ensure the previous reader is read till EOF before allowing // a new one. @@ -74,7 +71,6 @@ type Conn struct { // readFrameLock is acquired to read from bw. readFrameLock chan struct{} isReadClosed *atomicInt64 - isCloseHandshake *atomicInt64 readHeaderBuf []byte controlPayloadBuf []byte @@ -102,7 +98,6 @@ func (c *Conn) init() { c.writeFrameLock = make(chan struct{}, 1) c.readFrameLock = make(chan struct{}, 1) - c.isCloseHandshake = &atomicInt64{} c.setReadTimeout = make(chan context.Context) c.setWriteTimeout = make(chan context.Context) @@ -206,20 +201,20 @@ func (c *Conn) releaseLock(lock chan struct{}) { } } -func (c *Conn) readTillMsg(ctx context.Context) (header, error) { +func (c *Conn) readTillMsg(ctx context.Context, lock bool) (header, error) { for { - h, err := c.readFrameHeader(ctx) + h, err := c.readFrameHeader(ctx, lock) if err != nil { return header{}, err } if h.rsv1 || h.rsv2 || h.rsv3 { - c.Close(StatusProtocolError, fmt.Sprintf("received header with rsv bits set: %v:%v:%v", h.rsv1, h.rsv2, h.rsv3)) + c.writeClose(StatusProtocolError, fmt.Sprintf("received header with rsv bits set: %v:%v:%v", h.rsv1, h.rsv2, h.rsv3), false) return header{}, c.closeErr } if h.opcode.controlOp() { - err = c.handleControl(ctx, h) + err = c.handleControl(ctx, h, lock) if err != nil { return header{}, fmt.Errorf("failed to handle control frame: %w", err) } @@ -230,18 +225,20 @@ func (c *Conn) readTillMsg(ctx context.Context) (header, error) { case opBinary, opText, opContinuation: return h, nil default: - c.Close(StatusProtocolError, fmt.Sprintf("received unknown opcode %v", h.opcode)) + c.writeClose(StatusProtocolError, fmt.Sprintf("received unknown opcode %v", h.opcode), false) return header{}, c.closeErr } } } -func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { - err := c.acquireLock(ctx, c.readFrameLock) - if err != nil { - return header{}, err +func (c *Conn) readFrameHeader(ctx context.Context, lock bool) (header, error) { + if lock { + err := c.acquireLock(ctx, c.readFrameLock) + if err != nil { + return header{}, err + } + defer c.releaseLock(c.readFrameLock) } - defer c.releaseLock(c.readFrameLock) select { case <-c.closed: @@ -273,14 +270,14 @@ func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { return h, nil } -func (c *Conn) handleControl(ctx context.Context, h header) error { +func (c *Conn) handleControl(ctx context.Context, h header, lock bool) error { if h.payloadLength > maxControlFramePayload { - c.Close(StatusProtocolError, fmt.Sprintf("control frame too large at %v bytes", h.payloadLength)) + c.writeClose(StatusProtocolError, fmt.Sprintf("control frame too large at %v bytes", h.payloadLength), false) return c.closeErr } if !h.fin { - c.Close(StatusProtocolError, "received fragmented control frame") + c.writeClose(StatusProtocolError, "received fragmented control frame", false) return c.closeErr } @@ -288,7 +285,7 @@ func (c *Conn) handleControl(ctx context.Context, h header) error { defer cancel() b := c.controlPayloadBuf[:h.payloadLength] - _, err := c.readFramePayload(ctx, b) + _, err := c.readFramePayload(ctx, b, lock) if err != nil { return err } @@ -312,16 +309,14 @@ func (c *Conn) handleControl(ctx context.Context, h header) error { ce, err := parseClosePayload(b) if err != nil { err = fmt.Errorf("received invalid close payload: %w", err) - c.Close(StatusProtocolError, err.Error()) + c.writeClose(StatusProtocolError, err.Error(), false) return c.closeErr } // This ensures the closeErr of the Conn is always the received CloseError // in case the echo close frame write fails. // See https://github.com/nhooyr/websocket/issues/109 - c.setCloseErr(fmt.Errorf("received close frame: %w", ce)) - - c.readCloseFrame = ce + c.setCloseErr(ce) func() { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) @@ -329,6 +324,9 @@ func (c *Conn) handleControl(ctx context.Context, h header) error { c.writeControl(ctx, opClose, b) }() + if !lock { + c.releaseLock(c.readFrameLock) + } // We close with nil since the error is already set above. c.close(nil) return c.closeErr @@ -362,16 +360,7 @@ func (c *Conn) handleControl(ctx context.Context, h header) error { // Most users should not need this. func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) { if c.isReadClosed.Load() == 1 { - return 0, nil, fmt.Errorf("websocket connection read closed") - } - - if c.isCloseHandshake.Load() == 1 { - select { - case <-ctx.Done(): - return 0, nil, fmt.Errorf("failed to get reader: %w", ctx.Err()) - case <-c.closed: - return 0, nil, fmt.Errorf("failed to get reader: %w", c.closeErr) - } + return 0, nil, errors.New("websocket connection read closed") } typ, r, err := c.reader(ctx) @@ -381,23 +370,23 @@ func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) { return typ, r, nil } -func (c *Conn) reader(ctx context.Context) (MessageType, io.Reader, error) { +func (c *Conn) reader(ctx context.Context) (_ MessageType, _ io.Reader, err error) { if c.activeReader != nil && !c.readerFrameEOF { // The only way we know for sure the previous reader is not yet complete is // if there is an active frame not yet fully read. // Otherwise, a user may have read the last byte but not the EOF if the EOF // is in the next frame so we check for that below. - return 0, nil, fmt.Errorf("previous message not read to completion") + return 0, nil, errors.New("previous message not read to completion") } - h, err := c.readTillMsg(ctx) + h, err := c.readTillMsg(ctx, true) if err != nil { return 0, nil, err } if c.activeReader != nil && !c.activeReader.eof() { if h.opcode != opContinuation { - c.Close(StatusProtocolError, "received new data message without finishing the previous message") + c.writeClose(StatusProtocolError, "received new data message without finishing the previous message", false) return 0, nil, c.closeErr } @@ -407,12 +396,12 @@ func (c *Conn) reader(ctx context.Context) (MessageType, io.Reader, error) { c.activeReader = nil - h, err = c.readTillMsg(ctx) + h, err = c.readTillMsg(ctx, true) if err != nil { return 0, nil, err } } else if h.opcode == opContinuation { - c.Close(StatusProtocolError, "received continuation frame not after data or text frame") + c.writeClose(StatusProtocolError, "received continuation frame not after data or text frame", false) return 0, nil, c.closeErr } @@ -458,7 +447,7 @@ func (r *messageReader) read(p []byte) (int, error) { } if r.c.readMsgLeft <= 0 { - r.c.Close(StatusMessageTooBig, fmt.Sprintf("read limited at %v bytes", r.c.msgReadLimit)) + r.c.writeClose(StatusMessageTooBig, fmt.Sprintf("read limited at %v bytes", r.c.msgReadLimit), false) return 0, r.c.closeErr } @@ -467,13 +456,13 @@ func (r *messageReader) read(p []byte) (int, error) { } if r.c.readerFrameEOF { - h, err := r.c.readTillMsg(r.c.readerMsgCtx) + h, err := r.c.readTillMsg(r.c.readerMsgCtx, true) if err != nil { return 0, err } if h.opcode != opContinuation { - r.c.Close(StatusProtocolError, "received new data message without finishing the previous message") + r.c.writeClose(StatusProtocolError, "received new data message without finishing the previous message", false) return 0, r.c.closeErr } @@ -487,7 +476,7 @@ func (r *messageReader) read(p []byte) (int, error) { p = p[:h.payloadLength] } - n, err := r.c.readFramePayload(r.c.readerMsgCtx, p) + n, err := r.c.readFramePayload(r.c.readerMsgCtx, p, true) h.payloadLength -= int64(n) r.c.readMsgLeft -= int64(n) @@ -512,12 +501,14 @@ func (r *messageReader) read(p []byte) (int, error) { return n, nil } -func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { - err := c.acquireLock(ctx, c.readFrameLock) - if err != nil { - return 0, err +func (c *Conn) readFramePayload(ctx context.Context, p []byte, lock bool) (int, error) { + if lock { + err := c.acquireLock(ctx, c.readFrameLock) + if err != nil { + return 0, err + } + defer c.releaseLock(c.readFrameLock) } - defer c.releaseLock(c.readFrameLock) select { case <-c.closed: @@ -813,14 +804,14 @@ func (c *Conn) writePong(p []byte) error { // Close will unblock all goroutines interacting with the connection once // complete. func (c *Conn) Close(code StatusCode, reason string) error { - err := c.closeHandshake(code, reason) + err := c.writeClose(code, reason, true) if err != nil { return fmt.Errorf("failed to close websocket connection: %w", err) } return nil } -func (c *Conn) closeHandshake(code StatusCode, reason string) error { +func (c *Conn) writeClose(code StatusCode, reason string, handshake bool) error { ce := CloseError{ Code: code, Reason: reason, @@ -838,60 +829,58 @@ func (c *Conn) closeHandshake(code StatusCode, reason string) error { p, _ = ce.bytes() } + // Give the handshake 10 seconds. ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() - // Ensures the connection is closed if everything below succeeds. - // Up here because we must release the read lock first. - // nil because of the setCloseErr call below. - defer c.close(nil) - - // CloseErrors sent are made opaque to prevent applications from thinking - // they received a given status. - sentErr := fmt.Errorf("sent close frame: %v", ce) - // Other connections should only see this error. - c.setCloseErr(sentErr) - err = c.writeControl(ctx, opClose, p) if err != nil { return err } + c.setCloseErr(ce) + defer c.close(nil) - // Wait for close frame from peer. - err = c.waitClose(ctx) - // We didn't read a close frame. - if c.readCloseFrame == (CloseError{}) { - if ctx.Err() != nil { - return xerrors.Errorf("failed to wait for peer close frame: %w", ctx.Err()) - } - // We need to make the err returned from c.waitClose accurate. - return xerrors.Errorf("failed to read peer close frame for unknown reason") + if handshake { + // Try to wait for close frame peer but don't complain + // if one is not received since we already decided the + // close status of the connection above. + c.waitClose(ctx) } + return nil } func (c *Conn) waitClose(ctx context.Context) error { + err := c.acquireLock(ctx, c.readFrameLock) + if err != nil { + return err + } + defer c.releaseLock(c.readFrameLock) + b := bpool.Get() - buf := b.Bytes() - buf = buf[:cap(buf)] defer bpool.Put(b) - // Prevent reads from user code as we are going to be - // discarding all messages so they cannot rely on any ordering. - c.isCloseHandshake.Store(1) - - // From this point forward, any reader we receive means we are - // now the sole readers of the connection and so it is safe - // to discard all payloads. + var h header + if c.activeReader != nil && !c.readerFrameEOF { + h = c.readerMsgHeader + } for { - _, r, err := c.reader(ctx) - if err != nil { - return err + for h.payloadLength > 0 { + buf := b.Bytes() + if int64(cap(buf)) > h.payloadLength { + buf = buf[:h.payloadLength] + } else { + buf = buf[:cap(buf)] + } + n, err := c.readFramePayload(ctx, buf, false) + if err != nil { + return err + } + h.payloadLength -= int64(n) } - // Discard all payloads. - _, err = io.CopyBuffer(ioutil.Discard, r, buf) + h, err = c.readTillMsg(ctx, false) if err != nil { return err } diff --git a/conn_export_test.go b/conn_export_test.go index 32340b56..0fa3272b 100644 --- a/conn_export_test.go +++ b/conn_export_test.go @@ -23,12 +23,12 @@ const ( ) func (c *Conn) ReadFrame(ctx context.Context) (OpCode, []byte, error) { - h, err := c.readFrameHeader(ctx) + h, err := c.readFrameHeader(ctx, true) if err != nil { return 0, nil, err } b := make([]byte, h.payloadLength) - _, err = c.readFramePayload(ctx, b) + _, err = c.readFramePayload(ctx, b, true) if err != nil { return 0, nil, err } diff --git a/go.mod b/go.mod index 2a5bbae3..0e398366 100644 --- a/go.mod +++ b/go.mod @@ -21,6 +21,5 @@ require ( golang.org/x/sys v0.0.0-20190927073244-c990c680b611 // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72 - golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect ) diff --git a/websocket_js.go b/websocket_js.go index 27b83715..d11266dd 100644 --- a/websocket_js.go +++ b/websocket_js.go @@ -23,11 +23,12 @@ type Conn struct { // read limit for a message in bytes. msgReadLimit *atomicInt64 - isReadClosed *atomicInt64 - closeOnce sync.Once - closed chan struct{} - closeErrOnce sync.Once - closeErr error + isReadClosed *atomicInt64 + closeOnce sync.Once + closed chan struct{} + closeErrOnce sync.Once + closeErr error + closeWasClean bool releaseOnClose func() releaseOnMessage func() @@ -35,15 +36,14 @@ type Conn struct { readSignal chan struct{} readBufMu sync.Mutex readBuf []wsjs.MessageEvent - - closeEventCh chan wsjs.CloseEvent } -func (c *Conn) close(err error) { +func (c *Conn) close(err error, wasClean bool) { c.closeOnce.Do(func() { runtime.SetFinalizer(c, nil) c.setCloseErr(err) + c.closeWasClean = wasClean close(c.closed) }) } @@ -57,18 +57,15 @@ func (c *Conn) init() { c.isReadClosed = &atomicInt64{} - c.closeEventCh = make(chan wsjs.CloseEvent, 1) - c.releaseOnClose = c.ws.OnClose(func(e wsjs.CloseEvent) { - c.closeEventCh <- e - close(c.closeEventCh) - - cerr := CloseError{ + var err error = CloseError{ Code: StatusCode(e.Code), Reason: e.Reason, } - - c.close(fmt.Errorf("received close frame: %w", cerr)) + if !e.WasClean { + err = fmt.Errorf("connection close was not clean: %w", err) + } + c.close(err, e.WasClean) c.releaseOnClose() c.releaseOnMessage() @@ -209,32 +206,15 @@ func (c *Conn) exportedClose(code StatusCode, reason string) error { return fmt.Errorf("already closed: %w", c.closeErr) } - cerr := CloseError{ - Code: code, - Reason: reason, - } - closeErr := fmt.Errorf("sent close frame: %v", cerr) - c.close(closeErr) - if !errors.Is(c.closeErr, closeErr) { - return c.closeErr - } - - // We're the only goroutine allowed to get this far. // The only possible error from closing the connection here // is that the connection is already closed in which case, - // we do not really care. + // we do not really care since c.closed will immediately return. c.ws.Close(int(code), reason) - // Guaranteed for this channel receive to succeed since the above - // if statement means we are the goroutine that closed this connection. - ev := <-c.closeEventCh - if !ev.WasClean { - return fmt.Errorf("unclean connection close: %v", CloseError{ - Code: StatusCode(ev.Code), - Reason: ev.Reason, - }) + <-c.closed + if !c.closeWasClean { + return c.closeErr } - return nil } From 3cc6b45f1d40b95252a12c466a6b80fe555db3cb Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 8 Oct 2019 20:06:28 -0400 Subject: [PATCH 182/519] Perfect the close handshake implementation --- README.md | 2 +- conn.go | 263 +++++++++++++++++++++++++------------------ conn_common.go | 7 +- conn_export_test.go | 8 +- conn_test.go | 81 +++++++------ example_echo_test.go | 5 +- websocket_js.go | 19 ++-- 7 files changed, 222 insertions(+), 163 deletions(-) diff --git a/README.md b/README.md index 47165b0c..e7fea3aa 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ go get nhooyr.io/websocket - Highly optimized by default - Concurrent writes out of the box - [Complete Wasm](https://godoc.org/nhooyr.io/websocket#hdr-Wasm) support -- [WebSocket close handshake](https://godoc.org/nhooyr.io/websocket#Conn.Close) +- [Close handshake](https://godoc.org/nhooyr.io/websocket#Conn.Close) ## Roadmap diff --git a/conn.go b/conn.go index b162a42a..b7b9360e 100644 --- a/conn.go +++ b/conn.go @@ -46,6 +46,7 @@ type Conn struct { closeErrOnce sync.Once closeErr error closed chan struct{} + closing *atomicInt64 // messageWriter state. // writeMsgLock is acquired to write a data message. @@ -73,12 +74,14 @@ type Conn struct { isReadClosed *atomicInt64 readHeaderBuf []byte controlPayloadBuf []byte + readLock chan struct{} // messageReader state. - readerMsgCtx context.Context - readerMsgHeader header - readerFrameEOF bool - readerMaskPos int + readerMsgCtx context.Context + readerMsgHeader header + readerFrameEOF bool + readerMaskPos int + readerShouldLock bool setReadTimeout chan context.Context setWriteTimeout chan context.Context @@ -86,10 +89,13 @@ type Conn struct { pingCounter *atomicInt64 activePingsMu sync.Mutex activePings map[string]chan<- struct{} + + logf func(format string, v ...interface{}) } func (c *Conn) init() { c.closed = make(chan struct{}) + c.closing = &atomicInt64{} c.msgReadLimit = &atomicInt64{} c.msgReadLimit.Store(32768) @@ -98,6 +104,7 @@ func (c *Conn) init() { c.writeFrameLock = make(chan struct{}, 1) c.readFrameLock = make(chan struct{}, 1) + c.readLock = make(chan struct{}, 1) c.setReadTimeout = make(chan context.Context) c.setWriteTimeout = make(chan context.Context) @@ -115,6 +122,8 @@ func (c *Conn) init() { c.close(errors.New("connection garbage collected")) }) + c.logf = log.Printf + go c.timeoutLoop() } @@ -165,9 +174,14 @@ func (c *Conn) timeoutLoop() { case readCtx = <-c.setReadTimeout: case <-readCtx.Done(): - c.close(fmt.Errorf("read timed out: %w", readCtx.Err())) + c.setCloseErr(fmt.Errorf("read timed out: %w", readCtx.Err())) + // Guaranteed to eventually close the connection since it will not try and read + // but only write. + go c.exportedClose(StatusPolicyViolation, "read timed out", false) + readCtx = context.Background() case <-writeCtx.Done(): c.close(fmt.Errorf("write timed out: %w", writeCtx.Err())) + return } } } @@ -179,7 +193,7 @@ func (c *Conn) acquireLock(ctx context.Context, lock chan struct{}) error { switch lock { case c.writeFrameLock, c.writeMsgLock: err = fmt.Errorf("could not acquire write lock: %v", ctx.Err()) - case c.readFrameLock: + case c.readFrameLock, c.readLock: err = fmt.Errorf("could not acquire read lock: %v", ctx.Err()) default: panic(fmt.Sprintf("websocket: failed to acquire unknown lock: %v", ctx.Err())) @@ -201,22 +215,23 @@ func (c *Conn) releaseLock(lock chan struct{}) { } } -func (c *Conn) readTillMsg(ctx context.Context, lock bool) (header, error) { +func (c *Conn) readTillMsg(ctx context.Context) (header, error) { for { - h, err := c.readFrameHeader(ctx, lock) + h, err := c.readFrameHeader(ctx) if err != nil { return header{}, err } if h.rsv1 || h.rsv2 || h.rsv3 { - c.writeClose(StatusProtocolError, fmt.Sprintf("received header with rsv bits set: %v:%v:%v", h.rsv1, h.rsv2, h.rsv3), false) - return header{}, c.closeErr + err := fmt.Errorf("received header with rsv bits set: %v:%v:%v", h.rsv1, h.rsv2, h.rsv3) + c.exportedClose(StatusProtocolError, err.Error(), false) + return header{}, err } if h.opcode.controlOp() { - err = c.handleControl(ctx, h, lock) + err = c.handleControl(ctx, h) if err != nil { - return header{}, fmt.Errorf("failed to handle control frame: %w", err) + return header{}, fmt.Errorf("failed to handle control frame %v: %w", h.opcode, err) } continue } @@ -225,20 +240,28 @@ func (c *Conn) readTillMsg(ctx context.Context, lock bool) (header, error) { case opBinary, opText, opContinuation: return h, nil default: - c.writeClose(StatusProtocolError, fmt.Sprintf("received unknown opcode %v", h.opcode), false) - return header{}, c.closeErr + err := fmt.Errorf("received unknown opcode %v", h.opcode) + c.exportedClose(StatusProtocolError, err.Error(), false) + return header{}, err } } } -func (c *Conn) readFrameHeader(ctx context.Context, lock bool) (header, error) { - if lock { - err := c.acquireLock(ctx, c.readFrameLock) +func (c *Conn) readFrameHeader(ctx context.Context) (_ header, err error) { + wrap := func(err error) error { + return fmt.Errorf("failed to read frame header: %w", err) + } + defer func() { if err != nil { - return header{}, err + err = wrap(err) } - defer c.releaseLock(c.readFrameLock) + }() + + err = c.acquireLock(ctx, c.readFrameLock) + if err != nil { + return header{}, err } + defer c.releaseLock(c.readFrameLock) select { case <-c.closed: @@ -255,9 +278,8 @@ func (c *Conn) readFrameHeader(ctx context.Context, lock bool) (header, error) { err = ctx.Err() default: } - err := fmt.Errorf("failed to read header: %w", err) c.releaseLock(c.readFrameLock) - c.close(err) + c.close(wrap(err)) return header{}, err } @@ -270,22 +292,24 @@ func (c *Conn) readFrameHeader(ctx context.Context, lock bool) (header, error) { return h, nil } -func (c *Conn) handleControl(ctx context.Context, h header, lock bool) error { +func (c *Conn) handleControl(ctx context.Context, h header) error { if h.payloadLength > maxControlFramePayload { - c.writeClose(StatusProtocolError, fmt.Sprintf("control frame too large at %v bytes", h.payloadLength), false) - return c.closeErr + err := fmt.Errorf("received too big control frame at %v bytes", h.payloadLength) + c.exportedClose(StatusProtocolError, err.Error(), false) + return err } if !h.fin { - c.writeClose(StatusProtocolError, "received fragmented control frame", false) - return c.closeErr + err := errors.New("received fragmented control frame") + c.exportedClose(StatusProtocolError, err.Error(), false) + return err } ctx, cancel := context.WithTimeout(ctx, time.Second*5) defer cancel() b := c.controlPayloadBuf[:h.payloadLength] - _, err := c.readFramePayload(ctx, b, lock) + _, err := c.readFramePayload(ctx, b) if err != nil { return err } @@ -296,7 +320,7 @@ func (c *Conn) handleControl(ctx context.Context, h header, lock bool) error { switch h.opcode { case opPing: - return c.writePong(b) + return c.writeControl(ctx, opPong, b) case opPong: c.activePingsMu.Lock() pong, ok := c.activePings[string(b)] @@ -309,27 +333,13 @@ func (c *Conn) handleControl(ctx context.Context, h header, lock bool) error { ce, err := parseClosePayload(b) if err != nil { err = fmt.Errorf("received invalid close payload: %w", err) - c.writeClose(StatusProtocolError, err.Error(), false) - return c.closeErr + c.exportedClose(StatusProtocolError, err.Error(), false) + return err } - // This ensures the closeErr of the Conn is always the received CloseError - // in case the echo close frame write fails. - // See https://github.com/nhooyr/websocket/issues/109 - c.setCloseErr(ce) - - func() { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - defer cancel() - c.writeControl(ctx, opClose, b) - }() - - if !lock { - c.releaseLock(c.readFrameLock) - } - // We close with nil since the error is already set above. - c.close(nil) - return c.closeErr + err = fmt.Errorf("received close: %w", ce) + c.writeClose(b, err, false) + return err default: panic(fmt.Sprintf("websocket: unexpected control opcode: %#v", h)) } @@ -363,14 +373,22 @@ func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) { return 0, nil, errors.New("websocket connection read closed") } - typ, r, err := c.reader(ctx) + typ, r, err := c.reader(ctx, true) if err != nil { return 0, nil, fmt.Errorf("failed to get reader: %w", err) } return typ, r, nil } -func (c *Conn) reader(ctx context.Context) (_ MessageType, _ io.Reader, err error) { +func (c *Conn) reader(ctx context.Context, lock bool) (MessageType, io.Reader, error) { + if lock { + err := c.acquireLock(ctx, c.readLock) + if err != nil { + return 0, nil, err + } + defer c.releaseLock(c.readLock) + } + if c.activeReader != nil && !c.readerFrameEOF { // The only way we know for sure the previous reader is not yet complete is // if there is an active frame not yet fully read. @@ -379,15 +397,16 @@ func (c *Conn) reader(ctx context.Context) (_ MessageType, _ io.Reader, err erro return 0, nil, errors.New("previous message not read to completion") } - h, err := c.readTillMsg(ctx, true) + h, err := c.readTillMsg(ctx) if err != nil { return 0, nil, err } if c.activeReader != nil && !c.activeReader.eof() { if h.opcode != opContinuation { - c.writeClose(StatusProtocolError, "received new data message without finishing the previous message", false) - return 0, nil, c.closeErr + err := errors.New("received new data message without finishing the previous message") + c.exportedClose(StatusProtocolError, err.Error(), false) + return 0, nil, err } if !h.fin || h.payloadLength > 0 { @@ -396,13 +415,14 @@ func (c *Conn) reader(ctx context.Context) (_ MessageType, _ io.Reader, err erro c.activeReader = nil - h, err = c.readTillMsg(ctx, true) + h, err = c.readTillMsg(ctx) if err != nil { return 0, nil, err } } else if h.opcode == opContinuation { - c.writeClose(StatusProtocolError, "received continuation frame not after data or text frame", false) - return 0, nil, c.closeErr + err := errors.New("received continuation frame not after data or text frame") + c.exportedClose(StatusProtocolError, err.Error(), false) + return 0, nil, err } c.readerMsgCtx = ctx @@ -410,6 +430,7 @@ func (c *Conn) reader(ctx context.Context) (_ MessageType, _ io.Reader, err erro c.readerFrameEOF = false c.readerMaskPos = 0 c.readMsgLeft = c.msgReadLimit.Load() + c.readerShouldLock = lock r := &messageReader{ c: c, @@ -442,13 +463,22 @@ func (r *messageReader) Read(p []byte) (int, error) { } func (r *messageReader) read(p []byte) (int, error) { + if r.c.readerShouldLock { + err := r.c.acquireLock(r.c.readerMsgCtx, r.c.readLock) + if err != nil { + return 0, err + } + defer r.c.releaseLock(r.c.readLock) + } + if r.eof() { return 0, fmt.Errorf("cannot use EOFed reader") } if r.c.readMsgLeft <= 0 { - r.c.writeClose(StatusMessageTooBig, fmt.Sprintf("read limited at %v bytes", r.c.msgReadLimit), false) - return 0, r.c.closeErr + err := fmt.Errorf("read limited at %v bytes", r.c.msgReadLimit) + r.c.exportedClose(StatusMessageTooBig, err.Error(), false) + return 0, err } if int64(len(p)) > r.c.readMsgLeft { @@ -456,14 +486,15 @@ func (r *messageReader) read(p []byte) (int, error) { } if r.c.readerFrameEOF { - h, err := r.c.readTillMsg(r.c.readerMsgCtx, true) + h, err := r.c.readTillMsg(r.c.readerMsgCtx) if err != nil { return 0, err } if h.opcode != opContinuation { - r.c.writeClose(StatusProtocolError, "received new data message without finishing the previous message", false) - return 0, r.c.closeErr + err := errors.New("received new data message without finishing the previous message") + r.c.exportedClose(StatusProtocolError, err.Error(), false) + return 0, err } r.c.readerMsgHeader = h @@ -476,7 +507,7 @@ func (r *messageReader) read(p []byte) (int, error) { p = p[:h.payloadLength] } - n, err := r.c.readFramePayload(r.c.readerMsgCtx, p, true) + n, err := r.c.readFramePayload(r.c.readerMsgCtx, p) h.payloadLength -= int64(n) r.c.readMsgLeft -= int64(n) @@ -501,14 +532,21 @@ func (r *messageReader) read(p []byte) (int, error) { return n, nil } -func (c *Conn) readFramePayload(ctx context.Context, p []byte, lock bool) (int, error) { - if lock { - err := c.acquireLock(ctx, c.readFrameLock) +func (c *Conn) readFramePayload(ctx context.Context, p []byte) (_ int, err error) { + wrap := func(err error) error { + return fmt.Errorf("failed to read frame payload: %w", err) + } + defer func() { if err != nil { - return 0, err + err = wrap(err) } - defer c.releaseLock(c.readFrameLock) + }() + + err = c.acquireLock(ctx, c.readFrameLock) + if err != nil { + return 0, err } + defer c.releaseLock(c.readFrameLock) select { case <-c.closed: @@ -525,9 +563,8 @@ func (c *Conn) readFramePayload(ctx context.Context, p []byte, lock bool) (int, err = ctx.Err() default: } - err = fmt.Errorf("failed to read frame payload: %w", err) c.releaseLock(c.readFrameLock) - c.close(err) + c.close(wrap(err)) return n, err } @@ -661,9 +698,12 @@ func (w *messageWriter) close() error { } func (c *Conn) writeControl(ctx context.Context, opcode opcode, p []byte) error { + ctx, cancel := context.WithTimeout(ctx, time.Second*5) + defer cancel() + _, err := c.writeFrame(ctx, true, opcode, p) if err != nil { - return fmt.Errorf("failed to write control frame: %w", err) + return fmt.Errorf("failed to write control frame %v: %w", opcode, err) } return nil } @@ -780,19 +820,13 @@ func (c *Conn) realWriteFrame(ctx context.Context, h header, p []byte) (n int, e return n, nil } -func (c *Conn) writePong(p []byte) error { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - defer cancel() - - err := c.writeControl(ctx, opPong, p) - return err -} - // Close closes the WebSocket connection with the given status code and reason. // -// It will write a WebSocket close frame and then wait for the peer to respond -// with its own close frame. The entire process must complete within 10 seconds. +// It will write a WebSocket close frame with a timeout of 5s and then wait 5s for +// the peer to send a close frame. // Thus, it implements the full WebSocket close handshake. +// All data messages received from the peer during the close handshake +// will be discarded. // // The connection can only be closed once. Additional calls to Close // are no-ops. @@ -804,14 +838,14 @@ func (c *Conn) writePong(p []byte) error { // Close will unblock all goroutines interacting with the connection once // complete. func (c *Conn) Close(code StatusCode, reason string) error { - err := c.writeClose(code, reason, true) + err := c.exportedClose(code, reason, true) if err != nil { return fmt.Errorf("failed to close websocket connection: %w", err) } return nil } -func (c *Conn) writeClose(code StatusCode, reason string, handshake bool) error { +func (c *Conn) exportedClose(code StatusCode, reason string, handshake bool) error { ce := CloseError{ Code: code, Reason: reason, @@ -822,65 +856,72 @@ func (c *Conn) writeClose(code StatusCode, reason string, handshake bool) error // Definitely worth seeing what popular browsers do later. p, err := ce.bytes() if err != nil { - log.Printf("websocket: failed to marshal close frame: %+v", err) + c.logf("websocket: failed to marshal close frame: %+v", err) ce = CloseError{ Code: StatusInternalError, } p, _ = ce.bytes() } - // Give the handshake 10 seconds. - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) - defer cancel() + return c.writeClose(p, fmt.Errorf("sent close: %w", ce), handshake) +} - err = c.writeControl(ctx, opClose, p) - if err != nil { - return err +func (c *Conn) writeClose(p []byte, ce error, handshake bool) error { + select { + case <-c.closed: + return fmt.Errorf("tried to close with %v but connection already closed: %w", ce, c.closeErr) + default: + } + + if !c.closing.CAS(0, 1) { + return fmt.Errorf("another goroutine is closing") } + + // No matter what happens next, close error should be set. c.setCloseErr(ce) defer c.close(nil) + err := c.writeControl(context.Background(), opClose, p) + if err != nil { + return err + } + if handshake { - // Try to wait for close frame peer but don't complain - // if one is not received since we already decided the - // close status of the connection above. - c.waitClose(ctx) + err = c.waitClose() + if CloseStatus(err) == -1 { + // waitClose exited not due to receiving a close frame. + return fmt.Errorf("failed to wait for peer close frame: %w", err) + } } return nil } -func (c *Conn) waitClose(ctx context.Context) error { - err := c.acquireLock(ctx, c.readFrameLock) +func (c *Conn) waitClose() error { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + err := c.acquireLock(ctx, c.readLock) if err != nil { return err } - defer c.releaseLock(c.readFrameLock) + defer c.releaseLock(c.readLock) + c.readerShouldLock = false b := bpool.Get() + buf := b.Bytes() + buf = buf[:cap(buf)] defer bpool.Put(b) - var h header - if c.activeReader != nil && !c.readerFrameEOF { - h = c.readerMsgHeader - } - for { - for h.payloadLength > 0 { - buf := b.Bytes() - if int64(cap(buf)) > h.payloadLength { - buf = buf[:h.payloadLength] - } else { - buf = buf[:cap(buf)] - } - n, err := c.readFramePayload(ctx, buf, false) + if c.activeReader == nil || c.readerFrameEOF { + _, _, err := c.reader(ctx, false) if err != nil { - return err + return fmt.Errorf("failed to get reader: %w", err) } - h.payloadLength -= int64(n) } - h, err = c.readTillMsg(ctx, false) + _, err = io.CopyBuffer(ioutil.Discard, c.activeReader, buf) if err != nil { return err } diff --git a/conn_common.go b/conn_common.go index 162dc80d..5a11a79c 100644 --- a/conn_common.go +++ b/conn_common.go @@ -112,8 +112,9 @@ func (c *netConn) Read(p []byte) (int, error) { return 0, err } if typ != c.msgType { - c.c.Close(StatusUnsupportedData, fmt.Sprintf("unexpected frame type read (expected %v): %v", c.msgType, typ)) - return 0, c.c.closeErr + err := fmt.Errorf("unexpected frame type read (expected %v): %v", c.msgType, typ) + c.c.Close(StatusUnsupportedData, err.Error()) + return 0, err } c.reader = r } @@ -184,7 +185,7 @@ func (c *Conn) CloseRead(ctx context.Context) context.Context { go func() { defer cancel() // We use the unexported reader method so that we don't get the read closed error. - c.reader(ctx) + c.reader(ctx, true) // Either the connection is already closed since there was a read error // or the context was cancelled or a message was read and we should close // the connection. diff --git a/conn_export_test.go b/conn_export_test.go index 0fa3272b..94195a9c 100644 --- a/conn_export_test.go +++ b/conn_export_test.go @@ -22,13 +22,17 @@ const ( OpContinuation = OpCode(opContinuation) ) +func (c *Conn) SetLogf(fn func(format string, v ...interface{})) { + c.logf = fn +} + func (c *Conn) ReadFrame(ctx context.Context) (OpCode, []byte, error) { - h, err := c.readFrameHeader(ctx, true) + h, err := c.readFrameHeader(ctx) if err != nil { return 0, nil, err } b := make([]byte, h.payloadLength) - _, err = c.readFramePayload(ctx, b, true) + _, err = c.readFramePayload(ctx, b) if err != nil { return 0, nil, err } diff --git a/conn_test.go b/conn_test.go index 970d2350..2bc446d7 100644 --- a/conn_test.go +++ b/conn_test.go @@ -560,7 +560,10 @@ func TestConn(t *testing.T) { }, client: func(ctx context.Context, c *websocket.Conn) error { _, _, err := c.Read(ctx) - return assertErrorIs(io.EOF, err) + return assertErrorIs(websocket.CloseError{ + Code: websocket.StatusPolicyViolation, + Reason: "read timed out", + }, err) }, }, { @@ -612,7 +615,7 @@ func TestConn(t *testing.T) { }, client: func(ctx context.Context, c *websocket.Conn) error { _, _, err := c.Read(ctx) - return assertErrorContains(err, "too large") + return assertErrorContains(err, "too big") }, }, { @@ -880,6 +883,7 @@ func TestConn(t *testing.T) { return err } defer c.Close(websocket.StatusInternalError, "") + c.SetLogf(t.Logf) if tc.server == nil { return nil } @@ -905,6 +909,7 @@ func TestConn(t *testing.T) { t.Fatal(err) } defer c.Close(websocket.StatusInternalError, "") + c.SetLogf(t.Logf) if tc.response != nil { err = tc.response(resp) @@ -980,7 +985,10 @@ func TestAutobahn(t *testing.T) { ctx := r.Context() if testingClient { - wsecho.Loop(r.Context(), c) + err = wsecho.Loop(ctx, c) + if err != nil { + t.Logf("failed to wsecho: %+v", err) + } return nil } @@ -1022,7 +1030,10 @@ func TestAutobahn(t *testing.T) { return } - wsecho.Loop(ctx, c) + err = wsecho.Loop(ctx, c) + if err != nil { + t.Logf("failed to wsecho: %+v", err) + } } t.Run(name, func(t *testing.T) { t.Parallel() @@ -1130,13 +1141,14 @@ func TestAutobahn(t *testing.T) { err := c.PingWithPayload(ctx, string(p)) return assertCloseStatus(err, websocket.StatusProtocolError) }) - run(t, "streamPingPayload", func(ctx context.Context, c *websocket.Conn) error { - err := assertStreamPing(ctx, c, 125) - if err != nil { - return err - } - return assertCloseHandshake(ctx, c, websocket.StatusNormalClosure, "") - }) + // See comment on the tenStreamedPings test. + // run(t, "streamPingPayload", func(ctx context.Context, c *websocket.Conn) error { + // err := assertStreamPing(ctx, c, 125) + // if err != nil { + // return err + // } + // return c.Close(websocket.StatusNormalClosure, "") + // }) t.Run("unsolicitedPong", func(t *testing.T) { t.Parallel() @@ -1176,7 +1188,7 @@ func TestAutobahn(t *testing.T) { return err } } - return assertCloseHandshake(ctx, c, websocket.StatusNormalClosure, "") + return c.Close(websocket.StatusNormalClosure, "") }) } }) @@ -1199,16 +1211,19 @@ func TestAutobahn(t *testing.T) { err = c.Ping(context.Background()) return assertCloseStatus(err, websocket.StatusNormalClosure) }) - run(t, "tenStreamedPings", func(ctx context.Context, c *websocket.Conn) error { - for i := 0; i < 10; i++ { - err := assertStreamPing(ctx, c, 125) - if err != nil { - return err - } - } - return assertCloseHandshake(ctx, c, websocket.StatusNormalClosure, "") - }) + // Streamed pings tests are not useful with this implementation since we always + // use io.ReadFull. These tests cause failures when running with -race on my mac. + // run(t, "tenStreamedPings", func(ctx context.Context, c *websocket.Conn) error { + // for i := 0; i < 10; i++ { + // err := assertStreamPing(ctx, c, 125) + // if err != nil { + // return err + // } + // } + // + // return c.Close(websocket.StatusNormalClosure, "") + // }) }) // Section 3. @@ -1629,7 +1644,7 @@ func TestAutobahn(t *testing.T) { if err != nil { return err } - return assertCloseHandshake(ctx, c, websocket.StatusNormalClosure, "") + return c.Close(websocket.StatusNormalClosure, "") }) }) }) @@ -1695,15 +1710,15 @@ func TestAutobahn(t *testing.T) { }) run(t, "noReason", func(ctx context.Context, c *websocket.Conn) error { - return assertCloseHandshake(ctx, c, websocket.StatusNormalClosure, "") + return c.Close(websocket.StatusNormalClosure, "") }) run(t, "simpleReason", func(ctx context.Context, c *websocket.Conn) error { - return assertCloseHandshake(ctx, c, websocket.StatusNormalClosure, randString(16)) + return c.Close(websocket.StatusNormalClosure, randString(16)) }) run(t, "maxReason", func(ctx context.Context, c *websocket.Conn) error { - return assertCloseHandshake(ctx, c, websocket.StatusNormalClosure, randString(123)) + return c.Close(websocket.StatusNormalClosure, randString(123)) }) run(t, "tooBigReason", func(ctx context.Context, c *websocket.Conn) error { @@ -1736,7 +1751,7 @@ func TestAutobahn(t *testing.T) { } for _, code := range codes { run(t, strconv.Itoa(int(code)), func(ctx context.Context, c *websocket.Conn) error { - return assertCloseHandshake(ctx, c, code, randString(32)) + return c.Close(code, randString(32)) }) } }) @@ -1835,7 +1850,7 @@ func TestAutobahn(t *testing.T) { if err != nil { return err } - return assertCloseHandshake(ctx, c, websocket.StatusNormalClosure, "") + return c.Close(websocket.StatusNormalClosure, "") }) } }) @@ -1935,14 +1950,6 @@ func assertReadCloseFrame(ctx context.Context, c *websocket.Conn, code websocket return assert.Equalf(ce.Code, code, "unexpected frame close frame code with payload %q", actP) } -func assertCloseHandshake(ctx context.Context, c *websocket.Conn, code websocket.StatusCode, reason string) error { - p, err := c.WriteClose(ctx, code, reason) - if err != nil { - return err - } - return assertReadFrame(ctx, c, websocket.OpClose, p) -} - func assertStreamPing(ctx context.Context, c *websocket.Conn, l int) error { err := c.WriteHeader(ctx, websocket.Header{ Fin: true, @@ -1955,11 +1962,11 @@ func assertStreamPing(ctx context.Context, c *websocket.Conn, l int) error { for i := 0; i < l; i++ { err = c.BW().WriteByte(0xFE) if err != nil { - return err + return fmt.Errorf("failed to write byte %d: %w", i, err) } err = c.BW().Flush() if err != nil { - return err + return fmt.Errorf("failed to flush byte %d: %w", i, err) } } return assertReadFrame(ctx, c, websocket.OpPong, bytes.Repeat([]byte{0xFE}, l)) diff --git a/example_echo_test.go b/example_echo_test.go index b1afe8b3..ecc9b97c 100644 --- a/example_echo_test.go +++ b/example_echo_test.go @@ -67,8 +67,6 @@ func Example_echo() { // It ensures the client speaks the echo subprotocol and // only allows one message every 100ms with a 10 message burst. func echoServer(w http.ResponseWriter, r *http.Request) error { - log.Printf("serving %v", r.RemoteAddr) - c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ Subprotocols: []string{"echo"}, }) @@ -85,6 +83,9 @@ func echoServer(w http.ResponseWriter, r *http.Request) error { l := rate.NewLimiter(rate.Every(time.Millisecond*100), 10) for { err = echo(r.Context(), c, l) + if websocket.CloseStatus(err) == websocket.StatusNormalClosure { + return nil + } if err != nil { return fmt.Errorf("failed to echo with %v: %w", r.RemoteAddr, err) } diff --git a/websocket_js.go b/websocket_js.go index d11266dd..d7cbf5c7 100644 --- a/websocket_js.go +++ b/websocket_js.go @@ -23,6 +23,7 @@ type Conn struct { // read limit for a message in bytes. msgReadLimit *atomicInt64 + closeMu sync.Mutex isReadClosed *atomicInt64 closeOnce sync.Once closed chan struct{} @@ -106,8 +107,9 @@ func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { return 0, nil, fmt.Errorf("failed to read: %w", err) } if int64(len(p)) > c.msgReadLimit.Load() { - c.Close(StatusMessageTooBig, fmt.Sprintf("read limited at %v bytes", c.msgReadLimit)) - return 0, nil, c.closeErr + err := fmt.Errorf("read limited at %v bytes", c.msgReadLimit) + c.Close(StatusMessageTooBig, err.Error()) + return 0, nil, err } return typ, p, nil } @@ -202,14 +204,17 @@ func (c *Conn) Close(code StatusCode, reason string) error { } func (c *Conn) exportedClose(code StatusCode, reason string) error { + c.closeMu.Lock() + defer c.closeMu.Unlock() + if c.isClosed() { return fmt.Errorf("already closed: %w", c.closeErr) } - // The only possible error from closing the connection here - // is that the connection is already closed in which case, - // we do not really care since c.closed will immediately return. - c.ws.Close(int(code), reason) + err := c.ws.Close(int(code), reason) + if err != nil { + return err + } <-c.closed if !c.closeWasClean { @@ -287,7 +292,7 @@ func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) { } // Only implemented for use by *Conn.CloseRead in netconn.go -func (c *Conn) reader(ctx context.Context) { +func (c *Conn) reader(ctx context.Context, _ bool) { c.read(ctx) } From 9703ba2b24b2a6d07b0694582a83a7d6e80427d6 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 8 Oct 2019 21:06:21 -0400 Subject: [PATCH 183/519] Update image sha256 --- .github/workflows/ci.yml | 6 +++--- ci/test.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 774775d4..ac3770fc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,19 +4,19 @@ on: [push] jobs: fmt: runs-on: ubuntu-latest - container: nhooyr/websocket-ci@sha256:f8b6e53a9fd256bcf6c90029276385b9ec730b76a0d7ccf3ff19084bce210c50 + container: nhooyr/websocket-ci@sha256:13f9b8cc2f901e98c253595c4070254ece08543f6e100b4fa6682f87de4388eb steps: - uses: actions/checkout@v1 - run: yarn --frozen-lockfile && yarn fmt lint: runs-on: ubuntu-latest - container: nhooyr/websocket-ci@sha256:f8b6e53a9fd256bcf6c90029276385b9ec730b76a0d7ccf3ff19084bce210c50 + container: nhooyr/websocket-ci@sha256:13f9b8cc2f901e98c253595c4070254ece08543f6e100b4fa6682f87de4388eb steps: - uses: actions/checkout@v1 - run: yarn --frozen-lockfile && yarn lint test: runs-on: ubuntu-latest - container: nhooyr/websocket-ci@sha256:f8b6e53a9fd256bcf6c90029276385b9ec730b76a0d7ccf3ff19084bce210c50 + container: nhooyr/websocket-ci@sha256:13f9b8cc2f901e98c253595c4070254ece08543f6e100b4fa6682f87de4388eb steps: - uses: actions/checkout@v1 - run: yarn --frozen-lockfile && yarn test diff --git a/ci/test.ts b/ci/test.ts index aa1a0029..b44ae34b 100755 --- a/ci/test.ts +++ b/ci/test.ts @@ -11,7 +11,7 @@ if (require.main === module) { } export async function test(ctx: Promise) { - const args = ["-parallel=1024", "-coverprofile=ci/out/coverage.prof", "-coverpkg=./..."] + const args = ["-parallel=32", "-coverprofile=ci/out/coverage.prof", "-coverpkg=./..."] if (process.env.CI) { args.push("-race") From 09910a23cca649c375c5b650d7297d2a75c589b4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 8 Oct 2019 21:13:16 -0400 Subject: [PATCH 184/519] Re-enable stream ping tests Should not fail now that I've adjusted the chunk size. --- conn_test.go | 49 ++++++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/conn_test.go b/conn_test.go index 2bc446d7..f3f1a2b0 100644 --- a/conn_test.go +++ b/conn_test.go @@ -1141,14 +1141,13 @@ func TestAutobahn(t *testing.T) { err := c.PingWithPayload(ctx, string(p)) return assertCloseStatus(err, websocket.StatusProtocolError) }) - // See comment on the tenStreamedPings test. - // run(t, "streamPingPayload", func(ctx context.Context, c *websocket.Conn) error { - // err := assertStreamPing(ctx, c, 125) - // if err != nil { - // return err - // } - // return c.Close(websocket.StatusNormalClosure, "") - // }) + run(t, "streamPingPayload", func(ctx context.Context, c *websocket.Conn) error { + err := assertStreamPing(ctx, c, 125) + if err != nil { + return err + } + return c.Close(websocket.StatusNormalClosure, "") + }) t.Run("unsolicitedPong", func(t *testing.T) { t.Parallel() @@ -1212,18 +1211,16 @@ func TestAutobahn(t *testing.T) { return assertCloseStatus(err, websocket.StatusNormalClosure) }) - // Streamed pings tests are not useful with this implementation since we always - // use io.ReadFull. These tests cause failures when running with -race on my mac. - // run(t, "tenStreamedPings", func(ctx context.Context, c *websocket.Conn) error { - // for i := 0; i < 10; i++ { - // err := assertStreamPing(ctx, c, 125) - // if err != nil { - // return err - // } - // } - // - // return c.Close(websocket.StatusNormalClosure, "") - // }) + run(t, "tenStreamedPings", func(ctx context.Context, c *websocket.Conn) error { + for i := 0; i < 10; i++ { + err := assertStreamPing(ctx, c, 125) + if err != nil { + return err + } + } + + return c.Close(websocket.StatusNormalClosure, "") + }) }) // Section 3. @@ -1964,11 +1961,17 @@ func assertStreamPing(ctx context.Context, c *websocket.Conn, l int) error { if err != nil { return fmt.Errorf("failed to write byte %d: %w", i, err) } - err = c.BW().Flush() - if err != nil { - return fmt.Errorf("failed to flush byte %d: %w", i, err) + if i%32 == 0 { + err = c.BW().Flush() + if err != nil { + return fmt.Errorf("failed to flush at byte %d: %w", i, err) + } } } + err = c.BW().Flush() + if err != nil { + return fmt.Errorf("failed to flush: %v", err) + } return assertReadFrame(ctx, c, websocket.OpPong, bytes.Repeat([]byte{0xFE}, l)) } From e14c1003c69bd807bbda26206931f07af1287688 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 8 Oct 2019 22:06:43 -0400 Subject: [PATCH 185/519] Update comment on unexported js reader method --- websocket_js.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/websocket_js.go b/websocket_js.go index d7cbf5c7..33403498 100644 --- a/websocket_js.go +++ b/websocket_js.go @@ -291,7 +291,7 @@ func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) { return typ, bytes.NewReader(p), nil } -// Only implemented for use by *Conn.CloseRead in netconn.go +// Only implemented for use by *Conn.CloseRead in conn_common.go func (c *Conn) reader(ctx context.Context, _ bool) { c.read(ctx) } From 4d4a02d4bf9b8336272f6dc075f0e0a77566192d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 9 Oct 2019 16:01:53 -0400 Subject: [PATCH 186/519] Switch CI to make Much faster. --- .github/CONTRIBUTING.md | 23 +- .github/workflows/ci.yml | 12 +- Makefile | 13 + ci/.eslintrc.yaml | 23 - ci/all.ts | 14 - ci/fmt.mk | 22 + ci/fmt.ts | 39 - ci/fmtcheck.sh | 11 + ci/image/Dockerfile | 34 +- ci/image/dockerignore | 1 - ci/image/push.ts | 24 - ci/lib.ts | 123 --- ci/lint.mk | 16 + ci/lint.ts | 25 - ci/test.mk | 24 + ci/test.ts | 88 --- ci/tools.go | 11 - ci/tsconfig.json | 62 -- ci/wasmtest.sh | 9 + frame.go | 2 +- go.mod | 10 - go.sum | 65 -- internal/wsjstest/main.go | 58 +- package.json | 27 - yarn.lock | 1503 ------------------------------------- 25 files changed, 178 insertions(+), 2061 deletions(-) create mode 100644 Makefile delete mode 100644 ci/.eslintrc.yaml delete mode 100755 ci/all.ts create mode 100644 ci/fmt.mk delete mode 100755 ci/fmt.ts create mode 100755 ci/fmtcheck.sh delete mode 100644 ci/image/dockerignore delete mode 100755 ci/image/push.ts delete mode 100644 ci/lib.ts create mode 100644 ci/lint.mk delete mode 100755 ci/lint.ts create mode 100644 ci/test.mk delete mode 100755 ci/test.ts delete mode 100644 ci/tools.go delete mode 100644 ci/tsconfig.json create mode 100755 ci/wasmtest.sh delete mode 100644 package.json delete mode 100644 yarn.lock diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index edf2f839..cd6af4f1 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -31,25 +31,18 @@ CI will ensure your code is formatted, lints and passes tests. It will collect coverage and report it to [codecov](https://codecov.io/gh/nhooyr/websocket) and also upload a html `coverage` artifact that you can download to browse coverage. -You can run CI locally. You only need [Go](https://golang.org), [nodejs](https://nodejs.org/en/) and [yarn](https://yarnpkg.com). +You can run CI locally. -See the scripts in [package.json](../package.json). - -1. `yarn fmt` performs code generation and formatting. -1. `yarn lint` performs linting. -1. `yarn test` runs tests. -1. `yarn all` runs the above scripts in parallel. +See [ci/image/Dockerfile](../ci/image/Dockerfile) for the installation of the CI dependencies on Ubuntu. -For coverage details locally, see `ci/out/coverage.html` after running `yarn test`. +1. `make fmt` performs code generation and formatting. +1. `make lint` performs linting. +1. `make test` runs tests. +1. `make` runs the above targets. -CI is written with nodejs to enable running as much as possible concurrently. +For coverage details locally, see `ci/out/coverage.html` after running `make test`. -See [ci/image/Dockerfile](../ci/image/Dockerfile) for the installation of the CI dependencies on Ubuntu. - -You can also run tests normally with `go test`. `yarn test` just passes a default set of flags to +You can also run tests normally with `go test`. `make test` just passes a default set of flags to `go test` to collect coverage and runs the WASM tests. -You can pass flags to `yarn test` if you want to run a specific test or otherwise -control the behaviour of `go test` but also get coverage. - Coverage percentage from codecov and the CI scripts will be different because they are calculated differently. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ac3770fc..0f896ef2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,22 +4,22 @@ on: [push] jobs: fmt: runs-on: ubuntu-latest - container: nhooyr/websocket-ci@sha256:13f9b8cc2f901e98c253595c4070254ece08543f6e100b4fa6682f87de4388eb + container: nhooyr/websocket-ci@sha256:54a12b52be98d2d1588a054cfce35d17705c9b45546478d8bcdbac2a89a90001 steps: - uses: actions/checkout@v1 - - run: yarn --frozen-lockfile && yarn fmt + - run: make fmt lint: runs-on: ubuntu-latest - container: nhooyr/websocket-ci@sha256:13f9b8cc2f901e98c253595c4070254ece08543f6e100b4fa6682f87de4388eb + container: nhooyr/websocket-ci@sha256:54a12b52be98d2d1588a054cfce35d17705c9b45546478d8bcdbac2a89a90001 steps: - uses: actions/checkout@v1 - - run: yarn --frozen-lockfile && yarn lint + - run: make lint test: runs-on: ubuntu-latest - container: nhooyr/websocket-ci@sha256:13f9b8cc2f901e98c253595c4070254ece08543f6e100b4fa6682f87de4388eb + container: nhooyr/websocket-ci@sha256:54a12b52be98d2d1588a054cfce35d17705c9b45546478d8bcdbac2a89a90001 steps: - uses: actions/checkout@v1 - - run: yarn --frozen-lockfile && yarn test + - run: make test env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - name: Upload coverage.html diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..ce92ab5b --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +all: fmt lint test + +.SILENT: + +.PHONY: * + +include ci/fmt.mk +include ci/lint.mk +include ci/test.mk + +ci-image: + docker build -f ./ci/image/Dockerfile -t nhooyr/websocket-ci . + docker push nhooyr/websocket-ci diff --git a/ci/.eslintrc.yaml b/ci/.eslintrc.yaml deleted file mode 100644 index c6d53e23..00000000 --- a/ci/.eslintrc.yaml +++ /dev/null @@ -1,23 +0,0 @@ -parser: "@typescript-eslint/parser" -env: - node: true - -parserOptions: - ecmaVersion: 2018 - sourceType: module - -extends: - # https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#usage - - eslint:recommended - - plugin:@typescript-eslint/eslint-recommended - - plugin:@typescript-eslint/recommended - # https://www.npmjs.com/package/eslint-plugin-import#typescript - - plugin:import/recommended - - plugin:import/typescript - # https://dev.to/robertcoopercode/using-eslint-and-prettier-in-a-typescript-project-53jb - - prettier/@typescript-eslint - -rules: - "@typescript-eslint/no-use-before-define": off - "@typescript-eslint/explicit-function-return-type": off - "@typescript-eslint/no-non-null-assertion": off diff --git a/ci/all.ts b/ci/all.ts deleted file mode 100755 index c0d16a8b..00000000 --- a/ci/all.ts +++ /dev/null @@ -1,14 +0,0 @@ -#!/usr/bin/env -S npx ts-node -P ci/tsconfig.json - -import { fmt, gen } from "./fmt" -import { main } from "./lib" -import { lint } from "./lint" -import { test } from "./test" - -main(run) - -async function run(ctx: Promise) { - await gen(ctx) - - await Promise.all([fmt(ctx), lint(ctx), test(ctx)]) -} diff --git a/ci/fmt.mk b/ci/fmt.mk new file mode 100644 index 00000000..631a894d --- /dev/null +++ b/ci/fmt.mk @@ -0,0 +1,22 @@ +fmt: modtidy gofmt goimports prettier +ifdef CI + ./ci/fmtcheck.sh +endif + +modtidy: gen + go mod tidy + +gofmt: gen + gofmt -w -s . + +goimports: gen + goimports -w "-local=$$(go list -m)" . + +prettier: gen + prettier --write --print-width=120 --no-semi --trailing-comma=all --loglevel=warn $$(git ls-files "*.yaml" "*.yml" "*.md" "*.ts") + +shfmt: gen + shfmt -i 2 -w -s -sr . + +gen: + go generate ./... diff --git a/ci/fmt.ts b/ci/fmt.ts deleted file mode 100755 index 6d7543ed..00000000 --- a/ci/fmt.ts +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env -S npx ts-node -P ci/tsconfig.json - -import { exec, main } from "./lib" - -if (require.main === module) { - main(async (ctx: Promise) => { - await gen(ctx) - await fmt(ctx) - }) -} - -export async function fmt(ctx: Promise) { - await Promise.all([ - exec(ctx, "go mod tidy"), - exec(ctx, "gofmt -w -s ."), - exec(ctx, `go run go.coder.com/go-tools/cmd/goimports -w "-local=$(go list -m)" .`), - exec( - ctx, - `npx prettier --write --print-width=120 --no-semi --trailing-comma=all --loglevel=silent $(git ls-files "*.yaml" "*.yml" "*.md" "*.ts")`, - ), - ]) - - if (process.env.CI) { - const r = await exec(ctx, "git ls-files --other --modified --exclude-standard") - const files = r.stdout.toString().trim() - if (files.length) { - console.log(`files need generation or are formatted incorrectly: -${files} - -please run: - ./ci/fmt.js`) - process.exit(1) - } - } -} - -export async function gen(ctx: Promise) { - await exec(ctx, "go generate ./...") -} diff --git a/ci/fmtcheck.sh b/ci/fmtcheck.sh new file mode 100755 index 00000000..6e452a38 --- /dev/null +++ b/ci/fmtcheck.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +set -euo pipefail + +if [[ $(git ls-files --other --modified --exclude-standard) != "" ]]; then + echo "Files need generation or are formatted incorrectly." + git status + echo "Please run the following locally:" + echo " make fmt" + exit 1 +fi diff --git a/ci/image/Dockerfile b/ci/image/Dockerfile index 44c058d1..51fdb69b 100644 --- a/ci/image/Dockerfile +++ b/ci/image/Dockerfile @@ -1,24 +1,34 @@ FROM golang:1 -RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \ - echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list - -RUN curl -sL https://deb.nodesource.com/setup_12.x | bash - -RUN apt-get install -y nodejs chromium yarn - -COPY ./ci/image/gitignore /etc/git/ignore -RUN git config --system color.ui always -# Need to set set this explicitly for the system since github uses HOME=/home/github. -RUN git config --system core.excludesfile /etc/git/ignore +RUN apt-get update +RUN apt-get install -y chromium +RUN apt-get install -y npm +RUN apt-get install -y shellcheck ENV GOPATH=/root/gopath ENV PATH=$GOPATH/bin:$PATH ENV GOFLAGS="-mod=readonly" ENV PAGER=cat ENV CI=true +ENV MAKEFLAGS="-j --output-sync target" -# Cache go modules, build cache and yarn cache. +COPY ./ci/image/gitignore /root/.config/git/ignore +RUN git config --system color.ui always + +RUN npm install -g prettier +RUN go get golang.org/x/tools/cmd/stringer +RUN go get golang.org/x/tools/cmd/goimports +RUN go get mvdan.cc/sh/cmd/shfmt +RUN go get golang.org/x/lint/golint +RUN go get github.com/agnivade/wasmbrowsertest + +# Cache go modules and build cache. COPY . /tmp/websocket RUN cd /tmp/websocket && \ - yarn && CI= yarn ci && \ + CI= make && \ rm -rf /tmp/websocket + +# GitHub actions tries to override HOME to /github/home and then +# mounts a temp directory into there. We do not want this behaviour. +# I assume it is so that $HOME is preserved between steps in a job. +ENTRYPOINT ["env", "HOME=/root"] diff --git a/ci/image/dockerignore b/ci/image/dockerignore deleted file mode 100644 index 3c3629e6..00000000 --- a/ci/image/dockerignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/ci/image/push.ts b/ci/image/push.ts deleted file mode 100755 index ee2a50dc..00000000 --- a/ci/image/push.ts +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env -S npx ts-node -P ci/tsconfig.json - -import fs from "fs" -import { promisify } from "util" -import { main, spawn } from "../lib" - -main(run, { - timeout: 10 * 60_000, -}) - -async function run(ctx: Promise) { - await promisify(fs.copyFile)("./ci/image/dockerignore", ".dockerignore") - - try { - await spawn(ctx, "docker build -f ./ci/image/Dockerfile -t nhooyr/websocket-ci .", [], { - stdio: "inherit", - }) - await spawn(ctx, "docker push nhooyr/websocket-ci", [], { - stdio: "inherit", - }) - } finally { - await promisify(fs.unlink)(".dockerignore") - } -} diff --git a/ci/lib.ts b/ci/lib.ts deleted file mode 100644 index b6e5d0b4..00000000 --- a/ci/lib.ts +++ /dev/null @@ -1,123 +0,0 @@ -import Timeout from "await-timeout" -import cp from "child-process-promise" -import { ExecOptions, SpawnOptions } from "child_process" - -export async function main( - fn: (ctx: Promise) => void, - opts: { - timeout: number - } = { - timeout: 3 * 60_000, - }, -) { - const timer = new Timeout() - let ctx: Promise = timer.set(opts.timeout, "context timed out") - - const interrupted = new Promise((res, rej) => { - let int = 0 - process.on("SIGINT", () => { - int++ - if (int === 2) { - console.log("force exited") - process.exit(1) - } - rej("") - }) - }) - - ctx = Promise.race([ctx, interrupted]) - const { res, rej, p } = withCancel(ctx) - ctx = p - - try { - await init(ctx) - await fn(ctx) - res!() - } catch (e) { - console.log(e) - rej!() - process.on("beforeExit", () => { - process.exit(1) - }) - } finally { - timer.clear() - } -} - -// TODO promisify native versions -export async function exec(ctx: Promise, cmd: string, opts?: ExecOptions) { - opts = { - ...opts, - } - const p = cp.exec(cmd, opts) - - try { - return await selectCtx(ctx, p) - } finally { - p.childProcess.kill() - } -} - -export async function spawn(ctx: Promise, cmd: string, args: string[], opts?: SpawnOptions) { - if (args === undefined) { - args = [] - } - opts = { - shell: true, - ...opts, - } - const p = cp.spawn(cmd, args, opts) - - try { - return await selectCtx(ctx, p) - } finally { - p.childProcess.kill() - } -} - -async function init(ctx: Promise) { - const r = await exec(ctx, "git rev-parse --show-toplevel", { - cwd: __dirname, - }) - - process.chdir(r.stdout.toString().trim()) -} - -export async function selectCtx(ctx: Promise, p: Promise): Promise { - return (await Promise.race([ctx, p])) as Promise -} - -const cancelSymbol = Symbol() - -export function withCancel(p: Promise) { - let rej: () => void - let res: () => void - const p2 = new Promise((res2, rej2) => { - res = res2 - rej = () => { - rej2(cancelSymbol) - } - }) - - p = Promise.race([p, p2]) - p = p.catch(e => { - // We need this catch to prevent node from complaining about it being unhandled. - // Look into why more later. - if (e === cancelSymbol) { - return - } - throw e - }) as Promise - - return { - res: res!, - rej: rej!, - p: p, - } -} - -export const wasmEnv = { - ...process.env, - GOOS: "js", - GOARCH: "wasm", -} diff --git a/ci/lint.mk b/ci/lint.mk new file mode 100644 index 00000000..f68add41 --- /dev/null +++ b/ci/lint.mk @@ -0,0 +1,16 @@ +lint: govet golint govet-wasm golint-wasm shellcheck + +govet: + go vet ./... + +govet-wasm: + GOOS=js GOARCH=wasm go vet ./... + +golint: + golint -set_exit_status ./... + +golint-wasm: + GOOS=js GOARCH=wasm golint -set_exit_status ./... + +shellcheck: + shellcheck -x $$(git ls-files "*.sh") diff --git a/ci/lint.ts b/ci/lint.ts deleted file mode 100755 index 22a47df3..00000000 --- a/ci/lint.ts +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env -S npx ts-node -P ci/tsconfig.json - -import { exec, main, wasmEnv } from "./lib" - -if (require.main === module) { - main(lint) -} - -export async function lint(ctx: Promise) { - await Promise.all([ - exec(ctx, "go vet ./..."), - exec(ctx, "go run golang.org/x/lint/golint -set_exit_status ./..."), - exec(ctx, "git ls-files '*.ts' | xargs npx eslint --max-warnings 0 --fix", { - cwd: "ci", - }), - wasmLint(ctx), - ]) -} - -async function wasmLint(ctx: Promise) { - await exec(ctx, "go install golang.org/x/lint/golint") - await exec(ctx, "golint -set_exit_status ./...", { - env: wasmEnv, - }) -} diff --git a/ci/test.mk b/ci/test.mk new file mode 100644 index 00000000..2d72eadd --- /dev/null +++ b/ci/test.mk @@ -0,0 +1,24 @@ +test: gotest gotest-wasm + +gotest: _gotest htmlcov +ifdef CI +gotest: codecov +endif + +htmlcov: _gotest + go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html + +codecov: _gotest + curl -s https://codecov.io/bash | bash -s -- -Z -f ci/out/coverage.prof + +_gotest: + echo "--- gotest" && go test -parallel=32 -coverprofile=ci/out/coverage.prof -coverpkg=./... ./... + sed -i '/_stringer.go/d' ci/out/coverage.prof + sed -i '/wsjstest/d' ci/out/coverage.prof + sed -i '/wsecho/d' ci/out/coverage.prof + +gotest-wasm: wsjstest + echo "--- wsjstest" && ./ci/wasmtest.sh + +wsjstest: + go install ./internal/wsjstest diff --git a/ci/test.ts b/ci/test.ts deleted file mode 100755 index b44ae34b..00000000 --- a/ci/test.ts +++ /dev/null @@ -1,88 +0,0 @@ -#!/usr/bin/env -S npx ts-node -P ci/tsconfig.json - -import cp from "child_process" -import * as events from "events" -import * as readline from "readline" -import replaceInFile from "replace-in-file" -import { exec, main, selectCtx, spawn, wasmEnv } from "./lib" - -if (require.main === module) { - main(test) -} - -export async function test(ctx: Promise) { - const args = ["-parallel=32", "-coverprofile=ci/out/coverage.prof", "-coverpkg=./..."] - - if (process.env.CI) { - args.push("-race") - } - - const cliArgs = process.argv.splice(2) - if (cliArgs.length > 0) { - args.push(...cliArgs) - } else { - args.push("./...") - } - - const p1 = spawn(ctx, "go", ["test", ...args], { - stdio: "inherit", - }) - const p2 = wasmTest(ctx) - await Promise.all([p1, p2]) - - // Depending on the code tested, we may not have replaced anything so we do not - // check whether anything was replaced. - await selectCtx( - ctx, - replaceInFile({ - files: "./ci/out/coverage.prof", - from: [/.+frame_stringer.go.+\n/g, /.+wsjstest\/.+\n/g, /.+wsecho\/.+\n/g, /.+assert\/.+\n/g], - to: "", - }), - ) - - let p: Promise = exec(ctx, "go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html") - - if (process.env.CI) { - p = Promise.all([p, codecov(ctx)]) - } - - await p -} - -async function wasmTest(ctx: Promise) { - await Promise.all([ - exec(ctx, "go install ./internal/wsjstest"), - exec(ctx, "go install github.com/agnivade/wasmbrowsertest"), - ]) - - const url = await startWasmTestServer(ctx) - - await exec(ctx, "go test -exec=wasmbrowsertest ./...", { - env: { - ...wasmEnv, - WS_ECHO_SERVER_URL: url, - }, - }) -} - -async function startWasmTestServer(ctx: Promise): Promise { - const wsjstest = cp.spawn("wsjstest") - ctx.finally(wsjstest.kill.bind(wsjstest)) - - const rl = readline.createInterface({ - input: wsjstest.stdout!, - }) - - try { - const p = events.once(rl, "line") - const a = await selectCtx(ctx, p) - return a[0] - } finally { - rl.close() - } -} - -function codecov(ctx: Promise) { - return exec(ctx, "curl -s https://codecov.io/bash | bash -s -- -Z -f ci/out/coverage.prof") -} diff --git a/ci/tools.go b/ci/tools.go deleted file mode 100644 index 4cbd71cf..00000000 --- a/ci/tools.go +++ /dev/null @@ -1,11 +0,0 @@ -// +build tools - -package ci - -// See https://github.com/go-modules-by-example/index/blob/master/010_tools/README.md -import ( - _ "github.com/agnivade/wasmbrowsertest" - _ "go.coder.com/go-tools/cmd/goimports" - _ "golang.org/x/lint/golint" - _ "golang.org/x/tools/cmd/stringer" -) diff --git a/ci/tsconfig.json b/ci/tsconfig.json deleted file mode 100644 index cdf51985..00000000 --- a/ci/tsconfig.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "compilerOptions": { - /* Basic Options */ - "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ - "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ - // "lib": [], /* Specify library files to be included in the compilation. */ - // "allowJs": true, /* Allow javascript files to be compiled. */ - // "checkJs": true, /* Report errors in .js files. */ - // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ - // "declaration": true, /* Generates corresponding '.d.ts' file. */ - // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ - // "sourceMap": true, /* Generates corresponding '.map' file. */ - // "outFile": "./", /* Concatenate and emit output to single file. */ - // "outDir": "./", /* Redirect output structure to the directory. */ - "rootDir": "./ci", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ - // "composite": true, /* Enable project compilation */ - // "incremental": true, /* Enable incremental compilation */ - // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ - // "removeComments": true, /* Do not emit comments to output. */ - "noEmit": true, /* Do not emit outputs. */ - // "importHelpers": true, /* Import emit helpers from 'tslib'. */ - // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ - // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ - - /* Strict Type-Checking Options */ - "strict": true, /* Enable all strict type-checking options. */ - // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ - // "strictNullChecks": true, /* Enable strict null checks. */ - // "strictFunctionTypes": true, /* Enable strict checking of function types. */ - // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ - // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ - // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ - // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ - - /* Additional Checks */ - // "noUnusedLocals": true, /* Report errors on unused locals. */ - // "noUnusedParameters": true, /* Report errors on unused parameters. */ - // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ - // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ - - /* Module Resolution Options */ - // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ - // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ - // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ - // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ - // "typeRoots": [], /* List of folders to include type definitions from. */ - // "types": [], /* Type declaration files to be included in compilation. */ - // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ - // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ - - /* Source Map Options */ - // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ - // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ - // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ - // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ - - /* Experimental Options */ - // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ - // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ - } -} diff --git a/ci/wasmtest.sh b/ci/wasmtest.sh new file mode 100755 index 00000000..21335ce7 --- /dev/null +++ b/ci/wasmtest.sh @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +set -euo pipefail + +WS_ECHO_SERVER_URL="$(wsjstest)" +trap 'pkill -KILL wsjstest' EXIT INT +export WS_ECHO_SERVER_URL + +GOOS=js GOARCH=wasm go test -exec=wasmbrowsertest ./... diff --git a/frame.go b/frame.go index b5301d78..be23330e 100644 --- a/frame.go +++ b/frame.go @@ -8,7 +8,7 @@ import ( "math" ) -//go:generate go run golang.org/x/tools/cmd/stringer -type=opcode,MessageType,StatusCode -output=frame_stringer.go +//go:generate stringer -type=opcode,MessageType,StatusCode -output=frame_stringer.go // opcode represents a WebSocket Opcode. type opcode int diff --git a/go.mod b/go.mod index 0e398366..e733429b 100644 --- a/go.mod +++ b/go.mod @@ -3,23 +3,13 @@ module nhooyr.io/websocket go 1.13 require ( - github.com/agnivade/wasmbrowsertest v0.3.1 - github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6 // indirect - github.com/chromedp/chromedp v0.4.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/go-interpreter/wagon v0.6.0 // indirect github.com/golang/protobuf v1.3.2 github.com/google/go-cmp v0.3.1 - github.com/google/pprof v0.0.0-20190908185732-236ed259b199 // indirect github.com/kr/pretty v0.1.0 // indirect - github.com/mailru/easyjson v0.7.0 // indirect github.com/stretchr/testify v1.4.0 // indirect - go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16 go.uber.org/atomic v1.4.0 // indirect go.uber.org/multierr v1.1.0 - golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac - golang.org/x/sys v0.0.0-20190927073244-c990c680b611 // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 - golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect ) diff --git a/go.sum b/go.sum index afd13b74..954b79f5 100644 --- a/go.sum +++ b/go.sum @@ -1,92 +1,27 @@ -github.com/agnivade/wasmbrowsertest v0.3.1 h1:bA9aA+bcp7KuqGvmCuBdnMqy6PXxFjYP7FxsaT+JSqc= -github.com/agnivade/wasmbrowsertest v0.3.1/go.mod h1:zQt6ZTdl338xxRaMW395qccVE2eQm0SjC/SDz0mPWQI= -github.com/chromedp/cdproto v0.0.0-20190614062957-d6d2f92b486d/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw= -github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0 h1:4Wocv9f+KWF4GtZudyrn8JSBTgHQbGp86mcsoH7j1iQ= -github.com/chromedp/cdproto v0.0.0-20190621002710-8cbd498dd7a0/go.mod h1:S8mB5wY3vV+vRIzf39xDXsw3XKYewW9X6rW2aEmkrSw= -github.com/chromedp/cdproto v0.0.0-20190812224334-39ef923dcb8d/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0= -github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6 h1:C/dvU7FH/BUbkgKS/eYDrja/D5idxH+FxaVmFRWW3E8= -github.com/chromedp/cdproto v0.0.0-20190926234355-1b4886c6fad6/go.mod h1:0YChpVzuLJC5CPr+x3xkHN6Z8KOSXjNbL7qV8Wc4GW0= -github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901 h1:tg66ykM8VYqP9k4DFQwSMnYv84HNTruF+GR6kefFNg4= -github.com/chromedp/chromedp v0.3.1-0.20190619195644-fd957a4d2901/go.mod h1:mJdvfrVn594N9tfiPecUidF6W5jPRKHymqHfzbobPsM= -github.com/chromedp/chromedp v0.4.0 h1:0AJC5ejETuh/6n7Tcsw4u4G0eKZkI9aVRwckWaImLUE= -github.com/chromedp/chromedp v0.4.0/go.mod h1:DC3QUn4mJ24dwjcaGQLoZrhm4X/uPHZ6spDbS2uFhm4= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/edsrzf/mmap-go v1.0.0 h1:CEBF7HpRnUCSJgGUb5h1Gm7e3VkmVDrR8lvWVLtrOFw= -github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= -github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c h1:DLLAPVFrk9iNzljMKF512CUmrFImQ6WU3sDiUS4IRqk= -github.com/go-interpreter/wagon v0.5.1-0.20190713202023-55a163980b6c/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc= -github.com/go-interpreter/wagon v0.6.0 h1:BBxDxjiJiHgw9EdkYXAWs8NHhwnazZ5P2EWBW5hFNWw= -github.com/go-interpreter/wagon v0.6.0/go.mod h1:5+b/MBYkclRZngKF5s6qrgWxSLgE9F5dFdO1hAueZLc= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= -github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= -github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= -github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f h1:Jnx61latede7zDD3DiiP4gmNz33uK0U5HDUaF0a/HVQ= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190908185732-236ed259b199 h1:sEyCq3pOT7tNC+3gcLI7sZkBDgntZ6wQJNmr9lmIjIc= -github.com/google/pprof v0.0.0-20190908185732-236ed259b199/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307 h1:vl4eIlySbjertFaNwiMjXsGrFVK25aOWLq7n+3gh2ls= -github.com/knq/sysutil v0.0.0-20181215143952-f05b59f0f307/go.mod h1:BjPj+aVjl9FW/cCGiF3nGh5v+9Gd3VCgBQbod/GlMaQ= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/mailru/easyjson v0.0.0-20190403194419-1ea4449da983/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481 h1:IaSjLMT6WvkoZZjspGxy3rdaTEmWLoRm49WbtVUi9sA= -github.com/mailru/easyjson v0.0.0-20190620125010-da37f6c1e481/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM= -github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc h1:RTUQlKzoZZVG3umWNzOYeFecQLIh+dbxXvJp1zPQJTI= -github.com/twitchyliquid64/golang-asm v0.0.0-20190126203739-365674df15fc/go.mod h1:NoCfSFWosfqMqmmD7hApkirIK9ozpHjxRnRxs1l413A= -go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16 h1:3gGa1bM0nG7Ruhu5b7wKnoOOwAD/fJ8iyyAcpOzDG3A= -go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16/go.mod h1:iKV5yK9t+J5nG9O3uF6KYdPEz3dyfMyB15MN1rbQ8Qw= go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac h1:8R1esu+8QioDxo4E4mX6bFztO+dMTM49DNAaWfO5OeY= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190306220234-b354f8bf4d9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190618155005-516e3c20635f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190927073244-c990c680b611 h1:q9u40nxWT5zRClI/uU9dHCiYGottAg6Nzz4YUQyHxdA= -golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72 h1:bw9doJza/SFBEweII/rHQh338oozWyiFsBRHtrflcws= -golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= diff --git a/internal/wsjstest/main.go b/internal/wsjstest/main.go index 8145061b..5251fb84 100644 --- a/internal/wsjstest/main.go +++ b/internal/wsjstest/main.go @@ -6,19 +6,60 @@ import ( "errors" "fmt" "log" + "net" "net/http" - "net/http/httptest" "os" - "os/signal" - "strings" - "syscall" + "os/exec" "nhooyr.io/websocket" "nhooyr.io/websocket/internal/wsecho" ) +func fork() net.Listener { + if os.Getenv("FORKED") != "" { + f := os.NewFile(3, "listener") + l, err := net.FileListener(f) + if err != nil { + log.Fatalf("failed to create listener from fd: %+v", err) + } + return l + } + + l, err := net.Listen("tcp", "localhost:0") + if err != nil { + log.Fatalf("failed to listen: %+v", err) + } + f, err := l.(*net.TCPListener).File() + if err != nil { + log.Fatalf("failed to get file from tcp listener: %+v", err) + } + + cmd := exec.Command(os.Args[0]) + cmd.Stderr = os.Stderr + cmd.Env = append(os.Environ(), + fmt.Sprintf("FORKED=true"), + ) + cmd.ExtraFiles = append(cmd.ExtraFiles, f) + err = cmd.Start() + if err != nil { + log.Fatalf("failed to start command: %+v", err) + } + + fmt.Printf("ws://%v\n", l.Addr().String()) + os.Exit(0) + + panic("unreachable") +} + func main() { - s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + l := fork() + + err := serve(l) + log.Fatalf("failed to serve: %+v", err) +} + +func serve(l net.Listener) error { + return http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ Subprotocols: []string{"echo"}, InsecureSkipVerify: true, @@ -36,11 +77,4 @@ func main() { } })) - wsURL := strings.Replace(s.URL, "http", "ws", 1) - fmt.Printf("%v\n", wsURL) - - sigs := make(chan os.Signal) - signal.Notify(sigs, syscall.SIGTERM) - - <-sigs } diff --git a/package.json b/package.json deleted file mode 100644 index a8f436b0..00000000 --- a/package.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "private": true, - "devDependencies": { - "@types/await-timeout": "^0.3.1", - "@types/child-process-promise": "^2.2.1", - "@types/node": "^12.7.9", - "@typescript-eslint/eslint-plugin": "^2.3.2", - "@typescript-eslint/parser": "^2.3.2", - "await-timeout": "^0.6.0", - "axios": "^0.19.0", - "child-process-promise": "^2.2.1", - "eslint": "^6.5.1", - "eslint-config-prettier": "^6.3.0", - "eslint-plugin-import": "^2.18.2", - "prettier": "^1.18.2", - "replace-in-file": "^4.1.3", - "ts-node": "^8.4.1", - "typescript": "^3.6.3" - }, - "scripts": { - "fmt": "./ci/fmt.ts", - "lint": "./ci/lint.ts", - "test": "./ci/test.ts", - "ci": "./ci/all.ts", - "push-ci-image": "ci/image/push.ts" - } -} diff --git a/yarn.lock b/yarn.lock deleted file mode 100644 index a8690d81..00000000 --- a/yarn.lock +++ /dev/null @@ -1,1503 +0,0 @@ -# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. -# yarn lockfile v1 - - -"@babel/code-frame@^7.0.0": - version "7.5.5" - resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.5.5.tgz#bc0782f6d69f7b7d49531219699b988f669a8f9d" - integrity sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw== - dependencies: - "@babel/highlight" "^7.0.0" - -"@babel/highlight@^7.0.0": - version "7.5.0" - resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.5.0.tgz#56d11312bd9248fa619591d02472be6e8cb32540" - integrity sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ== - dependencies: - chalk "^2.0.0" - esutils "^2.0.2" - js-tokens "^4.0.0" - -"@types/await-timeout@^0.3.1": - version "0.3.1" - resolved "https://registry.yarnpkg.com/@types/await-timeout/-/await-timeout-0.3.1.tgz#3a0baafc3a96c7a14447a4dcfdcc76b21ce97c3b" - integrity sha512-H5PzROT4KuP7XQDua13Iw8did//OCKAZ/3TL15DjvMzDonrk4HvhH1+tLko96f2guU6XaD3AoqRa49ZOwbwNig== - -"@types/child-process-promise@^2.2.1": - version "2.2.1" - resolved "https://registry.yarnpkg.com/@types/child-process-promise/-/child-process-promise-2.2.1.tgz#049033bef102f77a1719b38672cc86a2c4710ab1" - integrity sha512-xZ4kkF82YkmqPCERqV9Tj0bVQj3Tk36BqGlNgxv5XhifgDRhwAqp+of+sccksdpZRbbPsNwMOkmUqOnLgxKtGw== - dependencies: - "@types/node" "*" - -"@types/eslint-visitor-keys@^1.0.0": - version "1.0.0" - resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" - integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== - -"@types/json-schema@^7.0.3": - version "7.0.3" - resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.3.tgz#bdfd69d61e464dcc81b25159c270d75a73c1a636" - integrity sha512-Il2DtDVRGDcqjDtE+rF8iqg1CArehSK84HZJCT7AMITlyXRBpuPhqGLDQMowraqqu1coEaimg4ZOqggt6L6L+A== - -"@types/node@*", "@types/node@^12.7.9": - version "12.7.9" - resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.9.tgz#da0210f91096aa67138cf5afd04c4d629f8a406a" - integrity sha512-P57oKTJ/vYivL2BCfxCC5tQjlS8qW31pbOL6qt99Yrjm95YdHgNZwjrTTjMBh+C2/y6PXIX4oz253+jUzxKKfQ== - -"@typescript-eslint/eslint-plugin@^2.3.2": - version "2.3.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.3.2.tgz#7e112ca0bb29044d915baf10163a8199a20f7c69" - integrity sha512-tcnpksq1bXzcIRbYLeXkgp6l+ggEMXXUcl1wsSvL807fRtmvVQKygElwEUf4hBA76dNag3VAK1q2m3vd7qJaZA== - dependencies: - "@typescript-eslint/experimental-utils" "2.3.2" - eslint-utils "^1.4.2" - functional-red-black-tree "^1.0.1" - regexpp "^2.0.1" - tsutils "^3.17.1" - -"@typescript-eslint/experimental-utils@2.3.2": - version "2.3.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.3.2.tgz#e50f31264507e6fec7b33840bb6af260c24f4ea8" - integrity sha512-t+JGdTT6dRbmvKDlhlVkEueoZa0fhJNfG6z2cpnRPLwm3VwYr2BjR//acJGC1Yza0I9ZNcDfRY7ubQEvvfG6Jg== - dependencies: - "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "2.3.2" - eslint-scope "^5.0.0" - -"@typescript-eslint/parser@^2.3.2": - version "2.3.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-2.3.2.tgz#e9b742e191cd1209930da469cde379591ad0af5b" - integrity sha512-nq1UQeNGdKdqdgF6Ww+Ov2OidWgiL96+JYdXXZ2rkP/OWyc6KMNSbs6MpRCpI8q+PmDa7hBnHNQIo7w/drYccA== - dependencies: - "@types/eslint-visitor-keys" "^1.0.0" - "@typescript-eslint/experimental-utils" "2.3.2" - "@typescript-eslint/typescript-estree" "2.3.2" - eslint-visitor-keys "^1.1.0" - -"@typescript-eslint/typescript-estree@2.3.2": - version "2.3.2" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.3.2.tgz#107414aa04e689fe6f7251eb63fb500217f2b7f4" - integrity sha512-eZNEAai16nwyhIVIEaWQlaUgAU3S9CkQ58qvK0+3IuSdLJD3W1PNuehQFMIhW/mTP1oFR9GNoTcLg7gtXz6lzA== - dependencies: - glob "^7.1.4" - is-glob "^4.0.1" - lodash.unescape "4.0.1" - semver "^6.3.0" - -acorn-jsx@^5.0.2: - version "5.0.2" - resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.0.2.tgz#84b68ea44b373c4f8686023a551f61a21b7c4a4f" - integrity sha512-tiNTrP1MP0QrChmD2DdupCr6HWSFeKVw5d/dHTu4Y7rkAkRhU/Dt7dphAfIUyxtHpl/eBVip5uTNSpQJHylpAw== - -acorn@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.1.0.tgz#949d36f2c292535da602283586c2477c57eb2d6c" - integrity sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ== - -ajv@^6.10.0, ajv@^6.10.2: - version "6.10.2" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.2.tgz#d3cea04d6b017b2894ad69040fec8b623eb4bd52" - integrity sha512-TXtUUEYHuaTEbLZWIKUr5pmBuhDLy+8KYtPYdcV8qC+pOZL+NKqYwvWSRrVXHn+ZmRRAu8vJTAznH7Oag6RVRw== - dependencies: - fast-deep-equal "^2.0.1" - fast-json-stable-stringify "^2.0.0" - json-schema-traverse "^0.4.1" - uri-js "^4.2.2" - -ansi-escapes@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" - integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== - -ansi-regex@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-3.0.0.tgz#ed0317c322064f79466c02966bddb605ab37d998" - integrity sha1-7QMXwyIGT3lGbAKWa922Bas32Zg= - -ansi-regex@^4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-4.1.0.tgz#8b9f8f08cf1acb843756a839ca8c7e3168c51997" - integrity sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg== - -ansi-styles@^3.2.0, ansi-styles@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" - integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== - dependencies: - color-convert "^1.9.0" - -arg@^4.1.0: - version "4.1.1" - resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.1.tgz#485f8e7c390ce4c5f78257dbea80d4be11feda4c" - integrity sha512-SlmP3fEA88MBv0PypnXZ8ZfJhwmDeIE3SP71j37AiXQBXYosPV0x6uISAaHYSlSVhmHOVkomen0tbGk6Anlebw== - -argparse@^1.0.7: - version "1.0.10" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" - integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg== - dependencies: - sprintf-js "~1.0.2" - -array-includes@^3.0.3: - version "3.0.3" - resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.0.3.tgz#184b48f62d92d7452bb31b323165c7f8bd02266d" - integrity sha1-GEtI9i2S10UrsxsyMWXH+L0CJm0= - dependencies: - define-properties "^1.1.2" - es-abstract "^1.7.0" - -astral-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" - integrity sha512-+Ryf6g3BKoRc7jfp7ad8tM4TtMiaWvbF/1/sQcZPkkS7ag3D5nMBCe2UfOTONtAkaG0tO0ij3C5Lwmf1EiyjHg== - -await-timeout@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/await-timeout/-/await-timeout-0.6.0.tgz#efb52f5dba4d5fea6cff043705b09c97e7c403dc" - integrity sha512-eMGnZxbqnmYTxCPqSYk5paIbPgcX8auC0UyVMScYMPY8pYmdk6o2wqQGw+SyN2hrhaDVZIQHVZX8DaHuBoRZcg== - -axios@^0.19.0: - version "0.19.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.0.tgz#8e09bff3d9122e133f7b8101c8fbdd00ed3d2ab8" - integrity sha512-1uvKqKQta3KBxIz14F2v06AEHZ/dIoeKfbTRkK1E5oqjDnuEerLmYTgJB5AiQZHJcljpg1TuRzdjDR06qNk0DQ== - dependencies: - follow-redirects "1.5.10" - is-buffer "^2.0.2" - -balanced-match@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.0.tgz#89b4d199ab2bee49de164ea02b89ce462d71b767" - integrity sha1-ibTRmasr7kneFk6gK4nORi1xt2c= - -brace-expansion@^1.1.7: - version "1.1.11" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" - integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== - dependencies: - balanced-match "^1.0.0" - concat-map "0.0.1" - -buffer-from@^1.0.0: - version "1.1.1" - resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.1.tgz#32713bc028f75c02fdb710d7c7bcec1f2c6070ef" - integrity sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A== - -callsites@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" - integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== - -camelcase@^5.0.0: - version "5.3.1" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320" - integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg== - -chalk@^2.0.0, chalk@^2.1.0, chalk@^2.4.2: - version "2.4.2" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" - integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== - dependencies: - ansi-styles "^3.2.1" - escape-string-regexp "^1.0.5" - supports-color "^5.3.0" - -chardet@^0.7.0: - version "0.7.0" - resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" - integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== - -child-process-promise@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/child-process-promise/-/child-process-promise-2.2.1.tgz#4730a11ef610fad450b8f223c79d31d7bdad8074" - integrity sha1-RzChHvYQ+tRQuPIjx50x172tgHQ= - dependencies: - cross-spawn "^4.0.2" - node-version "^1.0.0" - promise-polyfill "^6.0.1" - -cli-cursor@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-2.1.0.tgz#b35dac376479facc3e94747d41d0d0f5238ffcb5" - integrity sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU= - dependencies: - restore-cursor "^2.0.0" - -cli-width@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-2.2.0.tgz#ff19ede8a9a5e579324147b0c11f0fbcbabed639" - integrity sha1-/xnt6Kml5XkyQUewwR8PvLq+1jk= - -cliui@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" - integrity sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA== - dependencies: - string-width "^3.1.0" - strip-ansi "^5.2.0" - wrap-ansi "^5.1.0" - -color-convert@^1.9.0: - version "1.9.3" - resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" - integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== - dependencies: - color-name "1.1.3" - -color-name@1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" - integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= - -concat-map@0.0.1: - version "0.0.1" - resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" - integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= - -contains-path@^0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" - integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo= - -cross-spawn@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-4.0.2.tgz#7b9247621c23adfdd3856004a823cbe397424d41" - integrity sha1-e5JHYhwjrf3ThWAEqCPL45dCTUE= - dependencies: - lru-cache "^4.0.1" - which "^1.2.9" - -cross-spawn@^6.0.5: - version "6.0.5" - resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-6.0.5.tgz#4a5ec7c64dfae22c3a14124dbacdee846d80cbc4" - integrity sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ== - dependencies: - nice-try "^1.0.4" - path-key "^2.0.1" - semver "^5.5.0" - shebang-command "^1.2.0" - which "^1.2.9" - -debug@=3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - -debug@^2.6.8, debug@^2.6.9: - version "2.6.9" - resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" - integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== - dependencies: - ms "2.0.0" - -debug@^4.0.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" - integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== - dependencies: - ms "^2.1.1" - -decamelize@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" - integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= - -deep-is@~0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.3.tgz#b369d6fb5dbc13eecf524f91b070feedc357cf34" - integrity sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ= - -define-properties@^1.1.2, define-properties@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" - integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== - dependencies: - object-keys "^1.0.12" - -diff@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.1.tgz#0c667cb467ebbb5cea7f14f135cc2dba7780a8ff" - integrity sha512-s2+XdvhPCOF01LRQBC8hf4vhbVmI2CGS5aZnxLJlT5FtdhPCDFq80q++zK2KlrVorVDdL5BOGZ/VfLrVtYNF+Q== - -doctrine@1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" - integrity sha1-N53Ocw9hZvds76TmcHoVmwLFpvo= - dependencies: - esutils "^2.0.2" - isarray "^1.0.0" - -doctrine@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" - integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== - dependencies: - esutils "^2.0.2" - -emoji-regex@^7.0.1: - version "7.0.3" - resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" - integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== - -error-ex@^1.2.0: - version "1.3.2" - resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.2.tgz#b4ac40648107fdcdcfae242f428bea8a14d4f1bf" - integrity sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g== - dependencies: - is-arrayish "^0.2.1" - -es-abstract@^1.12.0, es-abstract@^1.7.0: - version "1.15.0" - resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.15.0.tgz#8884928ec7e40a79e3c9bc812d37d10c8b24cc57" - integrity sha512-bhkEqWJ2t2lMeaJDuk7okMkJWI/yqgH/EoGwpcvv0XW9RWQsRspI4wt6xuyuvMvvQE3gg/D9HXppgk21w78GyQ== - dependencies: - es-to-primitive "^1.2.0" - function-bind "^1.1.1" - has "^1.0.3" - has-symbols "^1.0.0" - is-callable "^1.1.4" - is-regex "^1.0.4" - object-inspect "^1.6.0" - object-keys "^1.1.1" - string.prototype.trimleft "^2.1.0" - string.prototype.trimright "^2.1.0" - -es-to-primitive@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.0.tgz#edf72478033456e8dda8ef09e00ad9650707f377" - integrity sha512-qZryBOJjV//LaxLTV6UC//WewneB3LcXOL9NP++ozKVXsIIIpm/2c13UDiD9Jp2eThsecw9m3jPqDwTyobcdbg== - dependencies: - is-callable "^1.1.4" - is-date-object "^1.0.1" - is-symbol "^1.0.2" - -escape-string-regexp@^1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" - integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= - -eslint-config-prettier@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.3.0.tgz#e73b48e59dc49d950843f3eb96d519e2248286a3" - integrity sha512-EWaGjlDAZRzVFveh2Jsglcere2KK5CJBhkNSa1xs3KfMUGdRiT7lG089eqPdvlzWHpAqaekubOsOMu8W8Yk71A== - dependencies: - get-stdin "^6.0.0" - -eslint-import-resolver-node@^0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.2.tgz#58f15fb839b8d0576ca980413476aab2472db66a" - integrity sha512-sfmTqJfPSizWu4aymbPr4Iidp5yKm8yDkHp+Ir3YiTHiiDfxh69mOUsmiqW6RZ9zRXFaF64GtYmN7e+8GHBv6Q== - dependencies: - debug "^2.6.9" - resolve "^1.5.0" - -eslint-module-utils@^2.4.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.4.1.tgz#7b4675875bf96b0dbf1b21977456e5bb1f5e018c" - integrity sha512-H6DOj+ejw7Tesdgbfs4jeS4YMFrT8uI8xwd1gtQqXssaR0EQ26L+2O/w6wkYFy2MymON0fTwHmXBvvfLNZVZEw== - dependencies: - debug "^2.6.8" - pkg-dir "^2.0.0" - -eslint-plugin-import@^2.18.2: - version "2.18.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz#02f1180b90b077b33d447a17a2326ceb400aceb6" - integrity sha512-5ohpsHAiUBRNaBWAF08izwUGlbrJoJJ+W9/TBwsGoR1MnlgfwMIKrFeSjWbt6moabiXW9xNvtFz+97KHRfI4HQ== - dependencies: - array-includes "^3.0.3" - contains-path "^0.1.0" - debug "^2.6.9" - doctrine "1.5.0" - eslint-import-resolver-node "^0.3.2" - eslint-module-utils "^2.4.0" - has "^1.0.3" - minimatch "^3.0.4" - object.values "^1.1.0" - read-pkg-up "^2.0.0" - resolve "^1.11.0" - -eslint-scope@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.0.0.tgz#e87c8887c73e8d1ec84f1ca591645c358bfc8fb9" - integrity sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw== - dependencies: - esrecurse "^4.1.0" - estraverse "^4.1.1" - -eslint-utils@^1.4.2: - version "1.4.2" - resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.2.tgz#166a5180ef6ab7eb462f162fd0e6f2463d7309ab" - integrity sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q== - dependencies: - eslint-visitor-keys "^1.0.0" - -eslint-visitor-keys@^1.0.0, eslint-visitor-keys@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2" - integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A== - -eslint@^6.5.1: - version "6.5.1" - resolved "https://registry.yarnpkg.com/eslint/-/eslint-6.5.1.tgz#828e4c469697d43bb586144be152198b91e96ed6" - integrity sha512-32h99BoLYStT1iq1v2P9uwpyznQ4M2jRiFB6acitKz52Gqn+vPaMDUTB1bYi1WN4Nquj2w+t+bimYUG83DC55A== - dependencies: - "@babel/code-frame" "^7.0.0" - ajv "^6.10.0" - chalk "^2.1.0" - cross-spawn "^6.0.5" - debug "^4.0.1" - doctrine "^3.0.0" - eslint-scope "^5.0.0" - eslint-utils "^1.4.2" - eslint-visitor-keys "^1.1.0" - espree "^6.1.1" - esquery "^1.0.1" - esutils "^2.0.2" - file-entry-cache "^5.0.1" - functional-red-black-tree "^1.0.1" - glob-parent "^5.0.0" - globals "^11.7.0" - ignore "^4.0.6" - import-fresh "^3.0.0" - imurmurhash "^0.1.4" - inquirer "^6.4.1" - is-glob "^4.0.0" - js-yaml "^3.13.1" - json-stable-stringify-without-jsonify "^1.0.1" - levn "^0.3.0" - lodash "^4.17.14" - minimatch "^3.0.4" - mkdirp "^0.5.1" - natural-compare "^1.4.0" - optionator "^0.8.2" - progress "^2.0.0" - regexpp "^2.0.1" - semver "^6.1.2" - strip-ansi "^5.2.0" - strip-json-comments "^3.0.1" - table "^5.2.3" - text-table "^0.2.0" - v8-compile-cache "^2.0.3" - -espree@^6.1.1: - version "6.1.1" - resolved "https://registry.yarnpkg.com/espree/-/espree-6.1.1.tgz#7f80e5f7257fc47db450022d723e356daeb1e5de" - integrity sha512-EYbr8XZUhWbYCqQRW0duU5LxzL5bETN6AjKBGy1302qqzPaCH10QbRg3Wvco79Z8x9WbiE8HYB4e75xl6qUYvQ== - dependencies: - acorn "^7.0.0" - acorn-jsx "^5.0.2" - eslint-visitor-keys "^1.1.0" - -esprima@^4.0.0: - version "4.0.1" - resolved "https://registry.yarnpkg.com/esprima/-/esprima-4.0.1.tgz#13b04cdb3e6c5d19df91ab6987a8695619b0aa71" - integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== - -esquery@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.0.1.tgz#406c51658b1f5991a5f9b62b1dc25b00e3e5c708" - integrity sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA== - dependencies: - estraverse "^4.0.0" - -esrecurse@^4.1.0: - version "4.2.1" - resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.2.1.tgz#007a3b9fdbc2b3bb87e4879ea19c92fdbd3942cf" - integrity sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ== - dependencies: - estraverse "^4.1.0" - -estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1: - version "4.3.0" - resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" - integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== - -esutils@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" - integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== - -external-editor@^3.0.3: - version "3.1.0" - resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" - integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== - dependencies: - chardet "^0.7.0" - iconv-lite "^0.4.24" - tmp "^0.0.33" - -fast-deep-equal@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz#7b05218ddf9667bf7f370bf7fdb2cb15fdd0aa49" - integrity sha1-ewUhjd+WZ79/Nwv3/bLLFf3Qqkk= - -fast-json-stable-stringify@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz#d5142c0caee6b1189f87d3a76111064f86c8bbf2" - integrity sha1-1RQsDK7msRifh9OnYREGT4bIu/I= - -fast-levenshtein@~2.0.4: - version "2.0.6" - resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" - integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= - -figures@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/figures/-/figures-2.0.0.tgz#3ab1a2d2a62c8bfb431a0c94cb797a2fce27c962" - integrity sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI= - dependencies: - escape-string-regexp "^1.0.5" - -file-entry-cache@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" - integrity sha512-bCg29ictuBaKUwwArK4ouCaqDgLZcysCFLmM/Yn/FDoqndh/9vNuQfXRDvTuXKLxfD/JtZQGKFT8MGcJBK644g== - dependencies: - flat-cache "^2.0.1" - -find-up@^2.0.0, find-up@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" - integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= - dependencies: - locate-path "^2.0.0" - -find-up@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/find-up/-/find-up-3.0.0.tgz#49169f1d7993430646da61ecc5ae355c21c97b73" - integrity sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg== - dependencies: - locate-path "^3.0.0" - -flat-cache@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-2.0.1.tgz#5d296d6f04bda44a4630a301413bdbc2ec085ec0" - integrity sha512-LoQe6yDuUMDzQAEH8sgmh4Md6oZnc/7PjtwjNFSzveXqSHt6ka9fPBuso7IGf9Rz4uqnSnWiFH2B/zj24a5ReA== - dependencies: - flatted "^2.0.0" - rimraf "2.6.3" - write "1.0.3" - -flatted@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/flatted/-/flatted-2.0.1.tgz#69e57caa8f0eacbc281d2e2cb458d46fdb449e08" - integrity sha512-a1hQMktqW9Nmqr5aktAux3JMNqaucxGcjtjWnZLHX7yyPCmlSV3M54nGYbqT8K+0GhF3NBgmJCc3ma+WOgX8Jg== - -follow-redirects@1.5.10: - version "1.5.10" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" - integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== - dependencies: - debug "=3.1.0" - -fs.realpath@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" - integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= - -function-bind@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" - integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== - -functional-red-black-tree@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" - integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= - -get-caller-file@^2.0.1: - version "2.0.5" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" - integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== - -get-stdin@^6.0.0: - version "6.0.0" - resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-6.0.0.tgz#9e09bf712b360ab9225e812048f71fde9c89657b" - integrity sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g== - -glob-parent@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.0.tgz#5f4c1d1e748d30cd73ad2944b3577a81b081e8c2" - integrity sha512-qjtRgnIVmOfnKUE3NJAQEdk+lKrxfw8t5ke7SXtfMTHcjsBfOfWXCQfdb30zfDoZQ2IRSIiidmjtbHZPZ++Ihw== - dependencies: - is-glob "^4.0.1" - -glob@^7.1.3, glob@^7.1.4: - version "7.1.4" - resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" - integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -globals@^11.7.0: - version "11.12.0" - resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" - integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== - -graceful-fs@^4.1.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.2.tgz#6f0952605d0140c1cfdb138ed005775b92d67b02" - integrity sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q== - -has-flag@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" - integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= - -has-symbols@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.0.tgz#ba1a8f1af2a0fc39650f5c850367704122063b44" - integrity sha1-uhqPGvKg/DllD1yFA2dwQSIGO0Q= - -has@^1.0.1, has@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" - integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== - dependencies: - function-bind "^1.1.1" - -hosted-git-info@^2.1.4: - version "2.8.4" - resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.4.tgz#44119abaf4bc64692a16ace34700fed9c03e2546" - integrity sha512-pzXIvANXEFrc5oFFXRMkbLPQ2rXRoDERwDLyrcUxGhaZhgP54BBSl9Oheh7Vv0T090cszWBxPjkQQ5Sq1PbBRQ== - -iconv-lite@^0.4.24: - version "0.4.24" - resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" - integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== - dependencies: - safer-buffer ">= 2.1.2 < 3" - -ignore@^4.0.6: - version "4.0.6" - resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" - integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== - -import-fresh@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.1.0.tgz#6d33fa1dcef6df930fae003446f33415af905118" - integrity sha512-PpuksHKGt8rXfWEr9m9EHIpgyyaltBy8+eF6GJM0QCAxMgxCfucMF3mjecK2QsJr0amJW7gTqh5/wht0z2UhEQ== - dependencies: - parent-module "^1.0.0" - resolve-from "^4.0.0" - -imurmurhash@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" - integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= - -inflight@^1.0.4: - version "1.0.6" - resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" - integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= - dependencies: - once "^1.3.0" - wrappy "1" - -inherits@2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" - integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== - -inquirer@^6.4.1: - version "6.5.2" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca" - integrity sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ== - dependencies: - ansi-escapes "^3.2.0" - chalk "^2.4.2" - cli-cursor "^2.1.0" - cli-width "^2.0.0" - external-editor "^3.0.3" - figures "^2.0.0" - lodash "^4.17.12" - mute-stream "0.0.7" - run-async "^2.2.0" - rxjs "^6.4.0" - string-width "^2.1.0" - strip-ansi "^5.1.0" - through "^2.3.6" - -is-arrayish@^0.2.1: - version "0.2.1" - resolved "https://registry.yarnpkg.com/is-arrayish/-/is-arrayish-0.2.1.tgz#77c99840527aa8ecb1a8ba697b80645a7a926a9d" - integrity sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0= - -is-buffer@^2.0.2: - version "2.0.4" - resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-2.0.4.tgz#3e572f23c8411a5cfd9557c849e3665e0b290623" - integrity sha512-Kq1rokWXOPXWuaMAqZiJW4XxsmD9zGx9q4aePabbn3qCRGedtH7Cm+zV8WETitMfu1wdh+Rvd6w5egwSngUX2A== - -is-callable@^1.1.4: - version "1.1.4" - resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.1.4.tgz#1e1adf219e1eeb684d691f9d6a05ff0d30a24d75" - integrity sha512-r5p9sxJjYnArLjObpjA4xu5EKI3CuKHkJXMhT7kwbpUyIFD1n5PMAsoPvWnvtZiNz7LjkYDRZhd7FlI0eMijEA== - -is-date-object@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.1.tgz#9aa20eb6aeebbff77fbd33e74ca01b33581d3a16" - integrity sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY= - -is-extglob@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" - integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= - -is-fullwidth-code-point@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" - integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= - -is-glob@^4.0.0, is-glob@^4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" - integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== - dependencies: - is-extglob "^2.1.1" - -is-promise@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" - integrity sha1-eaKp7OfwlugPNtKy87wWwf9L8/o= - -is-regex@^1.0.4: - version "1.0.4" - resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491" - integrity sha1-VRdIm1RwkbCTDglWVM7SXul+lJE= - dependencies: - has "^1.0.1" - -is-symbol@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.2.tgz#a055f6ae57192caee329e7a860118b497a950f38" - integrity sha512-HS8bZ9ox60yCJLH9snBpIwv9pYUAkcuLhSA1oero1UB5y9aiQpRA8y2ex945AOtCZL1lJDeIk3G5LthswI46Lw== - dependencies: - has-symbols "^1.0.0" - -isarray@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" - integrity sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE= - -isexe@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" - integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= - -js-tokens@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" - integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== - -js-yaml@^3.13.1: - version "3.13.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" - integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== - dependencies: - argparse "^1.0.7" - esprima "^4.0.0" - -json-schema-traverse@^0.4.1: - version "0.4.1" - resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" - integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== - -json-stable-stringify-without-jsonify@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" - integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= - -levn@^0.3.0, levn@~0.3.0: - version "0.3.0" - resolved "https://registry.yarnpkg.com/levn/-/levn-0.3.0.tgz#3b09924edf9f083c0490fdd4c0bc4421e04764ee" - integrity sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4= - dependencies: - prelude-ls "~1.1.2" - type-check "~0.3.2" - -load-json-file@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" - integrity sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg= - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - strip-bom "^3.0.0" - -locate-path@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" - integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= - dependencies: - p-locate "^2.0.0" - path-exists "^3.0.0" - -locate-path@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-3.0.0.tgz#dbec3b3ab759758071b58fe59fc41871af21400e" - integrity sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A== - dependencies: - p-locate "^3.0.0" - path-exists "^3.0.0" - -lodash.unescape@4.0.1: - version "4.0.1" - resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c" - integrity sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw= - -lodash@^4.17.12, lodash@^4.17.14: - version "4.17.15" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" - integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== - -lru-cache@^4.0.1: - version "4.1.5" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.5.tgz#8bbe50ea85bed59bc9e33dcab8235ee9bcf443cd" - integrity sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g== - dependencies: - pseudomap "^1.0.2" - yallist "^2.1.2" - -make-error@^1.1.1: - version "1.3.5" - resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.5.tgz#efe4e81f6db28cadd605c70f29c831b58ef776c8" - integrity sha512-c3sIjNUow0+8swNwVpqoH4YCShKNFkMaw6oH1mNS2haDZQqkeZFlHS3dhoeEbKKmJB4vXpJucU6oH75aDYeE9g== - -mimic-fn@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" - integrity sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ== - -minimatch@^3.0.4: - version "3.0.4" - resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" - integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== - dependencies: - brace-expansion "^1.1.7" - -minimist@0.0.8: - version "0.0.8" - resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" - integrity sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0= - -mkdirp@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" - integrity sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM= - dependencies: - minimist "0.0.8" - -ms@2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" - integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= - -ms@^2.1.1: - version "2.1.2" - resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" - integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== - -mute-stream@0.0.7: - version "0.0.7" - resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" - integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= - -natural-compare@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" - integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= - -nice-try@^1.0.4: - version "1.0.5" - resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" - integrity sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ== - -node-version@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/node-version/-/node-version-1.2.0.tgz#34fde3ffa8e1149bd323983479dda620e1b5060d" - integrity sha512-ma6oU4Sk0qOoKEAymVoTvk8EdXEobdS7m/mAGhDJ8Rouugho48crHBORAmy5BoOcv8wraPM6xumapQp5hl4iIQ== - -normalize-package-data@^2.3.2: - version "2.5.0" - resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" - integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== - dependencies: - hosted-git-info "^2.1.4" - resolve "^1.10.0" - semver "2 || 3 || 4 || 5" - validate-npm-package-license "^3.0.1" - -object-inspect@^1.6.0: - version "1.6.0" - resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.6.0.tgz#c70b6cbf72f274aab4c34c0c82f5167bf82cf15b" - integrity sha512-GJzfBZ6DgDAmnuaM3104jR4s1Myxr3Y3zfIyN4z3UdqN69oSRacNK8UhnobDdC+7J2AHCjGwxQubNJfE70SXXQ== - -object-keys@^1.0.12, object-keys@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" - integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== - -object.values@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.0.tgz#bf6810ef5da3e5325790eaaa2be213ea84624da9" - integrity sha512-8mf0nKLAoFX6VlNVdhGj31SVYpaNFtUnuoOXWyFEstsWRgU837AK+JYM0iAxwkSzGRbwn8cbFmgbyxj1j4VbXg== - dependencies: - define-properties "^1.1.3" - es-abstract "^1.12.0" - function-bind "^1.1.1" - has "^1.0.3" - -once@^1.3.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" - integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= - dependencies: - wrappy "1" - -onetime@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/onetime/-/onetime-2.0.1.tgz#067428230fd67443b2794b22bba528b6867962d4" - integrity sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ= - dependencies: - mimic-fn "^1.0.0" - -optionator@^0.8.2: - version "0.8.2" - resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.8.2.tgz#364c5e409d3f4d6301d6c0b4c05bba50180aeb64" - integrity sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q= - dependencies: - deep-is "~0.1.3" - fast-levenshtein "~2.0.4" - levn "~0.3.0" - prelude-ls "~1.1.2" - type-check "~0.3.2" - wordwrap "~1.0.0" - -os-tmpdir@~1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" - integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= - -p-limit@^1.1.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" - integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== - dependencies: - p-try "^1.0.0" - -p-limit@^2.0.0: - version "2.2.1" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.2.1.tgz#aa07a788cc3151c939b5131f63570f0dd2009537" - integrity sha512-85Tk+90UCVWvbDavCLKPOLC9vvY8OwEX/RtKF+/1OADJMVlFfEHOiMTPVyxg7mk/dKa+ipdHm0OUkTvCpMTuwg== - dependencies: - p-try "^2.0.0" - -p-locate@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" - integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= - dependencies: - p-limit "^1.1.0" - -p-locate@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-3.0.0.tgz#322d69a05c0264b25997d9f40cd8a891ab0064a4" - integrity sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ== - dependencies: - p-limit "^2.0.0" - -p-try@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" - integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= - -p-try@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" - integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== - -parent-module@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" - integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== - dependencies: - callsites "^3.0.0" - -parse-json@^2.2.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-2.2.0.tgz#f480f40434ef80741f8469099f8dea18f55a4dc9" - integrity sha1-9ID0BDTvgHQfhGkJn43qGPVaTck= - dependencies: - error-ex "^1.2.0" - -path-exists@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" - integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= - -path-is-absolute@^1.0.0: - version "1.0.1" - resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" - integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= - -path-key@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" - integrity sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A= - -path-parse@^1.0.6: - version "1.0.6" - resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.6.tgz#d62dbb5679405d72c4737ec58600e9ddcf06d24c" - integrity sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw== - -path-type@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" - integrity sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM= - dependencies: - pify "^2.0.0" - -pify@^2.0.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" - integrity sha1-7RQaasBDqEnqWISY59yosVMw6Qw= - -pkg-dir@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/pkg-dir/-/pkg-dir-2.0.0.tgz#f6d5d1109e19d63edf428e0bd57e12777615334b" - integrity sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s= - dependencies: - find-up "^2.1.0" - -prelude-ls@~1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" - integrity sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ= - -prettier@^1.18.2: - version "1.18.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.18.2.tgz#6823e7c5900017b4bd3acf46fe9ac4b4d7bda9ea" - integrity sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw== - -progress@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/progress/-/progress-2.0.3.tgz#7e8cf8d8f5b8f239c1bc68beb4eb78567d572ef8" - integrity sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA== - -promise-polyfill@^6.0.1: - version "6.1.0" - resolved "https://registry.yarnpkg.com/promise-polyfill/-/promise-polyfill-6.1.0.tgz#dfa96943ea9c121fca4de9b5868cb39d3472e057" - integrity sha1-36lpQ+qcEh/KTem1hoyznTRy4Fc= - -pseudomap@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3" - integrity sha1-8FKijacOYYkX7wqKw0wa5aaChrM= - -punycode@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" - integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== - -read-pkg-up@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" - integrity sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4= - dependencies: - find-up "^2.0.0" - read-pkg "^2.0.0" - -read-pkg@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" - integrity sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg= - dependencies: - load-json-file "^2.0.0" - normalize-package-data "^2.3.2" - path-type "^2.0.0" - -regexpp@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-2.0.1.tgz#8d19d31cf632482b589049f8281f93dbcba4d07f" - integrity sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw== - -replace-in-file@^4.1.3: - version "4.1.3" - resolved "https://registry.yarnpkg.com/replace-in-file/-/replace-in-file-4.1.3.tgz#0692332304c2b535e2323fffb39a1dfe59a8aac6" - integrity sha512-x8uSfKKK/5YiJ8JYsNMwa1PJYvHfPdUABVXd21ro09Nh5BRZ5ATuACwrqCNpktwVqVbFTk/cIGFepeMqY0oX3g== - dependencies: - chalk "^2.4.2" - glob "^7.1.4" - yargs "^13.3.0" - -require-directory@^2.1.1: - version "2.1.1" - resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" - integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= - -require-main-filename@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" - integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg== - -resolve-from@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" - integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== - -resolve@^1.10.0, resolve@^1.11.0, resolve@^1.5.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.12.0.tgz#3fc644a35c84a48554609ff26ec52b66fa577df6" - integrity sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w== - dependencies: - path-parse "^1.0.6" - -restore-cursor@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-2.0.0.tgz#9f7ee287f82fd326d4fd162923d62129eee0dfaf" - integrity sha1-n37ih/gv0ybU/RYpI9YhKe7g368= - dependencies: - onetime "^2.0.0" - signal-exit "^3.0.2" - -rimraf@2.6.3: - version "2.6.3" - resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.3.tgz#b2d104fe0d8fb27cf9e0a1cda8262dd3833c6cab" - integrity sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA== - dependencies: - glob "^7.1.3" - -run-async@^2.2.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.3.0.tgz#0371ab4ae0bdd720d4166d7dfda64ff7a445a6c0" - integrity sha1-A3GrSuC91yDUFm19/aZP96RFpsA= - dependencies: - is-promise "^2.1.0" - -rxjs@^6.4.0: - version "6.5.3" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.5.3.tgz#510e26317f4db91a7eb1de77d9dd9ba0a4899a3a" - integrity sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA== - dependencies: - tslib "^1.9.0" - -"safer-buffer@>= 2.1.2 < 3": - version "2.1.2" - resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" - integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== - -"semver@2 || 3 || 4 || 5", semver@^5.5.0: - version "5.7.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" - integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== - -semver@^6.1.2, semver@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" - integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== - -set-blocking@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" - integrity sha1-BF+XgtARrppoA93TgrJDkrPYkPc= - -shebang-command@^1.2.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea" - integrity sha1-RKrGW2lbAzmJaMOfNj/uXer98eo= - dependencies: - shebang-regex "^1.0.0" - -shebang-regex@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" - integrity sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM= - -signal-exit@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d" - integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0= - -slice-ansi@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-2.1.0.tgz#cacd7693461a637a5788d92a7dd4fba068e81636" - integrity sha512-Qu+VC3EwYLldKa1fCxuuvULvSJOKEgk9pi8dZeCVK7TqBfUNTH4sFkk4joj8afVSfAYgJoSOetjx9QWOJ5mYoQ== - dependencies: - ansi-styles "^3.2.0" - astral-regex "^1.0.0" - is-fullwidth-code-point "^2.0.0" - -source-map-support@^0.5.6: - version "0.5.13" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" - integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== - dependencies: - buffer-from "^1.0.0" - source-map "^0.6.0" - -source-map@^0.6.0: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== - -spdx-correct@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" - integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== - dependencies: - spdx-expression-parse "^3.0.0" - spdx-license-ids "^3.0.0" - -spdx-exceptions@^2.1.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.2.0.tgz#2ea450aee74f2a89bfb94519c07fcd6f41322977" - integrity sha512-2XQACfElKi9SlVb1CYadKDXvoajPgBVPn/gOQLrTvHdElaVhr7ZEbqJaRnJLVNeaI4cMEAgVCeBMKF6MWRDCRA== - -spdx-expression-parse@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" - integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== - dependencies: - spdx-exceptions "^2.1.0" - spdx-license-ids "^3.0.0" - -spdx-license-ids@^3.0.0: - version "3.0.5" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.5.tgz#3694b5804567a458d3c8045842a6358632f62654" - integrity sha512-J+FWzZoynJEXGphVIS+XEh3kFSjZX/1i9gFBaWQcB+/tmpe2qUsSBABpcxqxnAxFdiUFEgAX1bjYGQvIZmoz9Q== - -sprintf-js@~1.0.2: - version "1.0.3" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" - integrity sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw= - -string-width@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" - integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== - dependencies: - is-fullwidth-code-point "^2.0.0" - strip-ansi "^4.0.0" - -string-width@^3.0.0, string-width@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-3.1.0.tgz#22767be21b62af1081574306f69ac51b62203961" - integrity sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w== - dependencies: - emoji-regex "^7.0.1" - is-fullwidth-code-point "^2.0.0" - strip-ansi "^5.1.0" - -string.prototype.trimleft@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.1.0.tgz#6cc47f0d7eb8d62b0f3701611715a3954591d634" - integrity sha512-FJ6b7EgdKxxbDxc79cOlok6Afd++TTs5szo+zJTUyow3ycrRfJVE2pq3vcN53XexvKZu/DJMDfeI/qMiZTrjTw== - dependencies: - define-properties "^1.1.3" - function-bind "^1.1.1" - -string.prototype.trimright@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/string.prototype.trimright/-/string.prototype.trimright-2.1.0.tgz#669d164be9df9b6f7559fa8e89945b168a5a6c58" - integrity sha512-fXZTSV55dNBwv16uw+hh5jkghxSnc5oHq+5K/gXgizHwAvMetdAJlHqqoFC1FSDVPYWLkAKl2cxpUT41sV7nSg== - dependencies: - define-properties "^1.1.3" - function-bind "^1.1.1" - -strip-ansi@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-4.0.0.tgz#a8479022eb1ac368a871389b635262c505ee368f" - integrity sha1-qEeQIusaw2iocTibY1JixQXuNo8= - dependencies: - ansi-regex "^3.0.0" - -strip-ansi@^5.0.0, strip-ansi@^5.1.0, strip-ansi@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-5.2.0.tgz#8c9a536feb6afc962bdfa5b104a5091c1ad9c0ae" - integrity sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA== - dependencies: - ansi-regex "^4.1.0" - -strip-bom@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" - integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= - -strip-json-comments@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.0.1.tgz#85713975a91fb87bf1b305cca77395e40d2a64a7" - integrity sha512-VTyMAUfdm047mwKl+u79WIdrZxtFtn+nBxHeb844XBQ9uMNTuTHdx2hc5RiAJYqwTj3wc/xe5HLSdJSkJ+WfZw== - -supports-color@^5.3.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" - integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== - dependencies: - has-flag "^3.0.0" - -table@^5.2.3: - version "5.4.6" - resolved "https://registry.yarnpkg.com/table/-/table-5.4.6.tgz#1292d19500ce3f86053b05f0e8e7e4a3bb21079e" - integrity sha512-wmEc8m4fjnob4gt5riFRtTu/6+4rSe12TpAELNSqHMfF3IqnA+CH37USM6/YR3qRZv7e56kAEAtd6nKZaxe0Ug== - dependencies: - ajv "^6.10.2" - lodash "^4.17.14" - slice-ansi "^2.1.0" - string-width "^3.0.0" - -text-table@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" - integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= - -through@^2.3.6: - version "2.3.8" - resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" - integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= - -tmp@^0.0.33: - version "0.0.33" - resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" - integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== - dependencies: - os-tmpdir "~1.0.2" - -ts-node@^8.4.1: - version "8.4.1" - resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-8.4.1.tgz#270b0dba16e8723c9fa4f9b4775d3810fd994b4f" - integrity sha512-5LpRN+mTiCs7lI5EtbXmF/HfMeCjzt7DH9CZwtkr6SywStrNQC723wG+aOWFiLNn7zT3kD/RnFqi3ZUfr4l5Qw== - dependencies: - arg "^4.1.0" - diff "^4.0.1" - make-error "^1.1.1" - source-map-support "^0.5.6" - yn "^3.0.0" - -tslib@^1.8.1, tslib@^1.9.0: - version "1.10.0" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" - integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== - -tsutils@^3.17.1: - version "3.17.1" - resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.17.1.tgz#ed719917f11ca0dee586272b2ac49e015a2dd759" - integrity sha512-kzeQ5B8H3w60nFY2g8cJIuH7JDpsALXySGtwGJ0p2LSjLgay3NdIpqq5SoOBe46bKDW2iq25irHCr8wjomUS2g== - dependencies: - tslib "^1.8.1" - -type-check@~0.3.2: - version "0.3.2" - resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.3.2.tgz#5884cab512cf1d355e3fb784f30804b2b520db72" - integrity sha1-WITKtRLPHTVeP7eE8wgEsrUg23I= - dependencies: - prelude-ls "~1.1.2" - -typescript@^3.6.3: - version "3.6.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.6.3.tgz#fea942fabb20f7e1ca7164ff626f1a9f3f70b4da" - integrity sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw== - -uri-js@^4.2.2: - version "4.2.2" - resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0" - integrity sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ== - dependencies: - punycode "^2.1.0" - -v8-compile-cache@^2.0.3: - version "2.1.0" - resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.0.tgz#e14de37b31a6d194f5690d67efc4e7f6fc6ab30e" - integrity sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g== - -validate-npm-package-license@^3.0.1: - version "3.0.4" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" - integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== - dependencies: - spdx-correct "^3.0.0" - spdx-expression-parse "^3.0.0" - -which-module@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" - integrity sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho= - -which@^1.2.9: - version "1.3.1" - resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" - integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== - dependencies: - isexe "^2.0.0" - -wordwrap@~1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" - integrity sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus= - -wrap-ansi@^5.1.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" - integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== - dependencies: - ansi-styles "^3.2.0" - string-width "^3.0.0" - strip-ansi "^5.0.0" - -wrappy@1: - version "1.0.2" - resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" - integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= - -write@1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/write/-/write-1.0.3.tgz#0800e14523b923a387e415123c865616aae0f5c3" - integrity sha512-/lg70HAjtkUgWPVZhZcm+T4hkL8Zbtp1nFNOn3lRrxnlv50SRBv7cR7RqR+GMsd3hUXy9hWBo4CHTbFTcOYwig== - dependencies: - mkdirp "^0.5.1" - -y18n@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" - integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== - -yallist@^2.1.2: - version "2.1.2" - resolved "https://registry.yarnpkg.com/yallist/-/yallist-2.1.2.tgz#1c11f9218f076089a47dd512f93c6699a6a81d52" - integrity sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI= - -yargs-parser@^13.1.1: - version "13.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.1.tgz#d26058532aa06d365fe091f6a1fc06b2f7e5eca0" - integrity sha512-oVAVsHz6uFrg3XQheFII8ESO2ssAf9luWuAd6Wexsu4F3OtIW0o8IribPXYrD4WC24LWtPrJlGy87y5udK+dxQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - -yargs@^13.3.0: - version "13.3.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.0.tgz#4c657a55e07e5f2cf947f8a366567c04a0dedc83" - integrity sha512-2eehun/8ALW8TLoIl7MVaRUrg+yCnenu8B4kBlRxj3GJGDKU1Og7sMXPNm1BYyM1DOJmTZ4YeN/Nwxv+8XJsUA== - dependencies: - cliui "^5.0.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^13.1.1" - -yn@^3.0.0: - version "3.1.1" - resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" - integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== From 2d4ba4bcfd1d2dae9097cbcb36108f0e5ec4f56c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 9 Oct 2019 17:16:34 -0400 Subject: [PATCH 187/519] Ignore coverage for assert.go --- ci/test.mk | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ci/test.mk b/ci/test.mk index 2d72eadd..6d075136 100644 --- a/ci/test.mk +++ b/ci/test.mk @@ -14,8 +14,9 @@ codecov: _gotest _gotest: echo "--- gotest" && go test -parallel=32 -coverprofile=ci/out/coverage.prof -coverpkg=./... ./... sed -i '/_stringer.go/d' ci/out/coverage.prof - sed -i '/wsjstest/d' ci/out/coverage.prof - sed -i '/wsecho/d' ci/out/coverage.prof + sed -i '/wsjstest\/main.go/d' ci/out/coverage.prof + sed -i '/wsecho.go/d' ci/out/coverage.prof + sed -i '/assert.go/d' ci/out/coverage.prof gotest-wasm: wsjstest echo "--- wsjstest" && ./ci/wasmtest.sh From ba0fd40f3c8e6e2f30352f358de1cd98b679180d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 9 Oct 2019 20:31:00 -0400 Subject: [PATCH 188/519] Cleanup wasm test --- ci/test.mk | 8 ++--- ci/wasmtest.sh | 13 ++++++-- conn_common.go | 5 ++-- conn_test.go | 6 +--- internal/wsjstest/main.go | 62 +++++++-------------------------------- 5 files changed, 29 insertions(+), 65 deletions(-) diff --git a/ci/test.mk b/ci/test.mk index 6d075136..0fe0ce19 100644 --- a/ci/test.mk +++ b/ci/test.mk @@ -13,10 +13,10 @@ codecov: _gotest _gotest: echo "--- gotest" && go test -parallel=32 -coverprofile=ci/out/coverage.prof -coverpkg=./... ./... - sed -i '/_stringer.go/d' ci/out/coverage.prof - sed -i '/wsjstest\/main.go/d' ci/out/coverage.prof - sed -i '/wsecho.go/d' ci/out/coverage.prof - sed -i '/assert.go/d' ci/out/coverage.prof + sed -i '/_stringer\.go/d' ci/out/coverage.prof + sed -i '/wsjstest\/main\.go/d' ci/out/coverage.prof + sed -i '/wsecho\.go/d' ci/out/coverage.prof + sed -i '/assert\.go/d' ci/out/coverage.prof gotest-wasm: wsjstest echo "--- wsjstest" && ./ci/wasmtest.sh diff --git a/ci/wasmtest.sh b/ci/wasmtest.sh index 21335ce7..66d397a2 100755 --- a/ci/wasmtest.sh +++ b/ci/wasmtest.sh @@ -2,8 +2,17 @@ set -euo pipefail -WS_ECHO_SERVER_URL="$(wsjstest)" -trap 'pkill -KILL wsjstest' EXIT INT +wsjstestOut="$(mktemp -d)/wsjstestOut" +mkfifo "$wsjstestOut" +timeout 15s wsjstest > "$wsjstestOut" & +wsjstestPID="$!" + +WS_ECHO_SERVER_URL="$(head -n 1 "$wsjstestOut")" export WS_ECHO_SERVER_URL GOOS=js GOARCH=wasm go test -exec=wasmbrowsertest ./... + +if ! wait "$wsjstestPID" ; then + echo "wsjstest exited unsuccessfully" + exit 1 +fi diff --git a/conn_common.go b/conn_common.go index 5a11a79c..9f0b045a 100644 --- a/conn_common.go +++ b/conn_common.go @@ -5,7 +5,6 @@ package websocket import ( "context" - "errors" "fmt" "io" "math" @@ -104,8 +103,8 @@ func (c *netConn) Read(p []byte) (int, error) { if c.reader == nil { typ, r, err := c.c.Reader(c.readContext) if err != nil { - var ce CloseError - if errors.As(err, &ce) && (ce.Code == StatusNormalClosure) || (ce.Code == StatusGoingAway) { + switch CloseStatus(err) { + case StatusNormalClosure, StatusGoingAway: c.eofed = true return 0, io.EOF } diff --git a/conn_test.go b/conn_test.go index f3f1a2b0..8dcff944 100644 --- a/conn_test.go +++ b/conn_test.go @@ -589,11 +589,7 @@ func TestConn(t *testing.T) { return err } _, _, err = c.Read(ctx) - var cerr websocket.CloseError - if !errors.As(err, &cerr) || cerr.Code != websocket.StatusProtocolError { - return fmt.Errorf("expected close error with StatusProtocolError: %+v", err) - } - return nil + return assertCloseStatus(err, websocket.StatusProtocolError) }, client: func(ctx context.Context, c *websocket.Conn) error { _, _, err := c.Read(ctx) diff --git a/internal/wsjstest/main.go b/internal/wsjstest/main.go index 5251fb84..b8b1cba2 100644 --- a/internal/wsjstest/main.go +++ b/internal/wsjstest/main.go @@ -3,63 +3,20 @@ package main import ( - "errors" "fmt" "log" - "net" "net/http" + "net/http/httptest" "os" - "os/exec" + "runtime" + "strings" "nhooyr.io/websocket" "nhooyr.io/websocket/internal/wsecho" ) -func fork() net.Listener { - if os.Getenv("FORKED") != "" { - f := os.NewFile(3, "listener") - l, err := net.FileListener(f) - if err != nil { - log.Fatalf("failed to create listener from fd: %+v", err) - } - return l - } - - l, err := net.Listen("tcp", "localhost:0") - if err != nil { - log.Fatalf("failed to listen: %+v", err) - } - f, err := l.(*net.TCPListener).File() - if err != nil { - log.Fatalf("failed to get file from tcp listener: %+v", err) - } - - cmd := exec.Command(os.Args[0]) - cmd.Stderr = os.Stderr - cmd.Env = append(os.Environ(), - fmt.Sprintf("FORKED=true"), - ) - cmd.ExtraFiles = append(cmd.ExtraFiles, f) - err = cmd.Start() - if err != nil { - log.Fatalf("failed to start command: %+v", err) - } - - fmt.Printf("ws://%v\n", l.Addr().String()) - os.Exit(0) - - panic("unreachable") -} - func main() { - l := fork() - - err := serve(l) - log.Fatalf("failed to serve: %+v", err) -} - -func serve(l net.Listener) error { - return http.Serve(l, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ Subprotocols: []string{"echo"}, InsecureSkipVerify: true, @@ -70,11 +27,14 @@ func serve(l net.Listener) error { defer c.Close(websocket.StatusInternalError, "") err = wsecho.Loop(r.Context(), c) - - var ce websocket.CloseError - if !errors.As(err, &ce) || ce.Code != websocket.StatusNormalClosure { - log.Fatalf("unexpected loop error: %+v", err) + if websocket.CloseStatus(err) != websocket.StatusNormalClosure { + log.Fatalf("unexpected echo loop error: %+v", err) } + + os.Exit(0) })) + wsURL := strings.Replace(s.URL, "http", "ws", 1) + fmt.Printf("%v\n", wsURL) + runtime.Goexit() } From 773d2f293e795f37563e40b68e3a4e5901cf8421 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 9 Oct 2019 20:34:43 -0400 Subject: [PATCH 189/519] Fix racey wasm test --- .github/workflows/ci.yml | 6 +++--- ci/image/Dockerfile | 2 +- ci/wasmtest.sh | 2 +- websocket_js.go | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0f896ef2..37282e1b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,19 +4,19 @@ on: [push] jobs: fmt: runs-on: ubuntu-latest - container: nhooyr/websocket-ci@sha256:54a12b52be98d2d1588a054cfce35d17705c9b45546478d8bcdbac2a89a90001 + container: nhooyr/websocket-ci@sha256:ea94e078d2d589d654a2c759d952bf4199c754d80dadb20696dc3902359027cb steps: - uses: actions/checkout@v1 - run: make fmt lint: runs-on: ubuntu-latest - container: nhooyr/websocket-ci@sha256:54a12b52be98d2d1588a054cfce35d17705c9b45546478d8bcdbac2a89a90001 + container: nhooyr/websocket-ci@sha256:ea94e078d2d589d654a2c759d952bf4199c754d80dadb20696dc3902359027cb steps: - uses: actions/checkout@v1 - run: make lint test: runs-on: ubuntu-latest - container: nhooyr/websocket-ci@sha256:54a12b52be98d2d1588a054cfce35d17705c9b45546478d8bcdbac2a89a90001 + container: nhooyr/websocket-ci@sha256:ea94e078d2d589d654a2c759d952bf4199c754d80dadb20696dc3902359027cb steps: - uses: actions/checkout@v1 - run: make test diff --git a/ci/image/Dockerfile b/ci/image/Dockerfile index 51fdb69b..7fd5544a 100644 --- a/ci/image/Dockerfile +++ b/ci/image/Dockerfile @@ -10,7 +10,7 @@ ENV PATH=$GOPATH/bin:$PATH ENV GOFLAGS="-mod=readonly" ENV PAGER=cat ENV CI=true -ENV MAKEFLAGS="-j --output-sync target" +ENV MAKEFLAGS="--jobs=8 --output-sync=target" COPY ./ci/image/gitignore /root/.config/git/ignore RUN git config --system color.ui always diff --git a/ci/wasmtest.sh b/ci/wasmtest.sh index 66d397a2..586efec2 100755 --- a/ci/wasmtest.sh +++ b/ci/wasmtest.sh @@ -4,7 +4,7 @@ set -euo pipefail wsjstestOut="$(mktemp -d)/wsjstestOut" mkfifo "$wsjstestOut" -timeout 15s wsjstest > "$wsjstestOut" & +timeout 45s wsjstest > "$wsjstestOut" & wsjstestPID="$!" WS_ECHO_SERVER_URL="$(head -n 1 "$wsjstestOut")" diff --git a/websocket_js.go b/websocket_js.go index 33403498..f297f9d4 100644 --- a/websocket_js.go +++ b/websocket_js.go @@ -242,7 +242,7 @@ type DialOptions struct { func Dial(ctx context.Context, url string, opts *DialOptions) (*Conn, *http.Response, error) { c, resp, err := dial(ctx, url, opts) if err != nil { - return nil, resp, fmt.Errorf("failed to websocket dial: %w", err) + return nil, resp, fmt.Errorf("failed to websocket dial %q: %w", url, err) } return c, resp, nil } From 50179241fe3edeeae757c609a64429cc4c0abd14 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 10 Oct 2019 11:55:26 -0400 Subject: [PATCH 190/519] Cleanup CloseStatus/CloseError docs and improve wasm test script --- README.md | 2 +- ci/test.mk | 3 ++- ci/wasmtest.sh | 4 +-- conn.go | 18 ++++++++++--- conn_test.go | 25 ++++--------------- doc.go | 2 +- example_test.go | 2 +- internal/wsgrace/wsgrace.go | 50 +++++++++++++++++++++++++++++++++++++ internal/wsjstest/main.go | 21 +++++++++++++--- 9 files changed, 94 insertions(+), 33 deletions(-) create mode 100644 internal/wsgrace/wsgrace.go diff --git a/README.md b/README.md index e7fea3aa..9dd5d0a8 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ For a production quality example that shows off the full API, see the [echo exam Use the [errors.As](https://golang.org/pkg/errors/#As) function [new in Go 1.13](https://golang.org/doc/go1.13#error_wrapping) to check for [websocket.CloseError](https://godoc.org/nhooyr.io/websocket#CloseError). There is also [websocket.CloseStatus](https://godoc.org/nhooyr.io/websocket#CloseStatus) to quickly grab the close status code out of a [websocket.CloseError](https://godoc.org/nhooyr.io/websocket#CloseError). -See the [CloseError godoc example](https://godoc.org/nhooyr.io/websocket#example-CloseError). +See the [CloseStatus godoc example](https://godoc.org/nhooyr.io/websocket#example-CloseStatus). ### Server diff --git a/ci/test.mk b/ci/test.mk index 0fe0ce19..b86abb70 100644 --- a/ci/test.mk +++ b/ci/test.mk @@ -12,11 +12,12 @@ codecov: _gotest curl -s https://codecov.io/bash | bash -s -- -Z -f ci/out/coverage.prof _gotest: - echo "--- gotest" && go test -parallel=32 -coverprofile=ci/out/coverage.prof -coverpkg=./... ./... + echo "--- gotest" && go test -parallel=32 -coverprofile=ci/out/coverage.prof -coverpkg=./... $$TESTFLAGS ./... sed -i '/_stringer\.go/d' ci/out/coverage.prof sed -i '/wsjstest\/main\.go/d' ci/out/coverage.prof sed -i '/wsecho\.go/d' ci/out/coverage.prof sed -i '/assert\.go/d' ci/out/coverage.prof + sed -i '/wsgrace\.go/d' ci/out/coverage.prof gotest-wasm: wsjstest echo "--- wsjstest" && ./ci/wasmtest.sh diff --git a/ci/wasmtest.sh b/ci/wasmtest.sh index 586efec2..f285fdf4 100755 --- a/ci/wasmtest.sh +++ b/ci/wasmtest.sh @@ -5,14 +5,14 @@ set -euo pipefail wsjstestOut="$(mktemp -d)/wsjstestOut" mkfifo "$wsjstestOut" timeout 45s wsjstest > "$wsjstestOut" & -wsjstestPID="$!" WS_ECHO_SERVER_URL="$(head -n 1 "$wsjstestOut")" export WS_ECHO_SERVER_URL GOOS=js GOARCH=wasm go test -exec=wasmbrowsertest ./... -if ! wait "$wsjstestPID" ; then +kill %% +if ! wait %% ; then echo "wsjstest exited unsuccessfully" exit 1 fi diff --git a/conn.go b/conn.go index b7b9360e..43a94397 100644 --- a/conn.go +++ b/conn.go @@ -175,9 +175,14 @@ func (c *Conn) timeoutLoop() { case <-readCtx.Done(): c.setCloseErr(fmt.Errorf("read timed out: %w", readCtx.Err())) - // Guaranteed to eventually close the connection since it will not try and read - // but only write. - go c.exportedClose(StatusPolicyViolation, "read timed out", false) + // Guaranteed to eventually close the connection since we can only ever send + // one close frame. + go func() { + c.exportedClose(StatusPolicyViolation, "read timed out", true) + // Ensure the connection closes, i.e if we already sent a close frame and timed out + // to read the peer's close frame. + c.close(nil) + }() readCtx = context.Background() case <-writeCtx.Done(): c.close(fmt.Errorf("write timed out: %w", writeCtx.Err())) @@ -339,6 +344,13 @@ func (c *Conn) handleControl(ctx context.Context, h header) error { err = fmt.Errorf("received close: %w", ce) c.writeClose(b, err, false) + + if ctx.Err() != nil { + // The above close probably has been returned by the peer in response + // to our read timing out so we have to return the read timed out error instead. + return fmt.Errorf("read timed out: %w", ctx.Err()) + } + return err default: panic(fmt.Sprintf("websocket: unexpected control opcode: %#v", h)) diff --git a/conn_test.go b/conn_test.go index 8dcff944..1acdf595 100644 --- a/conn_test.go +++ b/conn_test.go @@ -22,7 +22,6 @@ import ( "reflect" "strconv" "strings" - "sync/atomic" "testing" "time" @@ -34,6 +33,7 @@ import ( "nhooyr.io/websocket" "nhooyr.io/websocket/internal/assert" "nhooyr.io/websocket/internal/wsecho" + "nhooyr.io/websocket/internal/wsgrace" "nhooyr.io/websocket/wsjson" "nhooyr.io/websocket/wspb" ) @@ -927,16 +927,7 @@ func TestConn(t *testing.T) { } func testServer(tb testing.TB, fn func(w http.ResponseWriter, r *http.Request) error, tls bool) (s *httptest.Server, closeFn func()) { - var conns int64 h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - atomic.AddInt64(&conns, 1) - defer atomic.AddInt64(&conns, -1) - - ctx, cancel := context.WithTimeout(r.Context(), time.Minute) - defer cancel() - - r = r.WithContext(ctx) - err := fn(w, r) if err != nil { tb.Errorf("server failed: %+v", err) @@ -947,18 +938,12 @@ func testServer(tb testing.TB, fn func(w http.ResponseWriter, r *http.Request) e } else { s = httptest.NewServer(h) } + closeFn2 := wsgrace.Grace(s.Config) return s, func() { - s.Close() - - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - - for atomic.LoadInt64(&conns) > 0 { - if ctx.Err() != nil { - tb.Fatalf("waiting for server to come down timed out: %v", ctx.Err()) - } + err := closeFn2() + if err != nil { + tb.Fatal(err) } - } } diff --git a/doc.go b/doc.go index 1610eed1..b29d2cdd 100644 --- a/doc.go +++ b/doc.go @@ -17,7 +17,7 @@ // // Use the errors.As function new in Go 1.13 to check for websocket.CloseError. // Or use the CloseStatus function to grab the StatusCode out of a websocket.CloseError -// See the CloseError example. +// See the CloseStatus example. // // Wasm // diff --git a/example_test.go b/example_test.go index 1cb3d799..bc603aff 100644 --- a/example_test.go +++ b/example_test.go @@ -64,7 +64,7 @@ func ExampleDial() { // This example dials a server and then expects to be disconnected with status code // websocket.StatusNormalClosure. -func ExampleCloseError() { +func ExampleCloseStatus() { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() diff --git a/internal/wsgrace/wsgrace.go b/internal/wsgrace/wsgrace.go new file mode 100644 index 00000000..513af1fe --- /dev/null +++ b/internal/wsgrace/wsgrace.go @@ -0,0 +1,50 @@ +package wsgrace + +import ( + "context" + "fmt" + "net/http" + "sync/atomic" + "time" +) + +// Grace wraps s.Handler to gracefully shutdown WebSocket connections. +// The returned function must be used to close the server instead of s.Close. +func Grace(s *http.Server) (closeFn func() error) { + h := s.Handler + var conns int64 + s.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + atomic.AddInt64(&conns, 1) + defer atomic.AddInt64(&conns, -1) + + ctx, cancel := context.WithTimeout(r.Context(), time.Minute) + defer cancel() + + r = r.WithContext(ctx) + + h.ServeHTTP(w, r) + }) + + return func() error { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + err := s.Shutdown(ctx) + if err != nil { + return fmt.Errorf("server shutdown failed: %v", err) + } + + t := time.NewTicker(time.Millisecond * 10) + defer t.Stop() + for { + select { + case <-t.C: + if atomic.LoadInt64(&conns) == 0 { + return nil + } + case <-ctx.Done(): + return fmt.Errorf("failed to wait for WebSocket connections: %v", ctx.Err()) + } + } + } +} diff --git a/internal/wsjstest/main.go b/internal/wsjstest/main.go index b8b1cba2..96eee2c0 100644 --- a/internal/wsjstest/main.go +++ b/internal/wsjstest/main.go @@ -8,14 +8,18 @@ import ( "net/http" "net/http/httptest" "os" - "runtime" + "os/signal" "strings" + "syscall" "nhooyr.io/websocket" "nhooyr.io/websocket/internal/wsecho" + "nhooyr.io/websocket/internal/wsgrace" ) func main() { + log.SetPrefix("wsecho") + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ Subprotocols: []string{"echo"}, @@ -30,11 +34,20 @@ func main() { if websocket.CloseStatus(err) != websocket.StatusNormalClosure { log.Fatalf("unexpected echo loop error: %+v", err) } - - os.Exit(0) })) + closeFn := wsgrace.Grace(s.Config) + defer func() { + err := closeFn() + if err != nil { + log.Fatal(err) + } + }() wsURL := strings.Replace(s.URL, "http", "ws", 1) fmt.Printf("%v\n", wsURL) - runtime.Goexit() + + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGTERM) + + <-sigs } From 62ea6c125cf0aad58445612e2b898a811e35b65b Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 11 Oct 2019 16:05:40 -0400 Subject: [PATCH 191/519] Add FUNDING.yml --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..fb83c3a9 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: nhooyr From bc4fce01803c504367a6996b2bb66aee1eb5a143 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 11 Oct 2019 15:18:35 -0400 Subject: [PATCH 192/519] Improve Close handshake behaviour - For JS we ensure we indicate which size initiated the close first from our POV - For normal Go, concurrent closes block until the first one succeeds instead of returning early --- conn.go | 33 ++++++++++++++++++++++++++++----- conn_common.go | 9 +++++++++ conn_test.go | 6 +++++- websocket_js.go | 33 +++++++++++++++------------------ 4 files changed, 57 insertions(+), 24 deletions(-) diff --git a/conn.go b/conn.go index 43a94397..861b2390 100644 --- a/conn.go +++ b/conn.go @@ -851,6 +851,13 @@ func (c *Conn) realWriteFrame(ctx context.Context, h header, p []byte) (n int, e // complete. func (c *Conn) Close(code StatusCode, reason string) error { err := c.exportedClose(code, reason, true) + var ec errClosing + if errors.As(err, &ec) { + <-c.closed + // We wait until the connection closes. + // We use writeClose and not exportedClose to avoid a second failed to marshal close frame error. + err = c.writeClose(nil, ec.ce, true) + } if err != nil { return fmt.Errorf("failed to close websocket connection: %w", err) } @@ -878,15 +885,31 @@ func (c *Conn) exportedClose(code StatusCode, reason string, handshake bool) err return c.writeClose(p, fmt.Errorf("sent close: %w", ce), handshake) } +type errClosing struct { + ce error +} + +func (e errClosing) Error() string { + return "already closing connection" +} + func (c *Conn) writeClose(p []byte, ce error, handshake bool) error { - select { - case <-c.closed: - return fmt.Errorf("tried to close with %v but connection already closed: %w", ce, c.closeErr) - default: + if c.isClosed() { + return fmt.Errorf("tried to close with %q but connection already closed: %w", ce, c.closeErr) } if !c.closing.CAS(0, 1) { - return fmt.Errorf("another goroutine is closing") + // Normally, we would want to wait until the connection is closed, + // at least for when a user calls into Close, so we handle that case in + // the exported Close function. + // + // But for internal library usage, we always want to return early, e.g. + // if we are performing a close handshake and the peer sends their close frame, + // we do not want to block here waiting for c.closed to close because it won't, + // at least not until we return since the gorouine that will close it is this one. + return errClosing{ + ce: ce, + } } // No matter what happens next, close error should be set. diff --git a/conn_common.go b/conn_common.go index 9f0b045a..1247df6e 100644 --- a/conn_common.go +++ b/conn_common.go @@ -234,3 +234,12 @@ func (v *atomicInt64) Increment(delta int64) int64 { func (v *atomicInt64) CAS(old, new int64) (swapped bool) { return atomic.CompareAndSwapInt64(&v.v, old, new) } + +func (c *Conn) isClosed() bool { + select { + case <-c.closed: + return true + default: + return false + } +} diff --git a/conn_test.go b/conn_test.go index 1acdf595..8413c4c2 100644 --- a/conn_test.go +++ b/conn_test.go @@ -602,7 +602,11 @@ func TestConn(t *testing.T) { { name: "largeControlFrame", server: func(ctx context.Context, c *websocket.Conn) error { - _, err := c.WriteFrame(ctx, true, websocket.OpClose, []byte(strings.Repeat("x", 4096))) + err := c.WriteHeader(ctx, websocket.Header{ + Fin: true, + OpCode: websocket.OpClose, + PayloadLength: 4096, + }) if err != nil { return err } diff --git a/websocket_js.go b/websocket_js.go index f297f9d4..d27809cf 100644 --- a/websocket_js.go +++ b/websocket_js.go @@ -23,7 +23,7 @@ type Conn struct { // read limit for a message in bytes. msgReadLimit *atomicInt64 - closeMu sync.Mutex + closingMu sync.Mutex isReadClosed *atomicInt64 closeOnce sync.Once closed chan struct{} @@ -43,6 +43,9 @@ func (c *Conn) close(err error, wasClean bool) { c.closeOnce.Do(func() { runtime.SetFinalizer(c, nil) + if !wasClean { + err = fmt.Errorf("unclean connection close: %w", err) + } c.setCloseErr(err) c.closeWasClean = wasClean close(c.closed) @@ -59,14 +62,11 @@ func (c *Conn) init() { c.isReadClosed = &atomicInt64{} c.releaseOnClose = c.ws.OnClose(func(e wsjs.CloseEvent) { - var err error = CloseError{ + err := CloseError{ Code: StatusCode(e.Code), Reason: e.Reason, } - if !e.WasClean { - err = fmt.Errorf("connection close was not clean: %w", err) - } - c.close(err, e.WasClean) + c.close(fmt.Errorf("received close: %w", err), e.WasClean) c.releaseOnClose() c.releaseOnMessage() @@ -182,15 +182,6 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) error { } } -func (c *Conn) isClosed() bool { - select { - case <-c.closed: - return true - default: - return false - } -} - // Close closes the websocket with the given code and reason. // It will wait until the peer responds with a close frame // or the connection is closed. @@ -204,13 +195,19 @@ func (c *Conn) Close(code StatusCode, reason string) error { } func (c *Conn) exportedClose(code StatusCode, reason string) error { - c.closeMu.Lock() - defer c.closeMu.Unlock() + c.closingMu.Lock() + defer c.closingMu.Unlock() + + ce := fmt.Errorf("sent close: %w", CloseError{ + Code: code, + Reason: reason, + }) if c.isClosed() { - return fmt.Errorf("already closed: %w", c.closeErr) + return fmt.Errorf("tried to close with %q but connection already closed: %w", ce, c.closeErr) } + c.setCloseErr(ce) err := c.ws.Close(int(code), reason) if err != nil { return err From d85d44f2ebe1adb292950e2f0220c6d82ccab8cc Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 13 Oct 2019 18:56:09 -0400 Subject: [PATCH 193/519] Remove separate script for wasm tests --- ci/test.mk | 11 ++------ ci/wasmtest.sh | 18 ------------- conn_test.go | 35 ++++++++++++++++++++++++++ internal/wsjstest/main.go | 53 --------------------------------------- 4 files changed, 37 insertions(+), 80 deletions(-) delete mode 100755 ci/wasmtest.sh delete mode 100644 internal/wsjstest/main.go diff --git a/ci/test.mk b/ci/test.mk index b86abb70..8d46c94a 100644 --- a/ci/test.mk +++ b/ci/test.mk @@ -1,4 +1,4 @@ -test: gotest gotest-wasm +test: gotest gotest: _gotest htmlcov ifdef CI @@ -12,15 +12,8 @@ codecov: _gotest curl -s https://codecov.io/bash | bash -s -- -Z -f ci/out/coverage.prof _gotest: - echo "--- gotest" && go test -parallel=32 -coverprofile=ci/out/coverage.prof -coverpkg=./... $$TESTFLAGS ./... + go test -parallel=32 -coverprofile=ci/out/coverage.prof -coverpkg=./... $$TESTFLAGS ./... sed -i '/_stringer\.go/d' ci/out/coverage.prof - sed -i '/wsjstest\/main\.go/d' ci/out/coverage.prof sed -i '/wsecho\.go/d' ci/out/coverage.prof sed -i '/assert\.go/d' ci/out/coverage.prof sed -i '/wsgrace\.go/d' ci/out/coverage.prof - -gotest-wasm: wsjstest - echo "--- wsjstest" && ./ci/wasmtest.sh - -wsjstest: - go install ./internal/wsjstest diff --git a/ci/wasmtest.sh b/ci/wasmtest.sh deleted file mode 100755 index f285fdf4..00000000 --- a/ci/wasmtest.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -wsjstestOut="$(mktemp -d)/wsjstestOut" -mkfifo "$wsjstestOut" -timeout 45s wsjstest > "$wsjstestOut" & - -WS_ECHO_SERVER_URL="$(head -n 1 "$wsjstestOut")" -export WS_ECHO_SERVER_URL - -GOOS=js GOARCH=wasm go test -exec=wasmbrowsertest ./... - -kill %% -if ! wait %% ; then - echo "wsjstest exited unsuccessfully" - exit 1 -fi diff --git a/conn_test.go b/conn_test.go index 8413c4c2..4c7d1390 100644 --- a/conn_test.go +++ b/conn_test.go @@ -2337,3 +2337,38 @@ func checkWSTestIndex(t *testing.T, path string) { } } } + +func TestWASM(t *testing.T) { + t.Parallel() + + s, closeFn := testServer(t, func(w http.ResponseWriter, r *http.Request) error { + c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + Subprotocols: []string{"echo"}, + InsecureSkipVerify: true, + }) + if err != nil { + return err + } + defer c.Close(websocket.StatusInternalError, "") + + err = wsecho.Loop(r.Context(), c) + if websocket.CloseStatus(err) != websocket.StatusNormalClosure { + return err + } + return nil + }, false) + defer closeFn() + + wsURL := strings.Replace(s.URL, "http", "ws", 1) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) + defer cancel() + + cmd := exec.CommandContext(ctx, "go", "test", "-exec=wasmbrowsertest", "./...") + cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm", fmt.Sprintf("WS_ECHO_SERVER_URL=%v", wsURL)) + + b, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("wasm test binary failed: %v:\n%s", err, b) + } +} diff --git a/internal/wsjstest/main.go b/internal/wsjstest/main.go deleted file mode 100644 index 96eee2c0..00000000 --- a/internal/wsjstest/main.go +++ /dev/null @@ -1,53 +0,0 @@ -// +build !js - -package main - -import ( - "fmt" - "log" - "net/http" - "net/http/httptest" - "os" - "os/signal" - "strings" - "syscall" - - "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/wsecho" - "nhooyr.io/websocket/internal/wsgrace" -) - -func main() { - log.SetPrefix("wsecho") - - s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ - Subprotocols: []string{"echo"}, - InsecureSkipVerify: true, - }) - if err != nil { - log.Fatalf("echo server: failed to accept: %+v", err) - } - defer c.Close(websocket.StatusInternalError, "") - - err = wsecho.Loop(r.Context(), c) - if websocket.CloseStatus(err) != websocket.StatusNormalClosure { - log.Fatalf("unexpected echo loop error: %+v", err) - } - })) - closeFn := wsgrace.Grace(s.Config) - defer func() { - err := closeFn() - if err != nil { - log.Fatal(err) - } - }() - - wsURL := strings.Replace(s.URL, "http", "ws", 1) - fmt.Printf("%v\n", wsURL) - - sigs := make(chan os.Signal, 1) - signal.Notify(sigs, syscall.SIGTERM) - - <-sigs -} From 0d98321619709ca8a66f975a2c7978727d5196a8 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 13 Oct 2019 19:10:51 -0400 Subject: [PATCH 194/519] Fix typo in fmt.mk --- ci/fmt.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/fmt.mk b/ci/fmt.mk index 631a894d..16a3b24e 100644 --- a/ci/fmt.mk +++ b/ci/fmt.mk @@ -1,4 +1,4 @@ -fmt: modtidy gofmt goimports prettier +fmt: modtidy gofmt goimports prettier shfmt ifdef CI ./ci/fmtcheck.sh endif From 2d35d72b6827ef62563911d6b70251ac914fe576 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 14 Oct 2019 13:42:19 -0400 Subject: [PATCH 195/519] Switch to coveralls.io from codecov.io codecov's uploader keeps failing due to the API being unresponsive and the UI is cluttered compared to codecov. --- .github/CONTRIBUTING.md | 7 ++----- .github/workflows/ci.yml | 2 +- Makefile | 4 ++++ README.md | 4 ++-- ci/.codecov.yml | 10 ---------- ci/fmt.mk | 17 ++++++++++------- ci/fmtcheck.sh | 11 ----------- ci/image/Dockerfile | 4 +--- ci/lint.mk | 5 +---- ci/test.mk | 17 ++++++++--------- 10 files changed, 29 insertions(+), 52 deletions(-) delete mode 100644 ci/.codecov.yml delete mode 100755 ci/fmtcheck.sh diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index cd6af4f1..357c314a 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -28,7 +28,7 @@ CI must pass on your changes for them to be merged. ### CI CI will ensure your code is formatted, lints and passes tests. -It will collect coverage and report it to [codecov](https://codecov.io/gh/nhooyr/websocket) +It will collect coverage and report it to [coveralls](https://coveralls.io/github/nhooyr/websocket) and also upload a html `coverage` artifact that you can download to browse coverage. You can run CI locally. @@ -42,7 +42,4 @@ See [ci/image/Dockerfile](../ci/image/Dockerfile) for the installation of the CI For coverage details locally, see `ci/out/coverage.html` after running `make test`. -You can also run tests normally with `go test`. `make test` just passes a default set of flags to -`go test` to collect coverage and runs the WASM tests. - -Coverage percentage from codecov and the CI scripts will be different because they are calculated differently. +You can run tests normally with `go test`. `make test` wraps around `go test` to collect coverage. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37282e1b..d13e9028 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: - uses: actions/checkout@v1 - run: make test env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + COVERALLS_TOKEN: ${{ secrets.COVERALLS_TOKEN }} - name: Upload coverage.html uses: actions/upload-artifact@master with: diff --git a/Makefile b/Makefile index ce92ab5b..62633e39 100644 --- a/Makefile +++ b/Makefile @@ -4,6 +4,10 @@ all: fmt lint test .PHONY: * +.ONESHELL: +SHELL = bash +.SHELLFLAGS = -ceuo pipefail + include ci/fmt.mk include ci/lint.mk include ci/test.mk diff --git a/README.md b/README.md index 9dd5d0a8..4a425a16 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # websocket -[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/nhooyr/websocket?color=6b9ded&sort=semver)](https://github.com/nhooyr/websocket/releases) +[![GitHub Release](https://img.shields.io/github/v/release/nhooyr/websocket?color=6b9ded&sort=semver)](https://github.com/nhooyr/websocket/releases) [![GoDoc](https://godoc.org/nhooyr.io/websocket?status.svg)](https://godoc.org/nhooyr.io/websocket) -[![Codecov](https://img.shields.io/codecov/c/github/nhooyr/websocket.svg?color=65d6a4)](https://codecov.io/gh/nhooyr/websocket) +[![Codecov](https://img.shields.io/coveralls/github/nhooyr/websocket?color=65d6a4)](https://coveralls.io/github/nhooyr/websocket) [![Actions Status](https://github.com/nhooyr/websocket/workflows/ci/badge.svg)](https://github.com/nhooyr/websocket/actions) websocket is a minimal and idiomatic WebSocket library for Go. diff --git a/ci/.codecov.yml b/ci/.codecov.yml deleted file mode 100644 index fa7c5f0a..00000000 --- a/ci/.codecov.yml +++ /dev/null @@ -1,10 +0,0 @@ -comment: off -coverage: - status: - # Prevent small changes in coverage from failing CI. - project: - default: - threshold: 15% - patch: - default: - threshold: 100% diff --git a/ci/fmt.mk b/ci/fmt.mk index 16a3b24e..8e61bc24 100644 --- a/ci/fmt.mk +++ b/ci/fmt.mk @@ -1,6 +1,12 @@ -fmt: modtidy gofmt goimports prettier shfmt +fmt: modtidy gofmt goimports prettier ifdef CI - ./ci/fmtcheck.sh + if [[ $$(git ls-files --other --modified --exclude-standard) != "" ]]; then + echo "Files need generation or are formatted incorrectly:" + git -c color.ui=always status | grep --color=no '\e\[31m' + echo "Please run the following locally:" + echo " make fmt" + exit 1 + fi endif modtidy: gen @@ -12,11 +18,8 @@ gofmt: gen goimports: gen goimports -w "-local=$$(go list -m)" . -prettier: gen - prettier --write --print-width=120 --no-semi --trailing-comma=all --loglevel=warn $$(git ls-files "*.yaml" "*.yml" "*.md" "*.ts") - -shfmt: gen - shfmt -i 2 -w -s -sr . +prettier: + prettier --write --print-width=120 --no-semi --trailing-comma=all --loglevel=warn $$(git ls-files "*.yml" "*.md") gen: go generate ./... diff --git a/ci/fmtcheck.sh b/ci/fmtcheck.sh deleted file mode 100755 index 6e452a38..00000000 --- a/ci/fmtcheck.sh +++ /dev/null @@ -1,11 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -if [[ $(git ls-files --other --modified --exclude-standard) != "" ]]; then - echo "Files need generation or are formatted incorrectly." - git status - echo "Please run the following locally:" - echo " make fmt" - exit 1 -fi diff --git a/ci/image/Dockerfile b/ci/image/Dockerfile index 7fd5544a..183c9e43 100644 --- a/ci/image/Dockerfile +++ b/ci/image/Dockerfile @@ -3,7 +3,6 @@ FROM golang:1 RUN apt-get update RUN apt-get install -y chromium RUN apt-get install -y npm -RUN apt-get install -y shellcheck ENV GOPATH=/root/gopath ENV PATH=$GOPATH/bin:$PATH @@ -13,14 +12,13 @@ ENV CI=true ENV MAKEFLAGS="--jobs=8 --output-sync=target" COPY ./ci/image/gitignore /root/.config/git/ignore -RUN git config --system color.ui always RUN npm install -g prettier RUN go get golang.org/x/tools/cmd/stringer RUN go get golang.org/x/tools/cmd/goimports -RUN go get mvdan.cc/sh/cmd/shfmt RUN go get golang.org/x/lint/golint RUN go get github.com/agnivade/wasmbrowsertest +RUN go get github.com/mattn/goveralls # Cache go modules and build cache. COPY . /tmp/websocket diff --git a/ci/lint.mk b/ci/lint.mk index f68add41..a656ea8d 100644 --- a/ci/lint.mk +++ b/ci/lint.mk @@ -1,4 +1,4 @@ -lint: govet golint govet-wasm golint-wasm shellcheck +lint: govet golint govet-wasm golint-wasm govet: go vet ./... @@ -11,6 +11,3 @@ golint: golint-wasm: GOOS=js GOARCH=wasm golint -set_exit_status ./... - -shellcheck: - shellcheck -x $$(git ls-files "*.sh") diff --git a/ci/test.mk b/ci/test.mk index 8d46c94a..cebada77 100644 --- a/ci/test.mk +++ b/ci/test.mk @@ -1,18 +1,17 @@ -test: gotest - -gotest: _gotest htmlcov +test: gotest ci/out/coverage.html ifdef CI -gotest: codecov +test: coveralls endif -htmlcov: _gotest +ci/out/coverage.html: gotest go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html -codecov: _gotest - curl -s https://codecov.io/bash | bash -s -- -Z -f ci/out/coverage.prof +coveralls: gotest + echo "--- coveralls" + goveralls -coverprofile=ci/out/coverage.prof -service=github-actions -_gotest: - go test -parallel=32 -coverprofile=ci/out/coverage.prof -coverpkg=./... $$TESTFLAGS ./... +gotest: + go test -parallel=32 -covermode=count -coverprofile=ci/out/coverage.prof -coverpkg=./... $${TESTFLAGS-} ./... sed -i '/_stringer\.go/d' ci/out/coverage.prof sed -i '/wsecho\.go/d' ci/out/coverage.prof sed -i '/assert\.go/d' ci/out/coverage.prof From dbae76980cdf24bfa4bf1c969c1d81062fbad701 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 14 Oct 2019 13:52:50 -0400 Subject: [PATCH 196/519] Simplify autobahn tests No need to test both client and server side. --- ci/test.mk | 2 +- conn_test.go | 9 +-------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/ci/test.mk b/ci/test.mk index cebada77..a249ea45 100644 --- a/ci/test.mk +++ b/ci/test.mk @@ -11,7 +11,7 @@ coveralls: gotest goveralls -coverprofile=ci/out/coverage.prof -service=github-actions gotest: - go test -parallel=32 -covermode=count -coverprofile=ci/out/coverage.prof -coverpkg=./... $${TESTFLAGS-} ./... + go test -covermode=count -coverprofile=ci/out/coverage.prof -coverpkg=./... $${GOTESTFLAGS-} ./... sed -i '/_stringer\.go/d' ci/out/coverage.prof sed -i '/wsecho\.go/d' ci/out/coverage.prof sed -i '/assert\.go/d' ci/out/coverage.prof diff --git a/conn_test.go b/conn_test.go index 4c7d1390..d924fd0a 100644 --- a/conn_test.go +++ b/conn_test.go @@ -1023,14 +1023,7 @@ func TestAutobahn(t *testing.T) { t.Run(name, func(t *testing.T) { t.Parallel() - t.Run("server", func(t *testing.T) { - t.Parallel() - run2(t, false) - }) - t.Run("client", func(t *testing.T) { - t.Parallel() - run2(t, true) - }) + run2(t, true) }) } From b0ac42c3873c0099373d8cc8d1e0711e98db994b Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 14 Oct 2019 16:10:25 -0400 Subject: [PATCH 197/519] Cleanup CI image --- .github/workflows/ci.yml | 6 +++--- Makefile | 2 +- README.md | 2 +- ci/{image => }/Dockerfile | 2 -- ci/image/gitignore | 5 ----- ci/test.mk | 2 ++ 6 files changed, 7 insertions(+), 12 deletions(-) rename ci/{image => }/Dockerfile (94%) delete mode 100644 ci/image/gitignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d13e9028..6154df4c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,19 +4,19 @@ on: [push] jobs: fmt: runs-on: ubuntu-latest - container: nhooyr/websocket-ci@sha256:ea94e078d2d589d654a2c759d952bf4199c754d80dadb20696dc3902359027cb + container: nhooyr/websocket-ci@sha256:046cd02e79dcbf81dc06eb6fd333fa8643f2503f437ddd46a4c1af9132078a2c steps: - uses: actions/checkout@v1 - run: make fmt lint: runs-on: ubuntu-latest - container: nhooyr/websocket-ci@sha256:ea94e078d2d589d654a2c759d952bf4199c754d80dadb20696dc3902359027cb + container: nhooyr/websocket-ci@sha256:046cd02e79dcbf81dc06eb6fd333fa8643f2503f437ddd46a4c1af9132078a2c steps: - uses: actions/checkout@v1 - run: make lint test: runs-on: ubuntu-latest - container: nhooyr/websocket-ci@sha256:ea94e078d2d589d654a2c759d952bf4199c754d80dadb20696dc3902359027cb + container: nhooyr/websocket-ci@sha256:046cd02e79dcbf81dc06eb6fd333fa8643f2503f437ddd46a4c1af9132078a2c steps: - uses: actions/checkout@v1 - run: make test diff --git a/Makefile b/Makefile index 62633e39..8c8e1a08 100644 --- a/Makefile +++ b/Makefile @@ -13,5 +13,5 @@ include ci/lint.mk include ci/test.mk ci-image: - docker build -f ./ci/image/Dockerfile -t nhooyr/websocket-ci . + docker build -f ./ci/Dockerfile -t nhooyr/websocket-ci . docker push nhooyr/websocket-ci diff --git a/README.md b/README.md index 4a425a16..8d873fdc 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![GitHub Release](https://img.shields.io/github/v/release/nhooyr/websocket?color=6b9ded&sort=semver)](https://github.com/nhooyr/websocket/releases) [![GoDoc](https://godoc.org/nhooyr.io/websocket?status.svg)](https://godoc.org/nhooyr.io/websocket) -[![Codecov](https://img.shields.io/coveralls/github/nhooyr/websocket?color=65d6a4)](https://coveralls.io/github/nhooyr/websocket) +[![Coveralls](https://img.shields.io/coveralls/github/nhooyr/websocket?color=65d6a4)](https://coveralls.io/github/nhooyr/websocket) [![Actions Status](https://github.com/nhooyr/websocket/workflows/ci/badge.svg)](https://github.com/nhooyr/websocket/actions) websocket is a minimal and idiomatic WebSocket library for Go. diff --git a/ci/image/Dockerfile b/ci/Dockerfile similarity index 94% rename from ci/image/Dockerfile rename to ci/Dockerfile index 183c9e43..b9068fa1 100644 --- a/ci/image/Dockerfile +++ b/ci/Dockerfile @@ -11,8 +11,6 @@ ENV PAGER=cat ENV CI=true ENV MAKEFLAGS="--jobs=8 --output-sync=target" -COPY ./ci/image/gitignore /root/.config/git/ignore - RUN npm install -g prettier RUN go get golang.org/x/tools/cmd/stringer RUN go get golang.org/x/tools/cmd/goimports diff --git a/ci/image/gitignore b/ci/image/gitignore deleted file mode 100644 index 3917f38e..00000000 --- a/ci/image/gitignore +++ /dev/null @@ -1,5 +0,0 @@ -node_modules -.DS_Store -.idea -.gitignore -.dockerignore diff --git a/ci/test.mk b/ci/test.mk index a249ea45..25609538 100644 --- a/ci/test.mk +++ b/ci/test.mk @@ -8,6 +8,8 @@ ci/out/coverage.html: gotest coveralls: gotest echo "--- coveralls" + export GIT_BRANCH=$${GITHUB_REF} + export BUILD_NUMBER=$${GITHUB_ACTION} goveralls -coverprofile=ci/out/coverage.prof -service=github-actions gotest: From 9b9d7b4aac087688af34698a3a0b45fc8d6303a4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 14 Oct 2019 17:18:48 -0400 Subject: [PATCH 198/519] Cleanup coveralls setup --- .github/workflows/ci.yml | 10 +++++----- ci/Dockerfile | 1 + ci/test.mk | 12 ++++++++---- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6154df4c..2cc69828 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,27 +1,27 @@ name: ci -on: [push] +on: [push, pull_request] jobs: fmt: runs-on: ubuntu-latest - container: nhooyr/websocket-ci@sha256:046cd02e79dcbf81dc06eb6fd333fa8643f2503f437ddd46a4c1af9132078a2c + container: nhooyr/websocket-ci@sha256:8a8fd73fdea33585d50a33619c4936adfd016246a2ed6bbfbf06def24b518a6a steps: - uses: actions/checkout@v1 - run: make fmt lint: runs-on: ubuntu-latest - container: nhooyr/websocket-ci@sha256:046cd02e79dcbf81dc06eb6fd333fa8643f2503f437ddd46a4c1af9132078a2c + container: nhooyr/websocket-ci@sha256:8a8fd73fdea33585d50a33619c4936adfd016246a2ed6bbfbf06def24b518a6a steps: - uses: actions/checkout@v1 - run: make lint test: runs-on: ubuntu-latest - container: nhooyr/websocket-ci@sha256:046cd02e79dcbf81dc06eb6fd333fa8643f2503f437ddd46a4c1af9132078a2c + container: nhooyr/websocket-ci@sha256:8a8fd73fdea33585d50a33619c4936adfd016246a2ed6bbfbf06def24b518a6a steps: - uses: actions/checkout@v1 - run: make test env: - COVERALLS_TOKEN: ${{ secrets.COVERALLS_TOKEN }} + COVERALLS_TOKEN: ${{ secrets.github_token }} - name: Upload coverage.html uses: actions/upload-artifact@master with: diff --git a/ci/Dockerfile b/ci/Dockerfile index b9068fa1..0f0fc7d9 100644 --- a/ci/Dockerfile +++ b/ci/Dockerfile @@ -3,6 +3,7 @@ FROM golang:1 RUN apt-get update RUN apt-get install -y chromium RUN apt-get install -y npm +RUN apt-get install -y jq ENV GOPATH=/root/gopath ENV PATH=$GOPATH/bin:$PATH diff --git a/ci/test.mk b/ci/test.mk index 25609538..f34c2b7f 100644 --- a/ci/test.mk +++ b/ci/test.mk @@ -7,11 +7,15 @@ ci/out/coverage.html: gotest go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html coveralls: gotest + # https://github.com/coverallsapp/github-action/blob/master/src/run.ts echo "--- coveralls" - export GIT_BRANCH=$${GITHUB_REF} - export BUILD_NUMBER=$${GITHUB_ACTION} - goveralls -coverprofile=ci/out/coverage.prof -service=github-actions - + export GIT_BRANCH="$$GITHUB_REF" + export BUILD_NUMBER="$$GITHUB_SHA" + if [[ $$GITHUB_EVENT_NAME == pull_request ]]; then + export CI_PULL_REQUEST="$$(jq .number "$$GITHUB_EVENT_PATH")" + BUILD_NUMBER="$$BUILD_NUMBER-PR-$$CI_PULL_REQUEST" + fi + goveralls -coverprofile=ci/out/coverage.prof -service=github gotest: go test -covermode=count -coverprofile=ci/out/coverage.prof -coverpkg=./... $${GOTESTFLAGS-} ./... sed -i '/_stringer\.go/d' ci/out/coverage.prof From 50dd4262be9d5087b9573af5f6be79e82526132b Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 14 Oct 2019 18:34:29 -0400 Subject: [PATCH 199/519] Fix formatting --- ci/test.mk | 1 + 1 file changed, 1 insertion(+) diff --git a/ci/test.mk b/ci/test.mk index f34c2b7f..3183552e 100644 --- a/ci/test.mk +++ b/ci/test.mk @@ -16,6 +16,7 @@ coveralls: gotest BUILD_NUMBER="$$BUILD_NUMBER-PR-$$CI_PULL_REQUEST" fi goveralls -coverprofile=ci/out/coverage.prof -service=github + gotest: go test -covermode=count -coverprofile=ci/out/coverage.prof -coverpkg=./... $${GOTESTFLAGS-} ./... sed -i '/_stringer\.go/d' ci/out/coverage.prof From 4f014d23d63b60a6ed127590482938c65181405d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 23 Oct 2019 09:53:41 -0400 Subject: [PATCH 200/519] Fix concurrent read with close Closes #164 --- conn.go | 19 ++++++++++++++----- conn_test.go | 23 +++++++++++++++++++++++ 2 files changed, 37 insertions(+), 5 deletions(-) diff --git a/conn.go b/conn.go index 861b2390..cbb7fa56 100644 --- a/conn.go +++ b/conn.go @@ -42,11 +42,12 @@ type Conn struct { closer io.Closer client bool - closeOnce sync.Once - closeErrOnce sync.Once - closeErr error - closed chan struct{} - closing *atomicInt64 + closeOnce sync.Once + closeErrOnce sync.Once + closeErr error + closed chan struct{} + closing *atomicInt64 + closeReceived error // messageWriter state. // writeMsgLock is acquired to write a data message. @@ -339,10 +340,12 @@ func (c *Conn) handleControl(ctx context.Context, h header) error { if err != nil { err = fmt.Errorf("received invalid close payload: %w", err) c.exportedClose(StatusProtocolError, err.Error(), false) + c.closeReceived = err return err } err = fmt.Errorf("received close: %w", ce) + c.closeReceived = err c.writeClose(b, err, false) if ctx.Err() != nil { @@ -941,6 +944,12 @@ func (c *Conn) waitClose() error { return err } defer c.releaseLock(c.readLock) + + if c.closeReceived != nil { + // goroutine reading just received the close. + return c.closeReceived + } + c.readerShouldLock = false b := bpool.Get() diff --git a/conn_test.go b/conn_test.go index d924fd0a..83f09dbf 100644 --- a/conn_test.go +++ b/conn_test.go @@ -868,6 +868,29 @@ func TestConn(t *testing.T) { return c.Close(websocket.StatusNormalClosure, "") }, }, + { + // Issue #164 + name: "closeHandshake_concurrentRead", + server: func(ctx context.Context, c *websocket.Conn) error { + _, _, err := c.Read(ctx) + return assertCloseStatus(err, websocket.StatusNormalClosure) + }, + client: func(ctx context.Context, c *websocket.Conn) error { + errc := make(chan error, 1) + go func() { + _, _, err := c.Read(ctx) + errc <- err + }() + + err := c.Close(websocket.StatusNormalClosure, "") + if err != nil { + return err + } + + err = <-errc + return assertCloseStatus(err, websocket.StatusNormalClosure) + }, + }, } for _, tc := range testCases { tc := tc From e36318f9e092c573a64429321b1cd13e1e5a5d62 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 2 Nov 2019 11:17:48 -0400 Subject: [PATCH 201/519] Minor cleanup CODEOWNERS is unnecessary. --- .github/CODEOWNERS.txt | 1 - README.md | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) delete mode 100644 .github/CODEOWNERS.txt diff --git a/.github/CODEOWNERS.txt b/.github/CODEOWNERS.txt deleted file mode 100644 index d2eae33e..00000000 --- a/.github/CODEOWNERS.txt +++ /dev/null @@ -1 +0,0 @@ -* @nhooyr diff --git a/README.md b/README.md index 8d873fdc..25d35e88 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![GitHub Release](https://img.shields.io/github/v/release/nhooyr/websocket?color=6b9ded&sort=semver)](https://github.com/nhooyr/websocket/releases) [![GoDoc](https://godoc.org/nhooyr.io/websocket?status.svg)](https://godoc.org/nhooyr.io/websocket) [![Coveralls](https://img.shields.io/coveralls/github/nhooyr/websocket?color=65d6a4)](https://coveralls.io/github/nhooyr/websocket) -[![Actions Status](https://github.com/nhooyr/websocket/workflows/ci/badge.svg)](https://github.com/nhooyr/websocket/actions) +[![CI Status](https://github.com/nhooyr/websocket/workflows/ci/badge.svg)](https://github.com/nhooyr/websocket/actions) websocket is a minimal and idiomatic WebSocket library for Go. From 780bda4159cd001ed4e1704327c1292a1d21336d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 4 Nov 2019 18:50:29 -0500 Subject: [PATCH 202/519] Fix race with c.readerShouldLock Closes #168 --- conn.go | 53 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/conn.go b/conn.go index cbb7fa56..7d48b8aa 100644 --- a/conn.go +++ b/conn.go @@ -78,11 +78,10 @@ type Conn struct { readLock chan struct{} // messageReader state. - readerMsgCtx context.Context - readerMsgHeader header - readerFrameEOF bool - readerMaskPos int - readerShouldLock bool + readerMsgCtx context.Context + readerMsgHeader header + readerFrameEOF bool + readerMaskPos int setReadTimeout chan context.Context setWriteTimeout chan context.Context @@ -445,7 +444,6 @@ func (c *Conn) reader(ctx context.Context, lock bool) (MessageType, io.Reader, e c.readerFrameEOF = false c.readerMaskPos = 0 c.readMsgLeft = c.msgReadLimit.Load() - c.readerShouldLock = lock r := &messageReader{ c: c, @@ -465,7 +463,11 @@ func (r *messageReader) eof() bool { // Read reads as many bytes as possible into p. func (r *messageReader) Read(p []byte) (int, error) { - n, err := r.read(p) + return r.exportedRead(p, true) +} + +func (r *messageReader) exportedRead(p []byte, lock bool) (int, error) { + n, err := r.read(p, lock) if err != nil { // Have to return io.EOF directly for now, we cannot wrap as errors.Is // isn't used widely yet. @@ -477,17 +479,29 @@ func (r *messageReader) Read(p []byte) (int, error) { return n, nil } -func (r *messageReader) read(p []byte) (int, error) { - if r.c.readerShouldLock { - err := r.c.acquireLock(r.c.readerMsgCtx, r.c.readLock) - if err != nil { - return 0, err +func (r *messageReader) readUnlocked(p []byte) (int, error) { + return r.exportedRead(p, false) +} + +func (r *messageReader) read(p []byte, lock bool) (int, error) { + if lock { + // If we cannot acquire the read lock, then + // there is either a concurrent read or the close handshake + // is proceeding. + select { + case r.c.readLock <- struct{}{}: + defer r.c.releaseLock(r.c.readLock) + default: + if r.c.closing.Load() == 1 { + <-r.c.closed + return 0, r.c.closeErr + } + return 0, errors.New("concurrent read detected") } - defer r.c.releaseLock(r.c.readLock) } if r.eof() { - return 0, fmt.Errorf("cannot use EOFed reader") + return 0, errors.New("cannot use EOFed reader") } if r.c.readMsgLeft <= 0 { @@ -950,8 +964,6 @@ func (c *Conn) waitClose() error { return c.closeReceived } - c.readerShouldLock = false - b := bpool.Get() buf := b.Bytes() buf = buf[:cap(buf)] @@ -965,7 +977,8 @@ func (c *Conn) waitClose() error { } } - _, err = io.CopyBuffer(ioutil.Discard, c.activeReader, buf) + r := readerFunc(c.activeReader.readUnlocked) + _, err = io.CopyBuffer(ioutil.Discard, r, buf) if err != nil { return err } @@ -1019,6 +1032,12 @@ func (c *Conn) ping(ctx context.Context, p string) error { } } +type readerFunc func(p []byte) (int, error) + +func (f readerFunc) Read(p []byte) (int, error) { + return f(p) +} + type writerFunc func(p []byte) (int, error) func (f writerFunc) Write(p []byte) (int, error) { From 8b47056acf279f171189afe3ffd4e351f000132a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 4 Nov 2019 18:57:37 -0500 Subject: [PATCH 203/519] Pass through close errors --- conn.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/conn.go b/conn.go index 7d48b8aa..df3a4044 100644 --- a/conn.go +++ b/conn.go @@ -236,6 +236,10 @@ func (c *Conn) readTillMsg(ctx context.Context) (header, error) { if h.opcode.controlOp() { err = c.handleControl(ctx, h) if err != nil { + // Pass through CloseErrors when receiving a close frame. + if h.opcode == opClose && CloseStatus(err) != -1 { + return header{}, err + } return header{}, fmt.Errorf("failed to handle control frame %v: %w", h.opcode, err) } continue From 0fc34f98b0dba1d3595889d867c479534b52065c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 5 Nov 2019 10:13:25 -0500 Subject: [PATCH 204/519] Minor cleanup of README --- README.md | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 25d35e88..ea26237a 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,8 @@ go get nhooyr.io/websocket ## Roadmap -- [ ] WebSockets over HTTP/2 [#4](https://github.com/nhooyr/websocket/issues/4) +- [ ] Compression Extensions [#163](https://github.com/nhooyr/websocket/pull/163) +- [ ] HTTP/2 [#4](https://github.com/nhooyr/websocket/issues/4) ## Examples @@ -65,9 +66,6 @@ http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) { ### Client -The client side of this library requires at least Go 1.12 as it uses a [new feature -in net/http](https://github.com/golang/go/issues/26937#issuecomment-415855861) to perform WebSocket handshakes. - ```go ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() @@ -94,9 +92,6 @@ c.Close(websocket.StatusNormalClosure, "") - net.Conn is never exposed as WebSocket over HTTP/2 will not have a net.Conn. - Using net/http's Client for dialing means we do not have to reinvent dialing hooks and configurations like other WebSocket libraries -- We do not support the deflate compression extension because Go's compress/flate library - is very memory intensive and browsers do not handle WebSocket compression intelligently. - See [#5](https://github.com/nhooyr/websocket/issues/5) ## Comparison From 2f8f69cfed50f408ebca698e7bf0c202c10269b1 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 6 Nov 2019 09:49:39 -0500 Subject: [PATCH 205/519] Add more sizes to BenchmarkXOR --- frame_test.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frame_test.go b/frame_test.go index 8c5760e8..84742ff0 100644 --- a/frame_test.go +++ b/frame_test.go @@ -334,8 +334,12 @@ func basixXOR(maskKey [4]byte, pos int, b []byte) int { func BenchmarkXOR(b *testing.B) { sizes := []int{ 2, + 3, + 4, + 8, 16, 32, + 128, 512, 4096, 16384, From a13f5dc8e931f207ecdfc8897fc82b521f65023c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 5 Nov 2019 23:09:00 -0500 Subject: [PATCH 206/519] Optimize fastXOR with math/bits See https://github.com/golang/go/issues/31586#issuecomment-550040882 Thanks @renthraysk benchmark old MB/s new MB/s speedup BenchmarkXOR/2/fast-8 470.88 492.61 1.05x BenchmarkXOR/3/fast-8 602.24 719.25 1.19x BenchmarkXOR/4/fast-8 718.82 1186.64 1.65x BenchmarkXOR/8/fast-8 1027.60 1718.71 1.67x BenchmarkXOR/16/fast-8 1413.31 3430.46 2.43x BenchmarkXOR/32/fast-8 2701.81 5585.42 2.07x BenchmarkXOR/128/fast-8 7757.97 13432.37 1.73x BenchmarkXOR/512/fast-8 15155.03 18797.79 1.24x BenchmarkXOR/4096/fast-8 20689.95 20334.61 0.98x BenchmarkXOR/16384/fast-8 21687.87 21613.94 1.00x Now its faster than basic XOR at every byte size greater than 2 on little endian amd64 machines. --- conn.go | 17 +++---- conn_export_test.go | 2 +- frame.go | 111 ++++++++++++++++++++++---------------------- frame_test.go | 45 +++++++++++------- 4 files changed, 94 insertions(+), 81 deletions(-) diff --git a/conn.go b/conn.go index df3a4044..90a5a6a1 100644 --- a/conn.go +++ b/conn.go @@ -6,6 +6,7 @@ import ( "bufio" "context" "crypto/rand" + "encoding/binary" "errors" "fmt" "io" @@ -81,7 +82,7 @@ type Conn struct { readerMsgCtx context.Context readerMsgHeader header readerFrameEOF bool - readerMaskPos int + readerMaskKey uint32 setReadTimeout chan context.Context setWriteTimeout chan context.Context @@ -324,7 +325,7 @@ func (c *Conn) handleControl(ctx context.Context, h header) error { } if h.masked { - fastXOR(h.maskKey, 0, b) + fastXOR(h.maskKey, b) } switch h.opcode { @@ -445,8 +446,8 @@ func (c *Conn) reader(ctx context.Context, lock bool) (MessageType, io.Reader, e c.readerMsgCtx = ctx c.readerMsgHeader = h + c.readerMaskKey = h.maskKey c.readerFrameEOF = false - c.readerMaskPos = 0 c.readMsgLeft = c.msgReadLimit.Load() r := &messageReader{ @@ -532,7 +533,7 @@ func (r *messageReader) read(p []byte, lock bool) (int, error) { r.c.readerMsgHeader = h r.c.readerFrameEOF = false - r.c.readerMaskPos = 0 + r.c.readerMaskKey = h.maskKey } h := r.c.readerMsgHeader @@ -545,7 +546,7 @@ func (r *messageReader) read(p []byte, lock bool) (int, error) { h.payloadLength -= int64(n) r.c.readMsgLeft -= int64(n) if h.masked { - r.c.readerMaskPos = fastXOR(h.maskKey, r.c.readerMaskPos, p) + r.c.readerMaskKey = fastXOR(r.c.readerMaskKey, p) } r.c.readerMsgHeader = h @@ -761,7 +762,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, opcode opcode, p []byte c.writeHeader.payloadLength = int64(len(p)) if c.client { - _, err := io.ReadFull(rand.Reader, c.writeHeader.maskKey[:]) + err = binary.Read(rand.Reader, binary.BigEndian, &c.writeHeader.maskKey) if err != nil { return 0, fmt.Errorf("failed to generate masking key: %w", err) } @@ -809,7 +810,7 @@ func (c *Conn) realWriteFrame(ctx context.Context, h header, p []byte) (n int, e } if c.client { - var keypos int + maskKey := h.maskKey for len(p) > 0 { if c.bw.Available() == 0 { err = c.bw.Flush() @@ -831,7 +832,7 @@ func (c *Conn) realWriteFrame(ctx context.Context, h header, p []byte) (n int, e return n, err } - keypos = fastXOR(h.maskKey, keypos, c.writeBuf[i:i+n2]) + maskKey = fastXOR(maskKey, c.writeBuf[i:i+n2]) p = p[n2:] n += n2 diff --git a/conn_export_test.go b/conn_export_test.go index 94195a9c..9335381c 100644 --- a/conn_export_test.go +++ b/conn_export_test.go @@ -37,7 +37,7 @@ func (c *Conn) ReadFrame(ctx context.Context) (OpCode, []byte, error) { return 0, nil, err } if h.masked { - fastXOR(h.maskKey, 0, b) + fastXOR(h.maskKey, b) } return OpCode(h.opcode), b, nil } diff --git a/frame.go b/frame.go index be23330e..5345d516 100644 --- a/frame.go +++ b/frame.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "math" + "math/bits" ) //go:generate stringer -type=opcode,MessageType,StatusCode -output=frame_stringer.go @@ -69,7 +70,7 @@ type header struct { payloadLength int64 masked bool - maskKey [4]byte + maskKey uint32 } func makeWriteHeaderBuf() []byte { @@ -119,7 +120,7 @@ func writeHeader(b []byte, h header) []byte { if h.masked { b[1] |= 1 << 7 b = b[:len(b)+4] - copy(b[len(b)-4:], h.maskKey[:]) + binary.LittleEndian.PutUint32(b[len(b)-4:], h.maskKey) } return b @@ -192,7 +193,7 @@ func readHeader(b []byte, r io.Reader) (header, error) { } if h.masked { - copy(h.maskKey[:], b) + h.maskKey = binary.LittleEndian.Uint32(b) } return h, nil @@ -321,26 +322,18 @@ func (ce CloseError) bytes() ([]byte, error) { return buf, nil } -// xor applies the WebSocket masking algorithm to p -// with the given key where the first 3 bits of pos -// are the starting position in the key. +// fastXOR applies the WebSocket masking algorithm to p +// with the given key. // See https://tools.ietf.org/html/rfc6455#section-5.3 // -// The returned value is the position of the next byte -// to be used for masking in the key. This is so that -// unmasking can be performed without the entire frame. -func fastXOR(key [4]byte, keyPos int, b []byte) int { - // If the payload is greater than or equal to 16 bytes, then it's worth - // masking 8 bytes at a time. - // Optimization from https://github.com/golang/go/issues/31586#issuecomment-485530859 - if len(b) >= 16 { - // We first create a key that is 8 bytes long - // and is aligned on the position correctly. - var alignedKey [8]byte - for i := range alignedKey { - alignedKey[i] = key[(i+keyPos)&3] - } - k := binary.LittleEndian.Uint64(alignedKey[:]) +// The returned value is the correctly rotated key to +// to continue to mask/unmask the message. +// +// It is optimized for LittleEndian and expects the key +// to be in little endian. +func fastXOR(key uint32, b []byte) uint32 { + if len(b) >= 8 { + key64 := uint64(key)<<32 | uint64(key) // At some point in the future we can clean these unrolled loops up. // See https://github.com/golang/go/issues/31586#issuecomment-487436401 @@ -348,95 +341,103 @@ func fastXOR(key [4]byte, keyPos int, b []byte) int { // Then we xor until b is less than 128 bytes. for len(b) >= 128 { v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^k) + binary.LittleEndian.PutUint64(b, v^key64) v = binary.LittleEndian.Uint64(b[8:]) - binary.LittleEndian.PutUint64(b[8:], v^k) + binary.LittleEndian.PutUint64(b[8:], v^key64) v = binary.LittleEndian.Uint64(b[16:]) - binary.LittleEndian.PutUint64(b[16:], v^k) + binary.LittleEndian.PutUint64(b[16:], v^key64) v = binary.LittleEndian.Uint64(b[24:]) - binary.LittleEndian.PutUint64(b[24:], v^k) + binary.LittleEndian.PutUint64(b[24:], v^key64) v = binary.LittleEndian.Uint64(b[32:]) - binary.LittleEndian.PutUint64(b[32:], v^k) + binary.LittleEndian.PutUint64(b[32:], v^key64) v = binary.LittleEndian.Uint64(b[40:]) - binary.LittleEndian.PutUint64(b[40:], v^k) + binary.LittleEndian.PutUint64(b[40:], v^key64) v = binary.LittleEndian.Uint64(b[48:]) - binary.LittleEndian.PutUint64(b[48:], v^k) + binary.LittleEndian.PutUint64(b[48:], v^key64) v = binary.LittleEndian.Uint64(b[56:]) - binary.LittleEndian.PutUint64(b[56:], v^k) + binary.LittleEndian.PutUint64(b[56:], v^key64) v = binary.LittleEndian.Uint64(b[64:]) - binary.LittleEndian.PutUint64(b[64:], v^k) + binary.LittleEndian.PutUint64(b[64:], v^key64) v = binary.LittleEndian.Uint64(b[72:]) - binary.LittleEndian.PutUint64(b[72:], v^k) + binary.LittleEndian.PutUint64(b[72:], v^key64) v = binary.LittleEndian.Uint64(b[80:]) - binary.LittleEndian.PutUint64(b[80:], v^k) + binary.LittleEndian.PutUint64(b[80:], v^key64) v = binary.LittleEndian.Uint64(b[88:]) - binary.LittleEndian.PutUint64(b[88:], v^k) + binary.LittleEndian.PutUint64(b[88:], v^key64) v = binary.LittleEndian.Uint64(b[96:]) - binary.LittleEndian.PutUint64(b[96:], v^k) + binary.LittleEndian.PutUint64(b[96:], v^key64) v = binary.LittleEndian.Uint64(b[104:]) - binary.LittleEndian.PutUint64(b[104:], v^k) + binary.LittleEndian.PutUint64(b[104:], v^key64) v = binary.LittleEndian.Uint64(b[112:]) - binary.LittleEndian.PutUint64(b[112:], v^k) + binary.LittleEndian.PutUint64(b[112:], v^key64) v = binary.LittleEndian.Uint64(b[120:]) - binary.LittleEndian.PutUint64(b[120:], v^k) + binary.LittleEndian.PutUint64(b[120:], v^key64) b = b[128:] } // Then we xor until b is less than 64 bytes. for len(b) >= 64 { v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^k) + binary.LittleEndian.PutUint64(b, v^key64) v = binary.LittleEndian.Uint64(b[8:]) - binary.LittleEndian.PutUint64(b[8:], v^k) + binary.LittleEndian.PutUint64(b[8:], v^key64) v = binary.LittleEndian.Uint64(b[16:]) - binary.LittleEndian.PutUint64(b[16:], v^k) + binary.LittleEndian.PutUint64(b[16:], v^key64) v = binary.LittleEndian.Uint64(b[24:]) - binary.LittleEndian.PutUint64(b[24:], v^k) + binary.LittleEndian.PutUint64(b[24:], v^key64) v = binary.LittleEndian.Uint64(b[32:]) - binary.LittleEndian.PutUint64(b[32:], v^k) + binary.LittleEndian.PutUint64(b[32:], v^key64) v = binary.LittleEndian.Uint64(b[40:]) - binary.LittleEndian.PutUint64(b[40:], v^k) + binary.LittleEndian.PutUint64(b[40:], v^key64) v = binary.LittleEndian.Uint64(b[48:]) - binary.LittleEndian.PutUint64(b[48:], v^k) + binary.LittleEndian.PutUint64(b[48:], v^key64) v = binary.LittleEndian.Uint64(b[56:]) - binary.LittleEndian.PutUint64(b[56:], v^k) + binary.LittleEndian.PutUint64(b[56:], v^key64) b = b[64:] } // Then we xor until b is less than 32 bytes. for len(b) >= 32 { v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^k) + binary.LittleEndian.PutUint64(b, v^key64) v = binary.LittleEndian.Uint64(b[8:]) - binary.LittleEndian.PutUint64(b[8:], v^k) + binary.LittleEndian.PutUint64(b[8:], v^key64) v = binary.LittleEndian.Uint64(b[16:]) - binary.LittleEndian.PutUint64(b[16:], v^k) + binary.LittleEndian.PutUint64(b[16:], v^key64) v = binary.LittleEndian.Uint64(b[24:]) - binary.LittleEndian.PutUint64(b[24:], v^k) + binary.LittleEndian.PutUint64(b[24:], v^key64) b = b[32:] } // Then we xor until b is less than 16 bytes. for len(b) >= 16 { v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^k) + binary.LittleEndian.PutUint64(b, v^key64) v = binary.LittleEndian.Uint64(b[8:]) - binary.LittleEndian.PutUint64(b[8:], v^k) + binary.LittleEndian.PutUint64(b[8:], v^key64) b = b[16:] } // Then we xor until b is less than 8 bytes. for len(b) >= 8 { v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^k) + binary.LittleEndian.PutUint64(b, v^key64) b = b[8:] } } + // Then we xor until b is less than 4 bytes. + for len(b) >= 4 { + v := binary.LittleEndian.Uint32(b) + binary.LittleEndian.PutUint32(b, v^key) + b = b[4:] + } + // xor remaining bytes. for i := range b { - b[i] ^= key[keyPos&3] - keyPos++ + b[i] ^= byte(key) + key = bits.RotateLeft32(key, -8) } - return keyPos & 3 + + return key } diff --git a/frame_test.go b/frame_test.go index 84742ff0..c8f4cd8d 100644 --- a/frame_test.go +++ b/frame_test.go @@ -4,8 +4,10 @@ package websocket import ( "bytes" + "encoding/binary" "io" "math" + "math/bits" "math/rand" "strconv" "strings" @@ -133,7 +135,7 @@ func TestHeader(t *testing.T) { } if h.masked { - rand.Read(h.maskKey[:]) + h.maskKey = rand.Uint32() } testHeader(t, h) @@ -309,17 +311,17 @@ func Test_validWireCloseCode(t *testing.T) { func Test_xor(t *testing.T) { t.Parallel() - key := [4]byte{0xa, 0xb, 0xc, 0xff} + key := []byte{0xa, 0xb, 0xc, 0xff} + key32 := binary.LittleEndian.Uint32(key) p := []byte{0xa, 0xb, 0xc, 0xf2, 0xc} - pos := 0 - pos = fastXOR(key, pos, p) + gotKey32 := fastXOR(key32, p) if exp := []byte{0, 0, 0, 0x0d, 0x6}; !cmp.Equal(exp, p) { t.Fatalf("unexpected mask: %v", cmp.Diff(exp, p)) } - if exp := 1; !cmp.Equal(exp, pos) { - t.Fatalf("unexpected mask pos: %v", cmp.Diff(exp, pos)) + if exp := bits.RotateLeft32(key32, -8); !cmp.Equal(exp, gotKey32) { + t.Fatalf("unexpected mask key: %v", cmp.Diff(exp, gotKey32)) } } @@ -347,26 +349,37 @@ func BenchmarkXOR(b *testing.B) { fns := []struct { name string - fn func([4]byte, int, []byte) int + fn func(b *testing.B, key [4]byte, p []byte) }{ { - "basic", - basixXOR, + name: "basic", + fn: func(b *testing.B, key [4]byte, p []byte) { + for i := 0; i < b.N; i++ { + basixXOR(key, 0, p) + } + }, }, { - "fast", - fastXOR, + name: "fast", + fn: func(b *testing.B, key [4]byte, p []byte) { + key32 := binary.BigEndian.Uint32(key[:]) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + fastXOR(key32, p) + } + }, }, } - var maskKey [4]byte - _, err := rand.Read(maskKey[:]) + var key [4]byte + _, err := rand.Read(key[:]) if err != nil { b.Fatalf("failed to populate mask key: %v", err) } for _, size := range sizes { - data := make([]byte, size) + p := make([]byte, size) b.Run(strconv.Itoa(size), func(b *testing.B) { for _, fn := range fns { @@ -374,9 +387,7 @@ func BenchmarkXOR(b *testing.B) { b.ReportAllocs() b.SetBytes(int64(size)) - for i := 0; i < b.N; i++ { - fn.fn(maskKey, 0, data) - } + fn.fn(b, key, p) }) } }) From 3b6e614dc3b11e915bf1d77c20e87bb374839fbb Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 6 Nov 2019 10:02:41 -0500 Subject: [PATCH 207/519] Rename xor to mask --- conn.go | 10 +++++----- conn_export_test.go | 2 +- frame.go | 6 ++++-- frame_test.go | 15 +++++++-------- 4 files changed, 17 insertions(+), 16 deletions(-) diff --git a/conn.go b/conn.go index 90a5a6a1..26906c79 100644 --- a/conn.go +++ b/conn.go @@ -325,7 +325,7 @@ func (c *Conn) handleControl(ctx context.Context, h header) error { } if h.masked { - fastXOR(h.maskKey, b) + mask(h.maskKey, b) } switch h.opcode { @@ -446,8 +446,8 @@ func (c *Conn) reader(ctx context.Context, lock bool) (MessageType, io.Reader, e c.readerMsgCtx = ctx c.readerMsgHeader = h - c.readerMaskKey = h.maskKey c.readerFrameEOF = false + c.readerMaskKey = h.maskKey c.readMsgLeft = c.msgReadLimit.Load() r := &messageReader{ @@ -546,7 +546,7 @@ func (r *messageReader) read(p []byte, lock bool) (int, error) { h.payloadLength -= int64(n) r.c.readMsgLeft -= int64(n) if h.masked { - r.c.readerMaskKey = fastXOR(r.c.readerMaskKey, p) + r.c.readerMaskKey = mask(r.c.readerMaskKey, p) } r.c.readerMsgHeader = h @@ -762,7 +762,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, opcode opcode, p []byte c.writeHeader.payloadLength = int64(len(p)) if c.client { - err = binary.Read(rand.Reader, binary.BigEndian, &c.writeHeader.maskKey) + err = binary.Read(rand.Reader, binary.LittleEndian, &c.writeHeader.maskKey) if err != nil { return 0, fmt.Errorf("failed to generate masking key: %w", err) } @@ -832,7 +832,7 @@ func (c *Conn) realWriteFrame(ctx context.Context, h header, p []byte) (n int, e return n, err } - maskKey = fastXOR(maskKey, c.writeBuf[i:i+n2]) + maskKey = mask(maskKey, c.writeBuf[i:i+n2]) p = p[n2:] n += n2 diff --git a/conn_export_test.go b/conn_export_test.go index 9335381c..d5f5aa24 100644 --- a/conn_export_test.go +++ b/conn_export_test.go @@ -37,7 +37,7 @@ func (c *Conn) ReadFrame(ctx context.Context) (OpCode, []byte, error) { return 0, nil, err } if h.masked { - fastXOR(h.maskKey, b) + mask(h.maskKey, b) } return OpCode(h.opcode), b, nil } diff --git a/frame.go b/frame.go index 5345d516..a823d281 100644 --- a/frame.go +++ b/frame.go @@ -322,7 +322,7 @@ func (ce CloseError) bytes() ([]byte, error) { return buf, nil } -// fastXOR applies the WebSocket masking algorithm to p +// fastMask applies the WebSocket masking algorithm to p // with the given key. // See https://tools.ietf.org/html/rfc6455#section-5.3 // @@ -331,7 +331,9 @@ func (ce CloseError) bytes() ([]byte, error) { // // It is optimized for LittleEndian and expects the key // to be in little endian. -func fastXOR(key uint32, b []byte) uint32 { +// +// See https://github.com/golang/go/issues/31586 +func mask(key uint32, b []byte) uint32 { if len(b) >= 8 { key64 := uint64(key)<<32 | uint64(key) diff --git a/frame_test.go b/frame_test.go index c8f4cd8d..2f3d4df2 100644 --- a/frame_test.go +++ b/frame_test.go @@ -308,13 +308,13 @@ func Test_validWireCloseCode(t *testing.T) { } } -func Test_xor(t *testing.T) { +func Test_mask(t *testing.T) { t.Parallel() key := []byte{0xa, 0xb, 0xc, 0xff} key32 := binary.LittleEndian.Uint32(key) p := []byte{0xa, 0xb, 0xc, 0xf2, 0xc} - gotKey32 := fastXOR(key32, p) + gotKey32 := mask(key32, p) if exp := []byte{0, 0, 0, 0x0d, 0x6}; !cmp.Equal(exp, p) { t.Fatalf("unexpected mask: %v", cmp.Diff(exp, p)) @@ -325,7 +325,7 @@ func Test_xor(t *testing.T) { } } -func basixXOR(maskKey [4]byte, pos int, b []byte) int { +func basixMask(maskKey [4]byte, pos int, b []byte) int { for i := range b { b[i] ^= maskKey[pos&3] pos++ @@ -333,7 +333,7 @@ func basixXOR(maskKey [4]byte, pos int, b []byte) int { return pos & 3 } -func BenchmarkXOR(b *testing.B) { +func Benchmark_mask(b *testing.B) { sizes := []int{ 2, 3, @@ -355,18 +355,18 @@ func BenchmarkXOR(b *testing.B) { name: "basic", fn: func(b *testing.B, key [4]byte, p []byte) { for i := 0; i < b.N; i++ { - basixXOR(key, 0, p) + basixMask(key, 0, p) } }, }, { name: "fast", fn: func(b *testing.B, key [4]byte, p []byte) { - key32 := binary.BigEndian.Uint32(key[:]) + key32 := binary.LittleEndian.Uint32(key[:]) b.ResetTimer() for i := 0; i < b.N; i++ { - fastXOR(key32, p) + mask(key32, p) } }, }, @@ -384,7 +384,6 @@ func BenchmarkXOR(b *testing.B) { b.Run(strconv.Itoa(size), func(b *testing.B) { for _, fn := range fns { b.Run(fn.name, func(b *testing.B) { - b.ReportAllocs() b.SetBytes(int64(size)) fn.fn(b, key, p) From 15d0a18fa2042fb0bb8735374c7efe041e90c014 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 6 Nov 2019 21:02:56 -0500 Subject: [PATCH 208/519] Elminate BCE in mask algorithm Thanks again to @renthraysk This provides another significant speedup. benchmark old MB/s new MB/s speedup Benchmark_mask/2/fast-8 405.48 513.25 1.27x Benchmark_mask/3/fast-8 518.93 661.92 1.28x Benchmark_mask/4/fast-8 1207.10 1252.39 1.04x Benchmark_mask/8/fast-8 1708.82 1655.63 0.97x Benchmark_mask/16/fast-8 3418.58 3051.25 0.89x Benchmark_mask/32/fast-8 5789.43 5813.31 1.00x Benchmark_mask/128/fast-8 12819.53 14804.50 1.15x Benchmark_mask/512/fast-8 18247.06 21659.50 1.19x Benchmark_mask/4096/fast-8 19802.31 23885.68 1.21x Benchmark_mask/16384/fast-8 20896.97 25081.11 1.20x --- frame.go | 104 +++++++++++++++++++++++++++---------------------------- 1 file changed, 52 insertions(+), 52 deletions(-) diff --git a/frame.go b/frame.go index a823d281..e4bf931a 100644 --- a/frame.go +++ b/frame.go @@ -344,36 +344,36 @@ func mask(key uint32, b []byte) uint32 { for len(b) >= 128 { v := binary.LittleEndian.Uint64(b) binary.LittleEndian.PutUint64(b, v^key64) - v = binary.LittleEndian.Uint64(b[8:]) - binary.LittleEndian.PutUint64(b[8:], v^key64) - v = binary.LittleEndian.Uint64(b[16:]) - binary.LittleEndian.PutUint64(b[16:], v^key64) - v = binary.LittleEndian.Uint64(b[24:]) - binary.LittleEndian.PutUint64(b[24:], v^key64) - v = binary.LittleEndian.Uint64(b[32:]) - binary.LittleEndian.PutUint64(b[32:], v^key64) - v = binary.LittleEndian.Uint64(b[40:]) - binary.LittleEndian.PutUint64(b[40:], v^key64) - v = binary.LittleEndian.Uint64(b[48:]) - binary.LittleEndian.PutUint64(b[48:], v^key64) - v = binary.LittleEndian.Uint64(b[56:]) - binary.LittleEndian.PutUint64(b[56:], v^key64) - v = binary.LittleEndian.Uint64(b[64:]) - binary.LittleEndian.PutUint64(b[64:], v^key64) - v = binary.LittleEndian.Uint64(b[72:]) - binary.LittleEndian.PutUint64(b[72:], v^key64) - v = binary.LittleEndian.Uint64(b[80:]) - binary.LittleEndian.PutUint64(b[80:], v^key64) - v = binary.LittleEndian.Uint64(b[88:]) - binary.LittleEndian.PutUint64(b[88:], v^key64) - v = binary.LittleEndian.Uint64(b[96:]) - binary.LittleEndian.PutUint64(b[96:], v^key64) - v = binary.LittleEndian.Uint64(b[104:]) - binary.LittleEndian.PutUint64(b[104:], v^key64) - v = binary.LittleEndian.Uint64(b[112:]) - binary.LittleEndian.PutUint64(b[112:], v^key64) - v = binary.LittleEndian.Uint64(b[120:]) - binary.LittleEndian.PutUint64(b[120:], v^key64) + v = binary.LittleEndian.Uint64(b[8:16]) + binary.LittleEndian.PutUint64(b[8:16], v^key64) + v = binary.LittleEndian.Uint64(b[16:24]) + binary.LittleEndian.PutUint64(b[16:24], v^key64) + v = binary.LittleEndian.Uint64(b[24:32]) + binary.LittleEndian.PutUint64(b[24:32], v^key64) + v = binary.LittleEndian.Uint64(b[32:40]) + binary.LittleEndian.PutUint64(b[32:40], v^key64) + v = binary.LittleEndian.Uint64(b[40:48]) + binary.LittleEndian.PutUint64(b[40:48], v^key64) + v = binary.LittleEndian.Uint64(b[48:56]) + binary.LittleEndian.PutUint64(b[48:56], v^key64) + v = binary.LittleEndian.Uint64(b[56:64]) + binary.LittleEndian.PutUint64(b[56:64], v^key64) + v = binary.LittleEndian.Uint64(b[64:72]) + binary.LittleEndian.PutUint64(b[64:72], v^key64) + v = binary.LittleEndian.Uint64(b[72:80]) + binary.LittleEndian.PutUint64(b[72:80], v^key64) + v = binary.LittleEndian.Uint64(b[80:88]) + binary.LittleEndian.PutUint64(b[80:88], v^key64) + v = binary.LittleEndian.Uint64(b[88:96]) + binary.LittleEndian.PutUint64(b[88:96], v^key64) + v = binary.LittleEndian.Uint64(b[96:104]) + binary.LittleEndian.PutUint64(b[96:104], v^key64) + v = binary.LittleEndian.Uint64(b[104:112]) + binary.LittleEndian.PutUint64(b[104:112], v^key64) + v = binary.LittleEndian.Uint64(b[112:120]) + binary.LittleEndian.PutUint64(b[112:120], v^key64) + v = binary.LittleEndian.Uint64(b[120:128]) + binary.LittleEndian.PutUint64(b[120:128], v^key64) b = b[128:] } @@ -381,20 +381,20 @@ func mask(key uint32, b []byte) uint32 { for len(b) >= 64 { v := binary.LittleEndian.Uint64(b) binary.LittleEndian.PutUint64(b, v^key64) - v = binary.LittleEndian.Uint64(b[8:]) - binary.LittleEndian.PutUint64(b[8:], v^key64) - v = binary.LittleEndian.Uint64(b[16:]) - binary.LittleEndian.PutUint64(b[16:], v^key64) - v = binary.LittleEndian.Uint64(b[24:]) - binary.LittleEndian.PutUint64(b[24:], v^key64) - v = binary.LittleEndian.Uint64(b[32:]) - binary.LittleEndian.PutUint64(b[32:], v^key64) - v = binary.LittleEndian.Uint64(b[40:]) - binary.LittleEndian.PutUint64(b[40:], v^key64) - v = binary.LittleEndian.Uint64(b[48:]) - binary.LittleEndian.PutUint64(b[48:], v^key64) - v = binary.LittleEndian.Uint64(b[56:]) - binary.LittleEndian.PutUint64(b[56:], v^key64) + v = binary.LittleEndian.Uint64(b[8:16]) + binary.LittleEndian.PutUint64(b[8:16], v^key64) + v = binary.LittleEndian.Uint64(b[16:24]) + binary.LittleEndian.PutUint64(b[16:24], v^key64) + v = binary.LittleEndian.Uint64(b[24:32]) + binary.LittleEndian.PutUint64(b[24:32], v^key64) + v = binary.LittleEndian.Uint64(b[32:40]) + binary.LittleEndian.PutUint64(b[32:40], v^key64) + v = binary.LittleEndian.Uint64(b[40:48]) + binary.LittleEndian.PutUint64(b[40:48], v^key64) + v = binary.LittleEndian.Uint64(b[48:56]) + binary.LittleEndian.PutUint64(b[48:56], v^key64) + v = binary.LittleEndian.Uint64(b[56:64]) + binary.LittleEndian.PutUint64(b[56:64], v^key64) b = b[64:] } @@ -402,12 +402,12 @@ func mask(key uint32, b []byte) uint32 { for len(b) >= 32 { v := binary.LittleEndian.Uint64(b) binary.LittleEndian.PutUint64(b, v^key64) - v = binary.LittleEndian.Uint64(b[8:]) - binary.LittleEndian.PutUint64(b[8:], v^key64) - v = binary.LittleEndian.Uint64(b[16:]) - binary.LittleEndian.PutUint64(b[16:], v^key64) - v = binary.LittleEndian.Uint64(b[24:]) - binary.LittleEndian.PutUint64(b[24:], v^key64) + v = binary.LittleEndian.Uint64(b[8:16]) + binary.LittleEndian.PutUint64(b[8:16], v^key64) + v = binary.LittleEndian.Uint64(b[16:24]) + binary.LittleEndian.PutUint64(b[16:24], v^key64) + v = binary.LittleEndian.Uint64(b[24:32]) + binary.LittleEndian.PutUint64(b[24:32], v^key64) b = b[32:] } @@ -415,8 +415,8 @@ func mask(key uint32, b []byte) uint32 { for len(b) >= 16 { v := binary.LittleEndian.Uint64(b) binary.LittleEndian.PutUint64(b, v^key64) - v = binary.LittleEndian.Uint64(b[8:]) - binary.LittleEndian.PutUint64(b[8:], v^key64) + v = binary.LittleEndian.Uint64(b[8:16]) + binary.LittleEndian.PutUint64(b[8:16], v^key64) b = b[16:] } From edda9c633d5c78c7d38fcc952b4105dd4ccfb619 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 9 Nov 2019 17:29:17 -0500 Subject: [PATCH 209/519] Add gobwas/ws and gorilla/websocket to mask benchmarks On average, we are 1.75x faster than both now :rocket: goos: linux goarch: amd64 pkg: nhooyr.io/websocket Benchmark_mask/2/basic-8 263923018 4.55 ns/op 439.76 MB/s Benchmark_mask/2/nhooyr-8 200203578 6.00 ns/op 333.32 MB/s Benchmark_mask/2/gorilla-8 241622557 4.95 ns/op 404.38 MB/s Benchmark_mask/2/gobwas-8 200392592 6.05 ns/op 330.69 MB/s Benchmark_mask/3/basic-8 207684956 5.78 ns/op 519.07 MB/s Benchmark_mask/3/nhooyr-8 169297215 7.10 ns/op 422.49 MB/s Benchmark_mask/3/gorilla-8 205775799 5.81 ns/op 516.23 MB/s Benchmark_mask/3/gobwas-8 165921662 7.23 ns/op 415.06 MB/s Benchmark_mask/4/basic-8 167034886 7.18 ns/op 557.30 MB/s Benchmark_mask/4/nhooyr-8 287656454 4.30 ns/op 931.11 MB/s Benchmark_mask/4/gorilla-8 166140434 7.30 ns/op 547.57 MB/s Benchmark_mask/4/gobwas-8 138138087 8.73 ns/op 458.20 MB/s Benchmark_mask/8/basic-8 121179904 9.92 ns/op 806.67 MB/s Benchmark_mask/8/nhooyr-8 199632992 6.07 ns/op 1318.60 MB/s Benchmark_mask/8/gorilla-8 100000000 10.8 ns/op 739.65 MB/s Benchmark_mask/8/gobwas-8 157898031 7.54 ns/op 1061.27 MB/s Benchmark_mask/16/basic-8 73648268 16.5 ns/op 971.56 MB/s Benchmark_mask/16/nhooyr-8 186871615 6.38 ns/op 2506.61 MB/s Benchmark_mask/16/gorilla-8 72374540 16.6 ns/op 964.36 MB/s Benchmark_mask/16/gobwas-8 127698723 9.36 ns/op 1709.99 MB/s Benchmark_mask/32/basic-8 40010325 29.8 ns/op 1073.76 MB/s Benchmark_mask/32/nhooyr-8 168590156 7.12 ns/op 4494.72 MB/s Benchmark_mask/32/gorilla-8 67282072 17.7 ns/op 1808.59 MB/s Benchmark_mask/32/gobwas-8 120038877 9.96 ns/op 3213.55 MB/s Benchmark_mask/128/basic-8 10134963 118 ns/op 1082.74 MB/s Benchmark_mask/128/nhooyr-8 100000000 11.8 ns/op 10852.23 MB/s Benchmark_mask/128/gorilla-8 45452385 26.4 ns/op 4853.64 MB/s Benchmark_mask/128/gobwas-8 57188290 20.8 ns/op 6153.80 MB/s Benchmark_mask/512/basic-8 2707371 442 ns/op 1159.38 MB/s Benchmark_mask/512/nhooyr-8 37049421 32.4 ns/op 15785.82 MB/s Benchmark_mask/512/gorilla-8 19006171 62.8 ns/op 8150.01 MB/s Benchmark_mask/512/gobwas-8 21394864 55.8 ns/op 9169.49 MB/s Benchmark_mask/4096/basic-8 346566 3467 ns/op 1181.46 MB/s Benchmark_mask/4096/nhooyr-8 5170425 232 ns/op 17648.07 MB/s Benchmark_mask/4096/gorilla-8 2963664 405 ns/op 10105.59 MB/s Benchmark_mask/4096/gobwas-8 2989596 402 ns/op 10192.40 MB/s Benchmark_mask/16384/basic-8 86671 13833 ns/op 1184.38 MB/s Benchmark_mask/16384/nhooyr-8 1332649 889 ns/op 18436.40 MB/s Benchmark_mask/16384/gorilla-8 763900 1556 ns/op 10527.53 MB/s Benchmark_mask/16384/gobwas-8 764034 1553 ns/op 10548.60 MB/s PASS ok nhooyr.io/websocket 64.091s Results from a 8 GB 8 core Haswell VM on GCP. --- frame_test.go | 29 ++++++++++++++++++++++++++--- go.mod | 4 ++++ go.sum | 8 ++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/frame_test.go b/frame_test.go index 2f3d4df2..571e68fc 100644 --- a/frame_test.go +++ b/frame_test.go @@ -13,8 +13,11 @@ import ( "strings" "testing" "time" + _ "unsafe" + "github.com/gobwas/ws" "github.com/google/go-cmp/cmp" + _ "github.com/gorilla/websocket" "nhooyr.io/websocket/internal/assert" ) @@ -325,7 +328,7 @@ func Test_mask(t *testing.T) { } } -func basixMask(maskKey [4]byte, pos int, b []byte) int { +func basicMask(maskKey [4]byte, pos int, b []byte) int { for i := range b { b[i] ^= maskKey[pos&3] pos++ @@ -333,6 +336,9 @@ func basixMask(maskKey [4]byte, pos int, b []byte) int { return pos & 3 } +//go:linkname gorillaMaskBytes github.com/gorilla/websocket.maskBytes +func gorillaMaskBytes(key [4]byte, pos int, b []byte) int + func Benchmark_mask(b *testing.B) { sizes := []int{ 2, @@ -355,12 +361,13 @@ func Benchmark_mask(b *testing.B) { name: "basic", fn: func(b *testing.B, key [4]byte, p []byte) { for i := 0; i < b.N; i++ { - basixMask(key, 0, p) + basicMask(key, 0, p) } }, }, + { - name: "fast", + name: "nhooyr", fn: func(b *testing.B, key [4]byte, p []byte) { key32 := binary.LittleEndian.Uint32(key[:]) b.ResetTimer() @@ -370,6 +377,22 @@ func Benchmark_mask(b *testing.B) { } }, }, + { + name: "gorilla", + fn: func(b *testing.B, key [4]byte, p []byte) { + for i := 0; i < b.N; i++ { + gorillaMaskBytes(key, 0, p) + } + }, + }, + { + name: "gobwas", + fn: func(b *testing.B, key [4]byte, p []byte) { + for i := 0; i < b.N; i++ { + ws.Cipher(p, key, 0) + } + }, + }, } var key [4]byte diff --git a/go.mod b/go.mod index e733429b..e6ef0014 100644 --- a/go.mod +++ b/go.mod @@ -4,8 +4,12 @@ go 1.13 require ( github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee // indirect + github.com/gobwas/pool v0.2.0 // indirect + github.com/gobwas/ws v1.0.2 github.com/golang/protobuf v1.3.2 github.com/google/go-cmp v0.3.1 + github.com/gorilla/websocket v1.4.1 github.com/kr/pretty v0.1.0 // indirect github.com/stretchr/testify v1.4.0 // indirect go.uber.org/atomic v1.4.0 // indirect diff --git a/go.sum b/go.sum index 954b79f5..d2f1f0e4 100644 --- a/go.sum +++ b/go.sum @@ -2,10 +2,18 @@ github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8 github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= From fa825fdcf9758dbf80667c5ea6715bf235959cc8 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 9 Nov 2019 17:36:09 -0500 Subject: [PATCH 210/519] Mention masking algorithm performance in README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index ea26237a..f6780e02 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,9 @@ In terms of performance, the differences mostly depend on your application code. reuses message buffers out of the box if you use the wsjson and wspb subpackages. As mentioned above, nhooyr.io/websocket also supports concurrent writers. +The WebSocket masking algorithm used by this package is also [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster +on average than gorilla/websocket or gobwas/ws while using pure safe Go. + The only performance con to nhooyr.io/websocket is that it uses one extra goroutine to support cancellation with context.Context. This costs 2 KB of memory which is cheap compared to the benefits. From 58ab202e85a92ce7757e1d15bbfbcb8d9f0dc0e6 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 9 Nov 2019 17:37:37 -0500 Subject: [PATCH 211/519] Cleanup grammar in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f6780e02..c426423a 100644 --- a/README.md +++ b/README.md @@ -138,8 +138,8 @@ In terms of performance, the differences mostly depend on your application code. reuses message buffers out of the box if you use the wsjson and wspb subpackages. As mentioned above, nhooyr.io/websocket also supports concurrent writers. -The WebSocket masking algorithm used by this package is also [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster -on average than gorilla/websocket or gobwas/ws while using pure safe Go. +The WebSocket masking algorithm used by this package is also [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) +faster than gorilla/websocket or gobwas/ws while using only pure safe Go. The only performance con to nhooyr.io/websocket is that it uses one extra goroutine to support cancellation with context.Context. This costs 2 KB of memory which is cheap compared to From cb50d970b47dbaf197309be9c72b46732209b346 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 24 Nov 2019 22:15:43 -0500 Subject: [PATCH 212/519] Correct typo Closes #172 --- handshake.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/handshake.go b/handshake.go index d1a9fba4..2c01cab6 100644 --- a/handshake.go +++ b/handshake.go @@ -329,7 +329,7 @@ func dial(ctx context.Context, u string, opts *DialOptions) (_ *Conn, _ *http.Re rwc, ok := resp.Body.(io.ReadWriteCloser) if !ok { - return nil, resp, fmt.Errorf("response body is not a io.ReadWriteCloser: %T", rwc) + return nil, resp, fmt.Errorf("response body is not a io.ReadWriteCloser: %T", resp.Body) } c := &Conn{ From 8604dee32ef3a441729179f610eb3dce2e40c5ff Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 14 Oct 2019 21:31:46 -0400 Subject: [PATCH 213/519] Increase TestWASM timeout --- conn_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conn_test.go b/conn_test.go index 83f09dbf..d03a7214 100644 --- a/conn_test.go +++ b/conn_test.go @@ -2377,7 +2377,7 @@ func TestWASM(t *testing.T) { wsURL := strings.Replace(s.URL, "http", "ws", 1) - ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() cmd := exec.CommandContext(ctx, "go", "test", "-exec=wasmbrowsertest", "./...") From e55ac18137f04b40dc74556bfb2b92b242db32b5 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 14 Oct 2019 16:49:51 -0400 Subject: [PATCH 214/519] Document compression API So it begins :) --- handshake.go | 48 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/handshake.go b/handshake.go index 2c01cab6..81ebf48a 100644 --- a/handshake.go +++ b/handshake.go @@ -45,6 +45,11 @@ type AcceptOptions struct { // If you do, remember that if you store secure data in cookies, you wil need to verify the // Origin header yourself otherwise you are exposing yourself to a CSRF attack. InsecureSkipVerify bool + + // Compression sets the compression options. + // By default, compression is disabled. + // See docs on the CompressionOptions type. + Compression *CompressionOptions } func verifyClientRequest(w http.ResponseWriter, r *http.Request) error { @@ -240,6 +245,49 @@ type DialOptions struct { // Subprotocols lists the subprotocols to negotiate with the server. Subprotocols []string + + // Compression sets the compression options. + // By default, compression is disabled. + // See docs on the CompressionOptions type. + Compression CompressionOptions +} + +// CompressionOptions describes the available compression options. +// +// See https://tools.ietf.org/html/rfc7692 +// +// Enabling compression may spike memory usage as each flate.Writer takes up 1.2 MB. +// See https://github.com/gorilla/websocket/issues/203 +// Benchmark before enabling in production. +// +// This API is experimental and subject to change. +type CompressionOptions struct { + // ContextTakeover controls whether context takeover is enabled. + // + // If ContextTakeover == false, then a flate.Writer will be grabbed + // from the pool as needed for every message written to the connection. + // + // If ContextTakeover == true, then a flate.Writer will be allocated for each connection. + // This allows more efficient compression as the sliding window from previous + // messages will be used instead of resetting in between every message. + // The downside is that for every connection there will be a fixed allocation + // for the flate.Writer. + // + // See https://www.igvita.com/2013/11/27/configuring-and-optimizing-websocket-compression. + ContextTakeover bool + + // Level controls the compression level negotiated. + // Defaults to flate.BestSpeed. + Level int + + // Threshold controls the minimum message size in bytes before compression is used. + // In the case of ContextTakeover == false, a flate.Writer will not be grabbed + // from the pool until the message exceeds this threshold. + // + // Must not be greater than 4096 as that is the write buffer's size. + // + // Defaults to 512. + Threshold int } // Dial performs a WebSocket handshake on the given url with the given options. From e142e08cbe82354cbee73f4b023623f04a55924d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 11 Nov 2019 19:28:39 -0500 Subject: [PATCH 215/519] Improve compression docs --- README.md | 8 ++++---- handshake.go | 44 +++++++++++++++++++++++++++++--------------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index c426423a..b5adc59c 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # websocket -[![GitHub Release](https://img.shields.io/github/v/release/nhooyr/websocket?color=6b9ded&sort=semver)](https://github.com/nhooyr/websocket/releases) -[![GoDoc](https://godoc.org/nhooyr.io/websocket?status.svg)](https://godoc.org/nhooyr.io/websocket) -[![Coveralls](https://img.shields.io/coveralls/github/nhooyr/websocket?color=65d6a4)](https://coveralls.io/github/nhooyr/websocket) -[![CI Status](https://github.com/nhooyr/websocket/workflows/ci/badge.svg)](https://github.com/nhooyr/websocket/actions) +[![version](https://img.shields.io/github/v/release/nhooyr/websocket?color=6b9ded&sort=semver)](https://github.com/nhooyr/websocket/releases) +[![docs](https://godoc.org/nhooyr.io/websocket?status.svg)](https://godoc.org/nhooyr.io/websocket) +[![coverage](https://img.shields.io/coveralls/github/nhooyr/websocket?color=65d6a4)](https://coveralls.io/github/nhooyr/websocket) +[![ci](https://github.com/nhooyr/websocket/workflows/ci/badge.svg)](https://github.com/nhooyr/websocket/actions) websocket is a minimal and idiomatic WebSocket library for Go. diff --git a/handshake.go b/handshake.go index 81ebf48a..2cde6ae2 100644 --- a/handshake.go +++ b/handshake.go @@ -249,34 +249,45 @@ type DialOptions struct { // Compression sets the compression options. // By default, compression is disabled. // See docs on the CompressionOptions type. - Compression CompressionOptions + Compression *CompressionOptions } // CompressionOptions describes the available compression options. // // See https://tools.ietf.org/html/rfc7692 // -// Enabling compression may spike memory usage as each flate.Writer takes up 1.2 MB. +// The NoContextTakeover variables control whether a flate.Writer or flate.Reader is allocated +// for every connection (context takeover) versus shared from a pool (no context takeover). +// +// The advantage to context takeover is more efficient compression as the sliding window from previous +// messages will be used instead of being reset between every message. +// +// The advantage to no context takeover is that the flate structures are allocated as needed +// and shared between connections instead of giving each connection a fixed flate.Writer and +// flate.Reader. +// +// See https://www.igvita.com/2013/11/27/configuring-and-optimizing-websocket-compression. +// +// Enabling compression will increase memory and CPU usage. +// Thus it is not ideal for every use case and disabled by default. // See https://github.com/gorilla/websocket/issues/203 -// Benchmark before enabling in production. +// Profile before enabling in production. // // This API is experimental and subject to change. type CompressionOptions struct { - // ContextTakeover controls whether context takeover is enabled. - // - // If ContextTakeover == false, then a flate.Writer will be grabbed - // from the pool as needed for every message written to the connection. + // ServerNoContextTakeover controls whether the server should use context takeover. + // See docs on CompressionOptions for discussion regarding context takeover. // - // If ContextTakeover == true, then a flate.Writer will be allocated for each connection. - // This allows more efficient compression as the sliding window from previous - // messages will be used instead of resetting in between every message. - // The downside is that for every connection there will be a fixed allocation - // for the flate.Writer. + // If set by the client, will guarantee that the server does not use context takeover. + ServerNoContextTakeover bool + + // ClientNoContextTakeover controls whether the client should use context takeover. + // See docs on CompressionOptions for discussion regarding context takeover. // - // See https://www.igvita.com/2013/11/27/configuring-and-optimizing-websocket-compression. - ContextTakeover bool + // If set by the server, will guarantee that the client does not use context takeover. + ClientNoContextTakeover bool - // Level controls the compression level negotiated. + // Level controls the compression level used. // Defaults to flate.BestSpeed. Level int @@ -355,6 +366,9 @@ func dial(ctx context.Context, u string, opts *DialOptions) (_ *Conn, _ *http.Re if len(opts.Subprotocols) > 0 { req.Header.Set("Sec-WebSocket-Protocol", strings.Join(opts.Subprotocols, ",")) } + if opts.Compression != nil { + req.Header.Set("Sec-WebSocket-Extensions", "permessage-deflate; server_no_context_takeover; client_no_context_takeover") + } resp, err := opts.HTTPClient.Do(req) if err != nil { From 53c1aea0c6ec1169acb4359dd2361e938e910455 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 11 Nov 2019 21:01:08 -0500 Subject: [PATCH 216/519] Implement compression extension negotiation --- conn.go | 1 + doc.go | 1 + handshake.go | 190 +++++++++++++++++++++++++++++++++++----------- handshake_test.go | 2 +- 4 files changed, 149 insertions(+), 45 deletions(-) diff --git a/conn.go b/conn.go index 26906c79..14d93cf6 100644 --- a/conn.go +++ b/conn.go @@ -42,6 +42,7 @@ type Conn struct { writeBuf []byte closer io.Closer client bool + copts *CompressionOptions closeOnce sync.Once closeErrOnce sync.Once diff --git a/doc.go b/doc.go index b29d2cdd..804665fb 100644 --- a/doc.go +++ b/doc.go @@ -31,6 +31,7 @@ // - Accept and AcceptOptions // - Conn.Ping // - HTTPClient and HTTPHeader fields in DialOptions +// - CompressionOptions // // The *http.Response returned by Dial will always either be nil or &http.Response{} as // we do not have access to the handshake response in the browser. diff --git a/handshake.go b/handshake.go index 2cde6ae2..787fee2c 100644 --- a/handshake.go +++ b/handshake.go @@ -59,13 +59,13 @@ func verifyClientRequest(w http.ResponseWriter, r *http.Request) error { return err } - if !headerValuesContainsToken(r.Header, "Connection", "Upgrade") { + if !headerContainsToken(r.Header, "Connection", "Upgrade") { err := fmt.Errorf("websocket protocol violation: Connection header %q does not contain Upgrade", r.Header.Get("Connection")) http.Error(w, err.Error(), http.StatusBadRequest) return err } - if !headerValuesContainsToken(r.Header, "Upgrade", "WebSocket") { + if !headerContainsToken(r.Header, "Upgrade", "WebSocket") { err := fmt.Errorf("websocket protocol violation: Upgrade header %q does not contain websocket", r.Header.Get("Upgrade")) http.Error(w, err.Error(), http.StatusBadRequest) return err @@ -144,6 +144,18 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, w.Header().Set("Sec-WebSocket-Protocol", subproto) } + var copts *CompressionOptions + if opts.Compression != nil { + copts, err = negotiateCompression(r.Header, opts.Compression) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return nil, err + } + if copts != nil { + copts.setHeader(w.Header()) + } + } + w.WriteHeader(http.StatusSwitchingProtocols) netConn, brw, err := hj.Hijack() @@ -162,17 +174,23 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, br: brw.Reader, bw: brw.Writer, closer: netConn, + copts: copts, } c.init() return c, nil } -func headerValuesContainsToken(h http.Header, key, token string) bool { +func headerContainsToken(h http.Header, key, token string) bool { key = textproto.CanonicalMIMEHeaderKey(key) - for _, val2 := range h[key] { - if headerValueContainsToken(val2, token) { + token = strings.ToLower(token) + match := func(t string) bool { + return t == token + } + + for _, v := range h[key] { + if searchHeaderTokens(v, match) != "" { return true } } @@ -180,22 +198,41 @@ func headerValuesContainsToken(h http.Header, key, token string) bool { return false } -func headerValueContainsToken(val2, token string) bool { - val2 = strings.TrimSpace(val2) +func headerTokenHasPrefix(h http.Header, key, prefix string) string { + key = textproto.CanonicalMIMEHeaderKey(key) - for _, val2 := range strings.Split(val2, ",") { - val2 = strings.TrimSpace(val2) - if strings.EqualFold(val2, token) { - return true + prefix = strings.ToLower(prefix) + match := func(t string) bool { + return strings.HasPrefix(t, prefix) + } + + for _, v := range h[key] { + found := searchHeaderTokens(v, match) + if found != "" { + return found } } - return false + return "" +} + +func searchHeaderTokens(v string, match func(val string) bool) string { + v = strings.TrimSpace(v) + + for _, v2 := range strings.Split(v, ",") { + v2 = strings.TrimSpace(v2) + v2 = strings.ToLower(v2) + if match(v2) { + return v2 + } + } + + return "" } func selectSubprotocol(r *http.Request, subprotocols []string) string { for _, sp := range subprotocols { - if headerValuesContainsToken(r.Header, "Sec-WebSocket-Protocol", sp) { + if headerContainsToken(r.Header, "Sec-WebSocket-Protocol", sp) { return sp } } @@ -268,36 +305,32 @@ type DialOptions struct { // // See https://www.igvita.com/2013/11/27/configuring-and-optimizing-websocket-compression. // -// Enabling compression will increase memory and CPU usage. -// Thus it is not ideal for every use case and disabled by default. +// Enabling compression will increase memory and CPU usage and should +// be profiled before enabling in production. // See https://github.com/gorilla/websocket/issues/203 -// Profile before enabling in production. // // This API is experimental and subject to change. type CompressionOptions struct { - // ServerNoContextTakeover controls whether the server should use context takeover. - // See docs on CompressionOptions for discussion regarding context takeover. - // - // If set by the client, will guarantee that the server does not use context takeover. - ServerNoContextTakeover bool - // ClientNoContextTakeover controls whether the client should use context takeover. // See docs on CompressionOptions for discussion regarding context takeover. // // If set by the server, will guarantee that the client does not use context takeover. ClientNoContextTakeover bool + // ServerNoContextTakeover controls whether the server should use context takeover. + // See docs on CompressionOptions for discussion regarding context takeover. + // + // If set by the client, will guarantee that the server does not use context takeover. + ServerNoContextTakeover bool + // Level controls the compression level used. // Defaults to flate.BestSpeed. Level int // Threshold controls the minimum message size in bytes before compression is used. - // In the case of ContextTakeover == false, a flate.Writer will not be grabbed - // from the pool until the message exceeds this threshold. - // // Must not be greater than 4096 as that is the write buffer's size. // - // Defaults to 512. + // Defaults to 256. Threshold int } @@ -319,25 +352,32 @@ func Dial(ctx context.Context, u string, opts *DialOptions) (*Conn, *http.Respon return c, r, nil } -func dial(ctx context.Context, u string, opts *DialOptions) (_ *Conn, _ *http.Response, err error) { +func (opts *DialOptions) ensure() (*DialOptions, error) { if opts == nil { opts = &DialOptions{} + } else { + opts = &*opts } - // Shallow copy to ensure defaults do not affect user passed options. - opts2 := *opts - opts = &opts2 - if opts.HTTPClient == nil { opts.HTTPClient = http.DefaultClient } if opts.HTTPClient.Timeout > 0 { - return nil, nil, fmt.Errorf("use context for cancellation instead of http.Client.Timeout; see https://github.com/nhooyr/websocket/issues/67") + return nil, fmt.Errorf("use context for cancellation instead of http.Client.Timeout; see https://github.com/nhooyr/websocket/issues/67") } if opts.HTTPHeader == nil { opts.HTTPHeader = http.Header{} } + return opts, nil +} + +func dial(ctx context.Context, u string, opts *DialOptions) (_ *Conn, _ *http.Response, err error) { + opts, err = opts.ensure() + if err != nil { + return nil, nil, err + } + parsedURL, err := url.Parse(u) if err != nil { return nil, nil, fmt.Errorf("failed to parse url: %w", err) @@ -367,7 +407,7 @@ func dial(ctx context.Context, u string, opts *DialOptions) (_ *Conn, _ *http.Re req.Header.Set("Sec-WebSocket-Protocol", strings.Join(opts.Subprotocols, ",")) } if opts.Compression != nil { - req.Header.Set("Sec-WebSocket-Extensions", "permessage-deflate; server_no_context_takeover; client_no_context_takeover") + opts.Compression.setHeader(req.Header) } resp, err := opts.HTTPClient.Do(req) @@ -384,7 +424,7 @@ func dial(ctx context.Context, u string, opts *DialOptions) (_ *Conn, _ *http.Re } }() - err = verifyServerResponse(req, resp) + copts, err := verifyServerResponse(req, resp, opts) if err != nil { return nil, resp, err } @@ -400,6 +440,7 @@ func dial(ctx context.Context, u string, opts *DialOptions) (_ *Conn, _ *http.Re bw: getBufioWriter(rwc), closer: rwc, client: true, + copts: copts, } c.extractBufioWriterBuf(rwc) c.init() @@ -407,31 +448,40 @@ func dial(ctx context.Context, u string, opts *DialOptions) (_ *Conn, _ *http.Re return c, resp, nil } -func verifyServerResponse(r *http.Request, resp *http.Response) error { +func verifyServerResponse(r *http.Request, resp *http.Response, opts *DialOptions) (*CompressionOptions, error) { if resp.StatusCode != http.StatusSwitchingProtocols { - return fmt.Errorf("expected handshake response status code %v but got %v", http.StatusSwitchingProtocols, resp.StatusCode) + return nil, fmt.Errorf("expected handshake response status code %v but got %v", http.StatusSwitchingProtocols, resp.StatusCode) } - if !headerValuesContainsToken(resp.Header, "Connection", "Upgrade") { - return fmt.Errorf("websocket protocol violation: Connection header %q does not contain Upgrade", resp.Header.Get("Connection")) + if !headerContainsToken(resp.Header, "Connection", "Upgrade") { + return nil, fmt.Errorf("websocket protocol violation: Connection header %q does not contain Upgrade", resp.Header.Get("Connection")) } - if !headerValuesContainsToken(resp.Header, "Upgrade", "WebSocket") { - return fmt.Errorf("websocket protocol violation: Upgrade header %q does not contain websocket", resp.Header.Get("Upgrade")) + if !headerContainsToken(resp.Header, "Upgrade", "WebSocket") { + return nil, fmt.Errorf("websocket protocol violation: Upgrade header %q does not contain websocket", resp.Header.Get("Upgrade")) } if resp.Header.Get("Sec-WebSocket-Accept") != secWebSocketAccept(r.Header.Get("Sec-WebSocket-Key")) { - return fmt.Errorf("websocket protocol violation: invalid Sec-WebSocket-Accept %q, key %q", + return nil, fmt.Errorf("websocket protocol violation: invalid Sec-WebSocket-Accept %q, key %q", resp.Header.Get("Sec-WebSocket-Accept"), r.Header.Get("Sec-WebSocket-Key"), ) } - if proto := resp.Header.Get("Sec-WebSocket-Protocol"); proto != "" && !headerValuesContainsToken(r.Header, "Sec-WebSocket-Protocol", proto) { - return fmt.Errorf("websocket protocol violation: unexpected Sec-WebSocket-Protocol from server: %q", proto) + if proto := resp.Header.Get("Sec-WebSocket-Protocol"); proto != "" && !headerContainsToken(r.Header, "Sec-WebSocket-Protocol", proto) { + return nil, fmt.Errorf("websocket protocol violation: unexpected Sec-WebSocket-Protocol from server: %q", proto) } - return nil + var copts *CompressionOptions + if opts.Compression != nil { + var err error + copts, err = negotiateCompression(resp.Header, opts.Compression) + if err != nil { + return nil, err + } + } + + return copts, nil } // The below pools can only be used by the client because http.Hijacker will always @@ -477,3 +527,55 @@ func makeSecWebSocketKey() (string, error) { } return base64.StdEncoding.EncodeToString(b), nil } + +func negotiateCompression(h http.Header, copts *CompressionOptions) (*CompressionOptions, error) { + deflate := headerTokenHasPrefix(h, "Sec-WebSocket-Extensions", "permessage-deflate") + if deflate == "" { + return nil, nil + } + + // Ensures our changes do not modify the real compression options. + copts = &*copts + + params := strings.Split(deflate, ";") + for i := range params { + params[i] = strings.TrimSpace(params[i]) + } + + if params[0] != "permessage-deflate" { + return nil, fmt.Errorf("unexpected header format for permessage-deflate extension: %q", deflate) + } + + for _, p := range params[1:] { + switch p { + case "client_no_context_takeover": + copts.ClientNoContextTakeover = true + continue + case "server_no_context_takeover": + copts.ServerNoContextTakeover = true + continue + case "client_max_window_bits", "server-max-window-bits": + server := h.Get("Sec-WebSocket-Key") != "" + if server { + // If we are the server, we are allowed to ignore these parameters. + // However, if we are the client, we must obey them but because of + // https://github.com/golang/go/issues/3155 we cannot. + continue + } + } + return nil, fmt.Errorf("unsupported permessage-deflate parameter %q in header: %q", p, deflate) + } + + return copts, nil +} + +func (copts *CompressionOptions) setHeader(h http.Header) { + s := "permessage-deflate" + if copts.ClientNoContextTakeover { + s += "; client_no_context_takeover" + } + if copts.ServerNoContextTakeover { + s += "; server_no_context_takeover" + } + h.Set("Sec-WebSocket-Extensions", s) +} diff --git a/handshake_test.go b/handshake_test.go index cb09353f..82f958e0 100644 --- a/handshake_test.go +++ b/handshake_test.go @@ -377,7 +377,7 @@ func Test_verifyServerHandshake(t *testing.T) { resp.Header.Set("Sec-WebSocket-Accept", secWebSocketAccept(key)) } - err = verifyServerResponse(r, resp) + _, err = verifyServerResponse(r, resp, &DialOptions{}) if (err == nil) != tc.success { t.Fatalf("unexpected error: %+v", err) } From 2cf6c28875c3511edfee7409b5f25a994d2edbf3 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 11 Nov 2019 21:29:08 -0500 Subject: [PATCH 217/519] Implement compression writer and reader pooling --- conn.go | 53 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/conn.go b/conn.go index 14d93cf6..32dfa81b 100644 --- a/conn.go +++ b/conn.go @@ -4,6 +4,7 @@ package websocket import ( "bufio" + "compress/flate" "context" "crypto/rand" "encoding/binary" @@ -1063,3 +1064,55 @@ func (c *Conn) extractBufioWriterBuf(w io.Writer) { c.bw.Reset(w) } + +var flateWriterPoolsMu sync.Mutex +var flateWriterPools = make(map[int]*sync.Pool) + +func getFlateWriterPool(level int) *sync.Pool { + flateWriterPoolsMu.Lock() + defer flateWriterPoolsMu.Unlock() + + p, ok := flateWriterPools[level] + if !ok { + p = &sync.Pool{ + New: func() interface{} { + w, err := flate.NewWriter(nil, level) + if err != nil { + panic("websocket: unexpected error from flate.NewWriter: " + err.Error()) + } + return w + }, + } + flateWriterPools[level] = p + } + + return p +} + +func getFlateWriter(w io.Writer, level int) *flate.Writer { + p := getFlateWriterPool(level) + fw := p.Get().(*flate.Writer) + fw.Reset(w) + return fw +} + +func putFlateWriter(w *flate.Writer, level int) { + p := getFlateWriterPool(level) + p.Put(w) +} + +var flateReaderPool = &sync.Pool{ + New: func() interface{} { + return flate.NewReader(nil) + }, +} + +func getFlateReader(r flate.Reader) io.ReadCloser { + fr := flateReaderPool.Get().(io.ReadCloser) + fr.(flate.Resetter).Reset(r, nil) + return fr +} + +func putFlateReader(fr io.ReadCloser) { + flateReaderPool.Put(fr) +} From a01afeace4a00b64f92eb94a6d5c40d22b6386e3 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 11 Nov 2019 22:55:47 -0500 Subject: [PATCH 218/519] Support x-webkit-deflate-frame extension for Safari --- handshake.go | 130 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 93 insertions(+), 37 deletions(-) diff --git a/handshake.go b/handshake.go index 787fee2c..03331039 100644 --- a/handshake.go +++ b/handshake.go @@ -152,7 +152,7 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, return nil, err } if copts != nil { - copts.setHeader(w.Header()) + copts.setHeader(w.Header(), false) } } @@ -190,7 +190,7 @@ func headerContainsToken(h http.Header, key, token string) bool { } for _, v := range h[key] { - if searchHeaderTokens(v, match) != "" { + if searchHeaderTokens(v, match) { return true } } @@ -198,36 +198,54 @@ func headerContainsToken(h http.Header, key, token string) bool { return false } -func headerTokenHasPrefix(h http.Header, key, prefix string) string { - key = textproto.CanonicalMIMEHeaderKey(key) - - prefix = strings.ToLower(prefix) +// readCompressionExtensionHeader extracts compression extension info from h. +// The standard says we should support multiple compression extension configurations +// from the client but we don't need to as there is only a single deflate extension +// and we support every configuration without error so we only need to check the first +// and thus preferred configuration. +func readCompressionExtensionHeader(h http.Header) (xWebkitDeflateFrame bool, params []string, ok bool) { match := func(t string) bool { - return strings.HasPrefix(t, prefix) + vals := strings.Split(t, ";") + for i := range vals { + vals[i] = strings.TrimSpace(vals[i]) + } + params = vals[1:] + + if vals[0] == "permessage-deflate" { + return true + } + + // See https://bugs.webkit.org/show_bug.cgi?id=115504 + if vals[0] == "x-webkit-deflate-frame" { + xWebkitDeflateFrame = true + return true + } + + return false } + key := textproto.CanonicalMIMEHeaderKey("Sec-WebSocket-Extensions") for _, v := range h[key] { - found := searchHeaderTokens(v, match) - if found != "" { - return found + if searchHeaderTokens(v, match) { + return xWebkitDeflateFrame, params, true } } - return "" + return false, nil, false } -func searchHeaderTokens(v string, match func(val string) bool) string { +func searchHeaderTokens(v string, match func(val string) bool) bool { + v = strings.ToLower(v) v = strings.TrimSpace(v) for _, v2 := range strings.Split(v, ",") { v2 = strings.TrimSpace(v2) - v2 = strings.ToLower(v2) if match(v2) { - return v2 + return true } } - return "" + return false } func selectSubprotocol(r *http.Request, subprotocols []string) string { @@ -332,6 +350,10 @@ type CompressionOptions struct { // // Defaults to 256. Threshold int + + // This is used for supporting Safari as it still uses x-webkit-deflate-frame. + // See negotiateCompression. + xWebkitDeflateFrame bool } // Dial performs a WebSocket handshake on the given url with the given options. @@ -407,7 +429,7 @@ func dial(ctx context.Context, u string, opts *DialOptions) (_ *Conn, _ *http.Re req.Header.Set("Sec-WebSocket-Protocol", strings.Join(opts.Subprotocols, ",")) } if opts.Compression != nil { - opts.Compression.setHeader(req.Header) + opts.Compression.setHeader(req.Header, true) } resp, err := opts.HTTPClient.Do(req) @@ -529,24 +551,30 @@ func makeSecWebSocketKey() (string, error) { } func negotiateCompression(h http.Header, copts *CompressionOptions) (*CompressionOptions, error) { - deflate := headerTokenHasPrefix(h, "Sec-WebSocket-Extensions", "permessage-deflate") - if deflate == "" { + xWebkitDeflateFrame, params, ok := readCompressionExtensionHeader(h) + if !ok { return nil, nil } // Ensures our changes do not modify the real compression options. copts = &*copts - - params := strings.Split(deflate, ";") - for i := range params { - params[i] = strings.TrimSpace(params[i]) - } - - if params[0] != "permessage-deflate" { - return nil, fmt.Errorf("unexpected header format for permessage-deflate extension: %q", deflate) + copts.xWebkitDeflateFrame = xWebkitDeflateFrame + + // We are the client if the header contains the accept header, meaning its from the server. + client := h.Get("Sec-WebSocket-Accept") == "" + + if copts.xWebkitDeflateFrame { + // The other endpoint dictates whether or not we can + // use context takeover on our side. We cannot force it. + // Likewise, we tell the other side so we can force that. + if client { + copts.ClientNoContextTakeover = false + } else { + copts.ServerNoContextTakeover = false + } } - for _, p := range params[1:] { + for _, p := range params { switch p { case "client_no_context_takeover": copts.ClientNoContextTakeover = true @@ -555,27 +583,55 @@ func negotiateCompression(h http.Header, copts *CompressionOptions) (*Compressio copts.ServerNoContextTakeover = true continue case "client_max_window_bits", "server-max-window-bits": - server := h.Get("Sec-WebSocket-Key") != "" - if server { + if !client { // If we are the server, we are allowed to ignore these parameters. // However, if we are the client, we must obey them but because of // https://github.com/golang/go/issues/3155 we cannot. continue } + case "no_context_takeover": + if copts.xWebkitDeflateFrame { + if client { + copts.ClientNoContextTakeover = true + } else { + copts.ServerNoContextTakeover = true + } + continue + } + + // We explicitly fail on x-webkit-deflate-frame's max_window_bits parameter instead + // of ignoring it as the draft spec is unclear. It says the server can ignore it + // but the server has no way of signalling to the client it was ignored as parameters + // are set one way. + // Thus us ignoring it would make the client think we understood it which would cause issues. + // See https://tools.ietf.org/html/draft-tyoshino-hybi-websocket-perframe-deflate-06#section-4.1 + // + // Either way, we're only implementing this for webkit which never sends the max_window_bits + // parameter so we don't need to worry about it. } - return nil, fmt.Errorf("unsupported permessage-deflate parameter %q in header: %q", p, deflate) + + return nil, fmt.Errorf("unsupported permessage-deflate parameter: %q", p) } return copts, nil } -func (copts *CompressionOptions) setHeader(h http.Header) { - s := "permessage-deflate" - if copts.ClientNoContextTakeover { - s += "; client_no_context_takeover" - } - if copts.ServerNoContextTakeover { - s += "; server_no_context_takeover" +func (copts *CompressionOptions) setHeader(h http.Header, client bool) { + var s string + if !copts.xWebkitDeflateFrame { + s := "permessage-deflate" + if copts.ClientNoContextTakeover { + s += "; client_no_context_takeover" + } + if copts.ServerNoContextTakeover { + s += "; server_no_context_takeover" + } + } else { + s = "x-webkit-deflate-frame" + // We can only set no context takeover for the peer. + if client && copts.ServerNoContextTakeover || !client && copts.ClientNoContextTakeover { + s += "; no_context_takeover" + } } h.Set("Sec-WebSocket-Extensions", s) } From 531d4fab2b30955df6ca43aea0417eb7aa60d515 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 12 Nov 2019 11:15:17 -0500 Subject: [PATCH 219/519] Improve general compression API and write docs --- README.md | 46 +- accept.go | 330 +++++++++ handshake_test.go => accept_test.go | 143 ---- assert_test.go | 56 +- ci/fmt.mk | 2 +- close.go | 181 +++++ close_test.go | 196 ++++++ compress.go | 78 +++ conn.go | 297 +++++--- dial.go | 219 ++++++ dial_test.go | 149 ++++ doc.go | 2 +- frame.go | 445 ------------ frame_test.go | 457 ------------- handshake.go | 637 ------------------ internal/assert/assert.go | 18 +- internal/atomicint/atomicint.go | 32 + internal/{bpool/bpool.go => bufpool/buf.go} | 2 +- .../bpool_test.go => bufpool/buf_test.go} | 2 +- internal/bufpool/bufio.go | 40 ++ internal/wsframe/frame.go | 194 ++++++ .../wsframe/frame_stringer.go | 20 +- internal/wsframe/frame_test.go | 157 +++++ internal/wsframe/mask.go | 128 ++++ internal/wsframe/mask_test.go | 118 ++++ js_test.go | 50 ++ conn_common.go => netconn.go | 78 --- reader.go | 31 + websocket_js_test.go | 52 -- writer.go | 5 + websocket_js.go => ws_js.go | 58 +- ws_js_test.go | 22 + wsjson/wsjson.go | 7 +- wspb/wspb.go | 10 +- 34 files changed, 2243 insertions(+), 2019 deletions(-) create mode 100644 accept.go rename handshake_test.go => accept_test.go (62%) create mode 100644 close.go create mode 100644 close_test.go create mode 100644 compress.go create mode 100644 dial.go create mode 100644 dial_test.go delete mode 100644 frame.go delete mode 100644 frame_test.go delete mode 100644 handshake.go create mode 100644 internal/atomicint/atomicint.go rename internal/{bpool/bpool.go => bufpool/buf.go} (95%) rename internal/{bpool/bpool_test.go => bufpool/buf_test.go} (97%) create mode 100644 internal/bufpool/bufio.go create mode 100644 internal/wsframe/frame.go rename frame_stringer.go => internal/wsframe/frame_stringer.go (90%) create mode 100644 internal/wsframe/frame_test.go create mode 100644 internal/wsframe/mask.go create mode 100644 internal/wsframe/mask_test.go create mode 100644 js_test.go rename conn_common.go => netconn.go (60%) create mode 100644 reader.go delete mode 100644 websocket_js_test.go create mode 100644 writer.go rename websocket_js.go => ws_js.go (88%) create mode 100644 ws_js_test.go diff --git a/README.md b/README.md index b5adc59c..17c7c838 100644 --- a/README.md +++ b/README.md @@ -22,13 +22,14 @@ go get nhooyr.io/websocket - [Zero dependencies](https://godoc.org/nhooyr.io/websocket?imports) - JSON and ProtoBuf helpers in the [wsjson](https://godoc.org/nhooyr.io/websocket/wsjson) and [wspb](https://godoc.org/nhooyr.io/websocket/wspb) subpackages - Highly optimized by default + - Zero alloc reads and writes - Concurrent writes out of the box - [Complete Wasm](https://godoc.org/nhooyr.io/websocket#hdr-Wasm) support - [Close handshake](https://godoc.org/nhooyr.io/websocket#Conn.Close) +- Full support of [RFC 7692](https://tools.ietf.org/html/rfc7692) permessage-deflate compression extension ## Roadmap -- [ ] Compression Extensions [#163](https://github.com/nhooyr/websocket/pull/163) - [ ] HTTP/2 [#4](https://github.com/nhooyr/websocket/issues/4) ## Examples @@ -84,22 +85,12 @@ if err != nil { c.Close(websocket.StatusNormalClosure, "") ``` -## Design justifications - -- A minimal API is easier to maintain due to less docs, tests and bugs -- A minimal API is also easier to use and learn -- Context based cancellation is more ergonomic and robust than setting deadlines -- net.Conn is never exposed as WebSocket over HTTP/2 will not have a net.Conn. -- Using net/http's Client for dialing means we do not have to reinvent dialing hooks - and configurations like other WebSocket libraries - ## Comparison -Before the comparison, I want to point out that both gorilla/websocket and gobwas/ws were -extremely useful in implementing the WebSocket protocol correctly so _big thanks_ to the -authors of both. In particular, I made sure to go through the issue tracker of gorilla/websocket -to ensure I implemented details correctly and understood how people were using WebSockets in -production. +Before the comparison, I want to point out that gorilla/websocket was extremely useful in implementing the +WebSocket protocol correctly so _big thanks_ to its authors. In particular, I made sure to go through the +issue tracker of gorilla/websocket to ensure I implemented details correctly and understood how people were +using WebSockets in production. ### gorilla/websocket @@ -121,7 +112,7 @@ more code to test, more code to document and more surface area for bugs. Moreover, nhooyr.io/websocket supports newer Go idioms such as context.Context. It also uses net/http's Client and ResponseWriter directly for WebSocket handshakes. gorilla/websocket writes its handshakes to the underlying net.Conn. -Thus it has to reinvent hooks for TLS and proxies and prevents support of HTTP/2. +Thus it has to reinvent hooks for TLS and proxies and prevents easy support of HTTP/2. Some more advantages of nhooyr.io/websocket are that it supports concurrent writes and makes it very easy to close the connection with a status code and reason. In fact, @@ -138,10 +129,14 @@ In terms of performance, the differences mostly depend on your application code. reuses message buffers out of the box if you use the wsjson and wspb subpackages. As mentioned above, nhooyr.io/websocket also supports concurrent writers. -The WebSocket masking algorithm used by this package is also [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) -faster than gorilla/websocket or gobwas/ws while using only pure safe Go. +The WebSocket masking algorithm used by this package is [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) +faster than gorilla/websocket while using only pure safe Go. -The only performance con to nhooyr.io/websocket is that it uses one extra goroutine to support +The [permessage-deflate compression extension](https://tools.ietf.org/html/rfc7692) is fully supported by this library +whereas gorilla only supports no context takeover mode. See our godoc for the differences. This will make a big +difference on bandwidth used in most use cases. + +The only performance con to nhooyr.io/websocket is that it uses a goroutine to support cancellation with context.Context. This costs 2 KB of memory which is cheap compared to the benefits. @@ -160,14 +155,15 @@ https://github.com/gobwas/ws This library has an extremely flexible API but that comes at the cost of usability and clarity. -This library is fantastic in terms of performance. The author put in significant -effort to ensure its speed and I have applied as many of its optimizations as -I could into nhooyr.io/websocket. Definitely check out his fantastic [blog post](https://medium.freecodecamp.org/million-websockets-and-go-cc58418460bb) -about performant WebSocket servers. +Due to its flexibility, it can be used in a event driven style for performance. +Definitely check out his fantastic [blog post](https://medium.freecodecamp.org/million-websockets-and-go-cc58418460bb) about performant WebSocket servers. If you want a library that gives you absolute control over everything, this is the library. -But for 99.9% of use cases, nhooyr.io/websocket will fit better. It's nearly as performant -but much easier to use. +But for 99.9% of use cases, nhooyr.io/websocket will fit better as it is both easier and +faster for normal idiomatic Go. The masking implementation is [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) +faster, the compression extensions are fully supported and as much as possible is reused by default. + +See the gorilla/websocket comparison for more performance details. ## Contributing diff --git a/accept.go b/accept.go new file mode 100644 index 00000000..5ff2ea41 --- /dev/null +++ b/accept.go @@ -0,0 +1,330 @@ +package websocket + +import ( + "bytes" + "crypto/sha1" + "encoding/base64" + "errors" + "fmt" + "io" + "net/http" + "net/textproto" + "net/url" + "strings" +) + +// AcceptOptions represents the options available to pass to Accept. +type AcceptOptions struct { + // Subprotocols lists the websocket subprotocols that Accept will negotiate with a client. + // The empty subprotocol will always be negotiated as per RFC 6455. If you would like to + // reject it, close the connection if c.Subprotocol() == "". + Subprotocols []string + + // InsecureSkipVerify disables Accept's origin verification + // behaviour. By default Accept only allows the handshake to + // succeed if the javascript that is initiating the handshake + // is on the same domain as the server. This is to prevent CSRF + // attacks when secure data is stored in a cookie as there is no same + // origin policy for WebSockets. In other words, javascript from + // any domain can perform a WebSocket dial on an arbitrary server. + // This dial will include cookies which means the arbitrary javascript + // can perform actions as the authenticated user. + // + // See https://stackoverflow.com/a/37837709/4283659 + // + // The only time you need this is if your javascript is running on a different domain + // than your WebSocket server. + // Think carefully about whether you really need this option before you use it. + // If you do, remember that if you store secure data in cookies, you wil need to verify the + // Origin header yourself otherwise you are exposing yourself to a CSRF attack. + InsecureSkipVerify bool + + // CompressionMode sets the compression mode. + // See docs on the CompressionMode type and defined constants. + CompressionMode CompressionMode +} + +// Accept accepts a WebSocket HTTP handshake from a client and upgrades the +// the connection to a WebSocket. +// +// Accept will reject the handshake if the Origin domain is not the same as the Host unless +// the InsecureSkipVerify option is set. In other words, by default it does not allow +// cross origin requests. +// +// If an error occurs, Accept will write a response with a safe error message to w. +func Accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, error) { + c, err := accept(w, r, opts) + if err != nil { + return nil, fmt.Errorf("failed to accept websocket connection: %w", err) + } + return c, nil +} + +func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, error) { + if opts == nil { + opts = &AcceptOptions{} + } + + err := verifyClientRequest(w, r) + if err != nil { + return nil, err + } + + if !opts.InsecureSkipVerify { + err = authenticateOrigin(r) + if err != nil { + http.Error(w, err.Error(), http.StatusForbidden) + return nil, err + } + } + + hj, ok := w.(http.Hijacker) + if !ok { + err = errors.New("passed ResponseWriter does not implement http.Hijacker") + http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented) + return nil, err + } + + w.Header().Set("Upgrade", "websocket") + w.Header().Set("Connection", "Upgrade") + + handleSecWebSocketKey(w, r) + + subproto := selectSubprotocol(r, opts.Subprotocols) + if subproto != "" { + w.Header().Set("Sec-WebSocket-Protocol", subproto) + } + + copts, err := acceptCompression(r, w, opts.CompressionMode) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return nil, err + } + + w.WriteHeader(http.StatusSwitchingProtocols) + + netConn, brw, err := hj.Hijack() + if err != nil { + err = fmt.Errorf("failed to hijack connection: %w", err) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return nil, err + } + + // https://github.com/golang/go/issues/32314 + b, _ := brw.Reader.Peek(brw.Reader.Buffered()) + brw.Reader.Reset(io.MultiReader(bytes.NewReader(b), netConn)) + + c := &Conn{ + subprotocol: w.Header().Get("Sec-WebSocket-Protocol"), + br: brw.Reader, + bw: brw.Writer, + closer: netConn, + copts: copts, + } + c.init() + + return c, nil +} + +func authenticateOrigin(r *http.Request) error { + origin := r.Header.Get("Origin") + if origin == "" { + return nil + } + u, err := url.Parse(origin) + if err != nil { + return fmt.Errorf("failed to parse Origin header %q: %w", origin, err) + } + if !strings.EqualFold(u.Host, r.Host) { + return fmt.Errorf("request Origin %q is not authorized for Host %q", origin, r.Host) + } + return nil +} + +func verifyClientRequest(w http.ResponseWriter, r *http.Request) error { + if !r.ProtoAtLeast(1, 1) { + err := fmt.Errorf("websocket protocol violation: handshake request must be at least HTTP/1.1: %q", r.Proto) + http.Error(w, err.Error(), http.StatusBadRequest) + return err + } + + if !headerContainsToken(r.Header, "Connection", "Upgrade") { + err := fmt.Errorf("websocket protocol violation: Connection header %q does not contain Upgrade", r.Header.Get("Connection")) + http.Error(w, err.Error(), http.StatusBadRequest) + return err + } + + if !headerContainsToken(r.Header, "Upgrade", "WebSocket") { + err := fmt.Errorf("websocket protocol violation: Upgrade header %q does not contain websocket", r.Header.Get("Upgrade")) + http.Error(w, err.Error(), http.StatusBadRequest) + return err + } + + if r.Method != "GET" { + err := fmt.Errorf("websocket protocol violation: handshake request method is not GET but %q", r.Method) + http.Error(w, err.Error(), http.StatusBadRequest) + return err + } + + if r.Header.Get("Sec-WebSocket-Version") != "13" { + err := fmt.Errorf("unsupported websocket protocol version (only 13 is supported): %q", r.Header.Get("Sec-WebSocket-Version")) + http.Error(w, err.Error(), http.StatusBadRequest) + return err + } + + if r.Header.Get("Sec-WebSocket-Key") == "" { + err := errors.New("websocket protocol violation: missing Sec-WebSocket-Key") + http.Error(w, err.Error(), http.StatusBadRequest) + return err + } + + return nil +} + +func handleSecWebSocketKey(w http.ResponseWriter, r *http.Request) { + key := r.Header.Get("Sec-WebSocket-Key") + w.Header().Set("Sec-WebSocket-Accept", secWebSocketAccept(key)) +} + +func selectSubprotocol(r *http.Request, subprotocols []string) string { + for _, sp := range subprotocols { + if headerContainsToken(r.Header, "Sec-WebSocket-Protocol", sp) { + return sp + } + } + return "" +} + +func acceptCompression(r *http.Request, w http.ResponseWriter, mode CompressionMode) (*compressionOptions, error) { + if mode == CompressionDisabled { + return nil, nil + } + + for _, ext := range websocketExtensions(r.Header) { + switch ext.name { + case "permessage-deflate": + return acceptDeflate(w, ext, mode) + case "x-webkit-deflate-frame": + return acceptWebkitDeflate(w, ext, mode) + } + } + return nil, nil +} + +func acceptDeflate(w http.ResponseWriter, ext websocketExtension, mode CompressionMode) (*compressionOptions, error) { + copts := mode.opts() + + for _, p := range ext.params { + switch p { + case "client_no_context_takeover": + copts.clientNoContextTakeover = true + continue + case "server_no_context_takeover": + copts.serverNoContextTakeover = true + continue + case "client_max_window_bits", "server-max-window-bits": + continue + } + + return nil, fmt.Errorf("unsupported permessage-deflate parameter: %q", p) + } + + copts.setHeader(w.Header()) + + return copts, nil +} + +func acceptWebkitDeflate(w http.ResponseWriter, ext websocketExtension, mode CompressionMode) (*compressionOptions, error) { + copts := mode.opts() + // The peer must explicitly request it. + copts.serverNoContextTakeover = false + + for _, p := range ext.params { + if p == "no_context_takeover" { + copts.serverNoContextTakeover = true + continue + } + + // We explicitly fail on x-webkit-deflate-frame's max_window_bits parameter instead + // of ignoring it as the draft spec is unclear. It says the server can ignore it + // but the server has no way of signalling to the client it was ignored as the parameters + // are set one way. + // Thus us ignoring it would make the client think we understood it which would cause issues. + // See https://tools.ietf.org/html/draft-tyoshino-hybi-websocket-perframe-deflate-06#section-4.1 + // + // Either way, we're only implementing this for webkit which never sends the max_window_bits + // parameter so we don't need to worry about it. + return nil, fmt.Errorf("unsupported x-webkit-deflate-frame parameter: %q", p) + } + + s := "x-webkit-deflate-frame" + if copts.clientNoContextTakeover { + s += "; no_context_takeover" + } + w.Header().Set("Sec-WebSocket-Extensions", s) + + return copts, nil +} + + +func headerContainsToken(h http.Header, key, token string) bool { + token = strings.ToLower(token) + + for _, t := range headerTokens(h, key) { + if t == token { + return true + } + } + return false +} + +type websocketExtension struct { + name string + params []string +} + +func websocketExtensions(h http.Header) []websocketExtension { + var exts []websocketExtension + extStrs := headerTokens(h, "Sec-WebSocket-Extensions") + for _, extStr := range extStrs { + if extStr == "" { + continue + } + + vals := strings.Split(extStr, ";") + for i := range vals { + vals[i] = strings.TrimSpace(vals[i]) + } + + e := websocketExtension{ + name: vals[0], + params: vals[1:], + } + + exts = append(exts, e) + } + return exts +} + +func headerTokens(h http.Header, key string) []string { + key = textproto.CanonicalMIMEHeaderKey(key) + var tokens []string + for _, v := range h[key] { + v = strings.TrimSpace(v) + for _, t := range strings.Split(v, ",") { + t = strings.ToLower(t) + tokens = append(tokens, t) + } + } + return tokens +} + +var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11") + +func secWebSocketAccept(secWebSocketKey string) string { + h := sha1.New() + h.Write([]byte(secWebSocketKey)) + h.Write(keyGUID) + + return base64.StdEncoding.EncodeToString(h.Sum(nil)) +} diff --git a/handshake_test.go b/accept_test.go similarity index 62% rename from handshake_test.go rename to accept_test.go index 82f958e0..9598cd58 100644 --- a/handshake_test.go +++ b/accept_test.go @@ -1,14 +1,9 @@ -// +build !js - package websocket import ( - "context" - "net/http" "net/http/httptest" "strings" "testing" - "time" ) func TestAccept(t *testing.T) { @@ -246,141 +241,3 @@ func Test_authenticateOrigin(t *testing.T) { }) } } - -func TestBadDials(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - url string - opts *DialOptions - }{ - { - name: "badURL", - url: "://noscheme", - }, - { - name: "badURLScheme", - url: "ftp://nhooyr.io", - }, - { - name: "badHTTPClient", - url: "ws://nhooyr.io", - opts: &DialOptions{ - HTTPClient: &http.Client{ - Timeout: time.Minute, - }, - }, - }, - { - name: "badTLS", - url: "wss://totallyfake.nhooyr.io", - }, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - defer cancel() - - _, _, err := Dial(ctx, tc.url, tc.opts) - if err == nil { - t.Fatalf("expected non nil error: %+v", err) - } - }) - } -} - -func Test_verifyServerHandshake(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - response func(w http.ResponseWriter) - success bool - }{ - { - name: "badStatus", - response: func(w http.ResponseWriter) { - w.WriteHeader(http.StatusOK) - }, - success: false, - }, - { - name: "badConnection", - response: func(w http.ResponseWriter) { - w.Header().Set("Connection", "???") - w.WriteHeader(http.StatusSwitchingProtocols) - }, - success: false, - }, - { - name: "badUpgrade", - response: func(w http.ResponseWriter) { - w.Header().Set("Connection", "Upgrade") - w.Header().Set("Upgrade", "???") - w.WriteHeader(http.StatusSwitchingProtocols) - }, - success: false, - }, - { - name: "badSecWebSocketAccept", - response: func(w http.ResponseWriter) { - w.Header().Set("Connection", "Upgrade") - w.Header().Set("Upgrade", "websocket") - w.Header().Set("Sec-WebSocket-Accept", "xd") - w.WriteHeader(http.StatusSwitchingProtocols) - }, - success: false, - }, - { - name: "badSecWebSocketProtocol", - response: func(w http.ResponseWriter) { - w.Header().Set("Connection", "Upgrade") - w.Header().Set("Upgrade", "websocket") - w.Header().Set("Sec-WebSocket-Protocol", "xd") - w.WriteHeader(http.StatusSwitchingProtocols) - }, - success: false, - }, - { - name: "success", - response: func(w http.ResponseWriter) { - w.Header().Set("Connection", "Upgrade") - w.Header().Set("Upgrade", "websocket") - w.WriteHeader(http.StatusSwitchingProtocols) - }, - success: true, - }, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - w := httptest.NewRecorder() - tc.response(w) - resp := w.Result() - - r := httptest.NewRequest("GET", "/", nil) - key, err := makeSecWebSocketKey() - if err != nil { - t.Fatal(err) - } - r.Header.Set("Sec-WebSocket-Key", key) - - if resp.Header.Get("Sec-WebSocket-Accept") == "" { - resp.Header.Set("Sec-WebSocket-Accept", secWebSocketAccept(key)) - } - - _, err = verifyServerResponse(r, resp, &DialOptions{}) - if (err == nil) != tc.success { - t.Fatalf("unexpected error: %+v", err) - } - }) - } -} diff --git a/assert_test.go b/assert_test.go index 26fd1d48..af300999 100644 --- a/assert_test.go +++ b/assert_test.go @@ -4,6 +4,7 @@ import ( "context" "math/rand" "strings" + "testing" "time" "nhooyr.io/websocket" @@ -15,36 +16,30 @@ func init() { rand.Seed(time.Now().UnixNano()) } -func assertJSONEcho(ctx context.Context, c *websocket.Conn, n int) error { +func randBytes(n int) []byte { + b := make([]byte, n) + rand.Read(b) + return b +} + +func assertJSONEcho(t *testing.T, ctx context.Context, c *websocket.Conn, n int) { exp := randString(n) err := wsjson.Write(ctx, c, exp) - if err != nil { - return err - } + assert.Success(t, err) var act interface{} err = wsjson.Read(ctx, c, &act) - if err != nil { - return err - } + assert.Success(t, err) - return assert.Equalf(exp, act, "unexpected JSON") + assert.Equalf(t, exp, act, "unexpected JSON") } -func assertJSONRead(ctx context.Context, c *websocket.Conn, exp interface{}) error { +func assertJSONRead(t *testing.T, ctx context.Context, c *websocket.Conn, exp interface{}) { var act interface{} err := wsjson.Read(ctx, c, &act) - if err != nil { - return err - } - - return assert.Equalf(exp, act, "unexpected JSON") -} + assert.Success(t, err) -func randBytes(n int) []byte { - b := make([]byte, n) - rand.Read(b) - return b + assert.Equalf(t, exp, act, "unexpected JSON") } func randString(n int) string { @@ -60,23 +55,18 @@ func randString(n int) string { return s } -func assertEcho(ctx context.Context, c *websocket.Conn, typ websocket.MessageType, n int) error { +func assertEcho(t *testing.T, ctx context.Context, c *websocket.Conn, typ websocket.MessageType, n int) { p := randBytes(n) err := c.Write(ctx, typ, p) - if err != nil { - return err - } + assert.Success(t, err) + typ2, p2, err := c.Read(ctx) - if err != nil { - return err - } - err = assert.Equalf(typ, typ2, "unexpected data type") - if err != nil { - return err - } - return assert.Equalf(p, p2, "unexpected payload") + assert.Success(t, err) + + assert.Equalf(t, typ, typ2, "unexpected data type") + assert.Equalf(t, p, p2, "unexpected payload") } -func assertSubprotocol(c *websocket.Conn, exp string) error { - return assert.Equalf(exp, c.Subprotocol(), "unexpected subprotocol") +func assertSubprotocol(t *testing.T, c *websocket.Conn, exp string) { + assert.Equalf(t, exp, c.Subprotocol(), "unexpected subprotocol") } diff --git a/ci/fmt.mk b/ci/fmt.mk index 8e61bc24..3637c1ac 100644 --- a/ci/fmt.mk +++ b/ci/fmt.mk @@ -22,4 +22,4 @@ prettier: prettier --write --print-width=120 --no-semi --trailing-comma=all --loglevel=warn $$(git ls-files "*.yml" "*.md") gen: - go generate ./... + stringer -type=Opcode,MessageType,StatusCode -output=websocket_stringer.go diff --git a/close.go b/close.go new file mode 100644 index 00000000..4f48f1b3 --- /dev/null +++ b/close.go @@ -0,0 +1,181 @@ +package websocket + +import ( + "context" + "encoding/binary" + "errors" + "fmt" + "nhooyr.io/websocket/internal/wsframe" +) + +// StatusCode represents a WebSocket status code. +// https://tools.ietf.org/html/rfc6455#section-7.4 +type StatusCode int + +// These codes were retrieved from: +// https://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number +// +// The defined constants only represent the status codes registered with IANA. +// The 4000-4999 range of status codes is reserved for arbitrary use by applications. +const ( + StatusNormalClosure StatusCode = 1000 + StatusGoingAway StatusCode = 1001 + StatusProtocolError StatusCode = 1002 + StatusUnsupportedData StatusCode = 1003 + + // 1004 is reserved and so not exported. + statusReserved StatusCode = 1004 + + // StatusNoStatusRcvd cannot be sent in a close message. + // It is reserved for when a close message is received without + // an explicit status. + StatusNoStatusRcvd StatusCode = 1005 + + // StatusAbnormalClosure is only exported for use with Wasm. + // In non Wasm Go, the returned error will indicate whether the connection was closed or not or what happened. + StatusAbnormalClosure StatusCode = 1006 + + StatusInvalidFramePayloadData StatusCode = 1007 + StatusPolicyViolation StatusCode = 1008 + StatusMessageTooBig StatusCode = 1009 + StatusMandatoryExtension StatusCode = 1010 + StatusInternalError StatusCode = 1011 + StatusServiceRestart StatusCode = 1012 + StatusTryAgainLater StatusCode = 1013 + StatusBadGateway StatusCode = 1014 + + // StatusTLSHandshake is only exported for use with Wasm. + // In non Wasm Go, the returned error will indicate whether there was a TLS handshake failure. + StatusTLSHandshake StatusCode = 1015 +) + +// CloseError represents a WebSocket close frame. +// It is returned by Conn's methods when a WebSocket close frame is received from +// the peer. +// You will need to use the https://golang.org/pkg/errors/#As function, new in Go 1.13, +// to check for this error. See the CloseError example. +type CloseError struct { + Code StatusCode + Reason string +} + +func (ce CloseError) Error() string { + return fmt.Sprintf("status = %v and reason = %q", ce.Code, ce.Reason) +} + +// CloseStatus is a convenience wrapper around errors.As to grab +// the status code from a *CloseError. If the passed error is nil +// or not a *CloseError, the returned StatusCode will be -1. +func CloseStatus(err error) StatusCode { + var ce CloseError + if errors.As(err, &ce) { + return ce.Code + } + return -1 +} + +func parseClosePayload(p []byte) (CloseError, error) { + if len(p) == 0 { + return CloseError{ + Code: StatusNoStatusRcvd, + }, nil + } + + code, reason, err := wsframe.ParseClosePayload(p) + if err != nil { + return CloseError{}, err + } + + ce := CloseError{ + Code: StatusCode(code), + Reason: reason, + } + + if !validWireCloseCode(ce.Code) { + return CloseError{}, fmt.Errorf("invalid status code %v", ce.Code) + } + + return ce, nil +} + +// See http://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number +// and https://tools.ietf.org/html/rfc6455#section-7.4.1 +func validWireCloseCode(code StatusCode) bool { + switch code { + case statusReserved, StatusNoStatusRcvd, StatusAbnormalClosure, StatusTLSHandshake: + return false + } + + if code >= StatusNormalClosure && code <= StatusBadGateway { + return true + } + if code >= 3000 && code <= 4999 { + return true + } + + return false +} + +func (ce CloseError) bytes() ([]byte, error) { + // TODO move check into frame write + if len(ce.Reason) > wsframe.MaxControlFramePayload-2 { + return nil, fmt.Errorf("reason string max is %v but got %q with length %v", wsframe.MaxControlFramePayload-2, ce.Reason, len(ce.Reason)) + } + if !validWireCloseCode(ce.Code) { + return nil, fmt.Errorf("status code %v cannot be set", ce.Code) + } + + buf := make([]byte, 2+len(ce.Reason)) + binary.BigEndian.PutUint16(buf, uint16(ce.Code)) + copy(buf[2:], ce.Reason) + return buf, nil +} + +// CloseRead will start a goroutine to read from the connection until it is closed or a data message +// is received. If a data message is received, the connection will be closed with StatusPolicyViolation. +// Since CloseRead reads from the connection, it will respond to ping, pong and close frames. +// After calling this method, you cannot read any data messages from the connection. +// The returned context will be cancelled when the connection is closed. +// +// Use this when you do not want to read data messages from the connection anymore but will +// want to write messages to it. +func (c *Conn) CloseRead(ctx context.Context) context.Context { + c.isReadClosed.Store(1) + + ctx, cancel := context.WithCancel(ctx) + go func() { + defer cancel() + // We use the unexported reader method so that we don't get the read closed error. + c.reader(ctx, true) + // Either the connection is already closed since there was a read error + // or the context was cancelled or a message was read and we should close + // the connection. + c.Close(StatusPolicyViolation, "unexpected data message") + }() + return ctx +} + +// SetReadLimit sets the max number of bytes to read for a single message. +// It applies to the Reader and Read methods. +// +// By default, the connection has a message read limit of 32768 bytes. +// +// When the limit is hit, the connection will be closed with StatusMessageTooBig. +func (c *Conn) SetReadLimit(n int64) { + c.msgReadLimit.Store(n) +} + +func (c *Conn) setCloseErr(err error) { + c.closeErrOnce.Do(func() { + c.closeErr = fmt.Errorf("websocket closed: %w", err) + }) +} + +func (c *Conn) isClosed() bool { + select { + case <-c.closed: + return true + default: + return false + } +} diff --git a/close_test.go b/close_test.go new file mode 100644 index 00000000..78096d7e --- /dev/null +++ b/close_test.go @@ -0,0 +1,196 @@ +package websocket + +import ( + "github.com/google/go-cmp/cmp" + "io" + "math" + "nhooyr.io/websocket/internal/assert" + "nhooyr.io/websocket/internal/wsframe" + "strings" + "testing" +) + +func TestCloseError(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + ce CloseError + success bool + }{ + { + name: "normal", + ce: CloseError{ + Code: StatusNormalClosure, + Reason: strings.Repeat("x", wsframe.MaxControlFramePayload-2), + }, + success: true, + }, + { + name: "bigReason", + ce: CloseError{ + Code: StatusNormalClosure, + Reason: strings.Repeat("x", wsframe.MaxControlFramePayload-1), + }, + success: false, + }, + { + name: "bigCode", + ce: CloseError{ + Code: math.MaxUint16, + Reason: strings.Repeat("x", wsframe.MaxControlFramePayload-2), + }, + success: false, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + _, err := tc.ce.bytes() + if (err == nil) != tc.success { + t.Fatalf("unexpected error value: %+v", err) + } + }) + } +} + +func Test_parseClosePayload(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + p []byte + success bool + ce CloseError + }{ + { + name: "normal", + p: append([]byte{0x3, 0xE8}, []byte("hello")...), + success: true, + ce: CloseError{ + Code: StatusNormalClosure, + Reason: "hello", + }, + }, + { + name: "nothing", + success: true, + ce: CloseError{ + Code: StatusNoStatusRcvd, + }, + }, + { + name: "oneByte", + p: []byte{0}, + success: false, + }, + { + name: "badStatusCode", + p: []byte{0x17, 0x70}, + success: false, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ce, err := parseClosePayload(tc.p) + if (err == nil) != tc.success { + t.Fatalf("unexpected expected error value: %+v", err) + } + + if tc.success && tc.ce != ce { + t.Fatalf("unexpected close error: %v", cmp.Diff(tc.ce, ce)) + } + }) + } +} + +func Test_validWireCloseCode(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + code StatusCode + valid bool + }{ + { + name: "normal", + code: StatusNormalClosure, + valid: true, + }, + { + name: "noStatus", + code: StatusNoStatusRcvd, + valid: false, + }, + { + name: "3000", + code: 3000, + valid: true, + }, + { + name: "4999", + code: 4999, + valid: true, + }, + { + name: "unknown", + code: 5000, + valid: false, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + if valid := validWireCloseCode(tc.code); tc.valid != valid { + t.Fatalf("expected %v for %v but got %v", tc.valid, tc.code, valid) + } + }) + } +} + +func TestCloseStatus(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + in error + exp StatusCode + }{ + { + name: "nil", + in: nil, + exp: -1, + }, + { + name: "io.EOF", + in: io.EOF, + exp: -1, + }, + { + name: "StatusInternalError", + in: CloseError{ + Code: StatusInternalError, + }, + exp: StatusInternalError, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + assert.Equalf(t, tc.exp, CloseStatus(tc.in), "unexpected close status") + }) + } +} diff --git a/compress.go b/compress.go new file mode 100644 index 00000000..5b5fdce5 --- /dev/null +++ b/compress.go @@ -0,0 +1,78 @@ +// +build !js + +package websocket + +import ( + "net/http" +) + +// CompressionMode controls the modes available RFC 7692's deflate extension. +// See https://tools.ietf.org/html/rfc7692 +// +// A compatibility layer is implemented for the older deflate-frame extension used +// by safari. See https://tools.ietf.org/html/draft-tyoshino-hybi-websocket-perframe-deflate-06 +// It will work the same in every way except that we cannot signal to the peer we +// want to use no context takeover on our side, we can only signal that they should. +type CompressionMode int + +const ( + // CompressionContextTakeover uses a flate.Reader and flate.Writer per connection. + // This enables reusing the sliding window from previous messages. + // As most WebSocket protocols are repetitive, this is the default. + // + // The message will only be compressed if greater than or equal to 128 bytes. + // + // If the peer negotiates NoContextTakeover on the client or server side, it will be + // used instead as this is required by the RFC. + CompressionContextTakeover CompressionMode = iota + + // CompressionNoContextTakeover grabs a new flate.Reader and flate.Writer as needed + // for every message. This applies to both server and client side. + // + // This means less efficient compression as the sliding window from previous messages + // will not be used but the memory overhead will be much lower if the connections + // are long lived and seldom used. + // + // The message will only be compressed if greater than or equal to 512 bytes. + CompressionNoContextTakeover + + // CompressionDisabled disables the deflate extension. + // + // Use this if you are using a predominantly binary protocol with very + // little duplication in between messages or CPU and memory are more + // important than bandwidth. + CompressionDisabled +) + +func (m CompressionMode) opts() *compressionOptions { + if m == CompressionDisabled { + return nil + } + return &compressionOptions{ + clientNoContextTakeover: m == CompressionNoContextTakeover, + serverNoContextTakeover: m == CompressionNoContextTakeover, + } +} + +type compressionOptions struct { + clientNoContextTakeover bool + serverNoContextTakeover bool +} + +func (copts *compressionOptions) setHeader(h http.Header) { + s := "permessage-deflate" + if copts.clientNoContextTakeover { + s += "; client_no_context_takeover" + } + if copts.serverNoContextTakeover { + s += "; server_no_context_takeover" + } + h.Set("Sec-WebSocket-Extensions", s) +} + +// These bytes are required to get flate.Reader to return. +// They are removed when sending to avoid the overhead as +// WebSocket framing tell's when the message has ended but then +// we need to add them back otherwise flate.Reader keeps +// trying to return more bytes. +const deflateMessageTail = "\x00\x00\xff\xff" diff --git a/conn.go b/conn.go index 32dfa81b..791d9b4c 100644 --- a/conn.go +++ b/conn.go @@ -13,13 +13,28 @@ import ( "io" "io/ioutil" "log" + "nhooyr.io/websocket/internal/atomicint" + "nhooyr.io/websocket/internal/wsframe" "runtime" "strconv" + "strings" "sync" "sync/atomic" "time" - "nhooyr.io/websocket/internal/bpool" + "nhooyr.io/websocket/internal/bufpool" +) + +// MessageType represents the type of a WebSocket message. +// See https://tools.ietf.org/html/rfc6455#section-5.6 +type MessageType int + +// MessageType constants. +const ( + // MessageText is for UTF-8 encoded text messages like JSON. + MessageText MessageType = iota + 1 + // MessageBinary is for binary messages like Protobufs. + MessageBinary ) // Conn represents a WebSocket connection. @@ -36,20 +51,20 @@ import ( // This applies to the Read methods in the wsjson/wspb subpackages as well. type Conn struct { subprotocol string - br *bufio.Reader + fw *flate.Writer bw *bufio.Writer // writeBuf is used for masking, its the buffer in bufio.Writer. // Only used by the client for masking the bytes in the buffer. writeBuf []byte closer io.Closer client bool - copts *CompressionOptions + copts *compressionOptions closeOnce sync.Once closeErrOnce sync.Once closeErr error closed chan struct{} - closing *atomicInt64 + closing *atomicint.Int64 closeReceived error // messageWriter state. @@ -61,35 +76,18 @@ type Conn struct { writeHeaderBuf []byte writeHeader *header // read limit for a message in bytes. - msgReadLimit *atomicInt64 + msgReadLimit *atomicint.Int64 // Used to ensure a previous writer is not used after being closed. activeWriter atomic.Value // messageWriter state. writeMsgOpcode opcode writeMsgCtx context.Context - readMsgLeft int64 - - // Used to ensure the previous reader is read till EOF before allowing - // a new one. - activeReader *messageReader - // readFrameLock is acquired to read from bw. - readFrameLock chan struct{} - isReadClosed *atomicInt64 - readHeaderBuf []byte - controlPayloadBuf []byte - readLock chan struct{} - - // messageReader state. - readerMsgCtx context.Context - readerMsgHeader header - readerFrameEOF bool - readerMaskKey uint32 setReadTimeout chan context.Context setWriteTimeout chan context.Context - pingCounter *atomicInt64 + pingCounter *atomicint.Int64 activePingsMu sync.Mutex activePings map[string]chan<- struct{} @@ -98,9 +96,9 @@ type Conn struct { func (c *Conn) init() { c.closed = make(chan struct{}) - c.closing = &atomicInt64{} + c.closing = &atomicint.Int64{} - c.msgReadLimit = &atomicInt64{} + c.msgReadLimit = &atomicint.Int64{} c.msgReadLimit.Store(32768) c.writeMsgLock = make(chan struct{}, 1) @@ -108,17 +106,18 @@ func (c *Conn) init() { c.readFrameLock = make(chan struct{}, 1) c.readLock = make(chan struct{}, 1) + c.payloadReader = framePayloadReader{c} c.setReadTimeout = make(chan context.Context) c.setWriteTimeout = make(chan context.Context) - c.pingCounter = &atomicInt64{} + c.pingCounter = &atomicint.Int64{} c.activePings = make(map[string]chan<- struct{}) c.writeHeaderBuf = makeWriteHeaderBuf() c.writeHeader = &header{} c.readHeaderBuf = makeReadHeaderBuf() - c.isReadClosed = &atomicInt64{} + c.isReadClosed = &atomicint.Int64{} c.controlPayloadBuf = make([]byte, maxControlFramePayload) runtime.SetFinalizer(c, func(c *Conn) { @@ -127,6 +126,15 @@ func (c *Conn) init() { c.logf = log.Printf + if c.copts != nil { + if !c.readNoContextTakeOver() { + c.fr = getFlateReader(c.payloadReader) + } + if !c.writeNoContextTakeOver() { + c.fw = getFlateWriter(c.bw) + } + } + go c.timeoutLoop() } @@ -148,19 +156,26 @@ func (c *Conn) close(err error) { // closeErr. c.closer.Close() - // See comment on bufioReaderPool in handshake.go + // By acquiring the locks, we ensure no goroutine will touch the bufio reader or writer + // and we can safely return them. + // Whenever a caller holds this lock and calls close, it ensures to release the lock to prevent + // a deadlock. + // As of now, this is in writeFrame, readFramePayload and readHeader. + c.readFrameLock <- struct{}{} if c.client { - // By acquiring the locks, we ensure no goroutine will touch the bufio reader or writer - // and we can safely return them. - // Whenever a caller holds this lock and calls close, it ensures to release the lock to prevent - // a deadlock. - // As of now, this is in writeFrame, readFramePayload and readHeader. - c.readFrameLock <- struct{}{} returnBufioReader(c.br) + } + if c.fr != nil { + putFlateReader(c.fr) + } - c.writeFrameLock <- struct{}{} + c.writeFrameLock <- struct{}{} + if c.client { returnBufioWriter(c.bw) } + if c.fw != nil { + putFlateWriter(c.fw) + } }) } @@ -230,7 +245,7 @@ func (c *Conn) readTillMsg(ctx context.Context) (header, error) { return header{}, err } - if h.rsv1 || h.rsv2 || h.rsv3 { + if (h.rsv1 && (c.copts == nil || h.opcode.controlOp() || h.opcode == opContinuation)) || h.rsv2 || h.rsv3 { err := fmt.Errorf("received header with rsv bits set: %v:%v:%v", h.rsv1, h.rsv2, h.rsv3) c.exportedClose(StatusProtocolError, err.Error(), false) return header{}, err @@ -448,6 +463,13 @@ func (c *Conn) reader(ctx context.Context, lock bool) (MessageType, io.Reader, e c.readerMsgCtx = ctx c.readerMsgHeader = h + + c.readerPayloadCompressed = h.rsv1 + + if c.readerPayloadCompressed { + c.readerCompressTail.Reset(deflateMessageTail) + } + c.readerFrameEOF = false c.readerMaskKey = h.maskKey c.readMsgLeft = c.msgReadLimit.Load() @@ -456,9 +478,67 @@ func (c *Conn) reader(ctx context.Context, lock bool) (MessageType, io.Reader, e c: c, } c.activeReader = r + if c.readerPayloadCompressed && c.readNoContextTakeOver() { + c.fr = getFlateReader(c.payloadReader) + } return MessageType(h.opcode), r, nil } +type framePayloadReader struct { + c *Conn +} + +func (r framePayloadReader) Read(p []byte) (int, error) { + if r.c.readerFrameEOF { + if r.c.readerPayloadCompressed && r.c.readerMsgHeader.fin { + n, _ := r.c.readerCompressTail.Read(p) + return n, nil + } + + h, err := r.c.readTillMsg(r.c.readerMsgCtx) + if err != nil { + return 0, err + } + + if h.opcode != opContinuation { + err := errors.New("received new data message without finishing the previous message") + r.c.exportedClose(StatusProtocolError, err.Error(), false) + return 0, err + } + + r.c.readerMsgHeader = h + r.c.readerFrameEOF = false + r.c.readerMaskKey = h.maskKey + } + + h := r.c.readerMsgHeader + if int64(len(p)) > h.payloadLength { + p = p[:h.payloadLength] + } + + n, err := r.c.readFramePayload(r.c.readerMsgCtx, p) + + h.payloadLength -= int64(n) + if h.masked { + r.c.readerMaskKey = mask(r.c.readerMaskKey, p) + } + r.c.readerMsgHeader = h + + if err != nil { + return n, err + } + + if h.payloadLength == 0 { + r.c.readerFrameEOF = true + + if h.fin && !r.c.readerPayloadCompressed { + return n, io.EOF + } + } + + return n, nil +} + // messageReader enables reading a data frame from the WebSocket connection. type messageReader struct { c *Conn @@ -521,51 +601,27 @@ func (r *messageReader) read(p []byte, lock bool) (int, error) { p = p[:r.c.readMsgLeft] } - if r.c.readerFrameEOF { - h, err := r.c.readTillMsg(r.c.readerMsgCtx) - if err != nil { - return 0, err - } - - if h.opcode != opContinuation { - err := errors.New("received new data message without finishing the previous message") - r.c.exportedClose(StatusProtocolError, err.Error(), false) - return 0, err - } - - r.c.readerMsgHeader = h - r.c.readerFrameEOF = false - r.c.readerMaskKey = h.maskKey + pr := io.Reader(r.c.payloadReader) + if r.c.readerPayloadCompressed { + pr = r.c.fr } - h := r.c.readerMsgHeader - if int64(len(p)) > h.payloadLength { - p = p[:h.payloadLength] - } - - n, err := r.c.readFramePayload(r.c.readerMsgCtx, p) + n, err := pr.Read(p) - h.payloadLength -= int64(n) r.c.readMsgLeft -= int64(n) - if h.masked { - r.c.readerMaskKey = mask(r.c.readerMaskKey, p) - } - r.c.readerMsgHeader = h - if err != nil { - return n, err - } - - if h.payloadLength == 0 { - r.c.readerFrameEOF = true - - if h.fin { - r.c.activeReader = nil - return n, io.EOF + if r.c.readerFrameEOF && r.c.readerMsgHeader.fin { + if r.c.readerPayloadCompressed && r.c.readNoContextTakeOver() { + putFlateReader(r.c.fr) + r.c.fr = nil + } + r.c.activeReader = nil + if err == nil { + err = io.EOF } } - return n, nil + return n, err } func (c *Conn) readFramePayload(ctx context.Context, p []byte) (_ int, err error) { @@ -971,10 +1027,10 @@ func (c *Conn) waitClose() error { return c.closeReceived } - b := bpool.Get() + b := bufpool.Get() buf := b.Bytes() buf = buf[:cap(buf)] - defer bpool.Put(b) + defer bufpool.Put(b) for { if c.activeReader == nil || c.readerFrameEOF { @@ -1065,40 +1121,21 @@ func (c *Conn) extractBufioWriterBuf(w io.Writer) { c.bw.Reset(w) } -var flateWriterPoolsMu sync.Mutex -var flateWriterPools = make(map[int]*sync.Pool) - -func getFlateWriterPool(level int) *sync.Pool { - flateWriterPoolsMu.Lock() - defer flateWriterPoolsMu.Unlock() - - p, ok := flateWriterPools[level] - if !ok { - p = &sync.Pool{ - New: func() interface{} { - w, err := flate.NewWriter(nil, level) - if err != nil { - panic("websocket: unexpected error from flate.NewWriter: " + err.Error()) - } - return w - }, - } - flateWriterPools[level] = p - } - - return p +var flateWriterPool = &sync.Pool{ + New: func() interface{} { + w, _ := flate.NewWriter(nil, flate.BestSpeed) + return w + }, } -func getFlateWriter(w io.Writer, level int) *flate.Writer { - p := getFlateWriterPool(level) - fw := p.Get().(*flate.Writer) +func getFlateWriter(w io.Writer) *flate.Writer { + fw := flateWriterPool.Get().(*flate.Writer) fw.Reset(w) return fw } -func putFlateWriter(w *flate.Writer, level int) { - p := getFlateWriterPool(level) - p.Put(w) +func putFlateWriter(w *flate.Writer) { + flateWriterPool.Put(w) } var flateReaderPool = &sync.Pool{ @@ -1107,12 +1144,60 @@ var flateReaderPool = &sync.Pool{ }, } -func getFlateReader(r flate.Reader) io.ReadCloser { - fr := flateReaderPool.Get().(io.ReadCloser) +func getFlateReader(r io.Reader) io.Reader { + fr := flateReaderPool.Get().(io.Reader) fr.(flate.Resetter).Reset(r, nil) return fr } -func putFlateReader(fr io.ReadCloser) { +func putFlateReader(fr io.Reader) { flateReaderPool.Put(fr) } + +func (c *Conn) writeNoContextTakeOver() bool { + return c.client && c.copts.clientNoContextTakeover || !c.client && c.copts.serverNoContextTakeover +} + +func (c *Conn) readNoContextTakeOver() bool { + return !c.client && c.copts.clientNoContextTakeover || c.client && c.copts.serverNoContextTakeover +} + +type trimLastFourBytesWriter struct { + w io.Writer + tail []byte +} + +func (w *trimLastFourBytesWriter) Write(p []byte) (int, error) { + extra := len(w.tail) + len(p) - 4 + + if extra <= 0 { + w.tail = append(w.tail, p...) + return len(p), nil + } + + // Now we need to write as many extra bytes as we can from the previous tail. + if extra > len(w.tail) { + extra = len(w.tail) + } + if extra > 0 { + _, err := w.Write(w.tail[:extra]) + if err != nil { + return 0, err + } + w.tail = w.tail[extra:] + } + + // If p is less than or equal to 4 bytes, + // all of it is is part of the tail. + if len(p) <= 4 { + w.tail = append(w.tail, p...) + return len(p), nil + } + + // Otherwise, only the last 4 bytes are. + w.tail = append(w.tail, p[len(p)-4:]...) + + p = p[:len(p)-4] + n, err := w.w.Write(p) + return n + 4, err +} diff --git a/dial.go b/dial.go new file mode 100644 index 00000000..10088681 --- /dev/null +++ b/dial.go @@ -0,0 +1,219 @@ +package websocket + +import ( + "bytes" + "context" + "crypto/rand" + "encoding/base64" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "nhooyr.io/websocket/internal/bufpool" + "strings" +) + +// DialOptions represents the options available to pass to Dial. +type DialOptions struct { + // HTTPClient is the http client used for the handshake. + // Its Transport must return writable bodies + // for WebSocket handshakes. + // http.Transport does this correctly beginning with Go 1.12. + HTTPClient *http.Client + + // HTTPHeader specifies the HTTP headers included in the handshake request. + HTTPHeader http.Header + + // Subprotocols lists the subprotocols to negotiate with the server. + Subprotocols []string + + // See docs on CompressionMode. + CompressionMode CompressionMode +} + +// Dial performs a WebSocket handshake on the given url with the given options. +// The response is the WebSocket handshake response from the server. +// If an error occurs, the returned response may be non nil. However, you can only +// read the first 1024 bytes of its body. +// +// You never need to close the resp.Body yourself. +// +// This function requires at least Go 1.12 to succeed as it uses a new feature +// in net/http to perform WebSocket handshakes and get a writable body +// from the transport. See https://github.com/golang/go/issues/26937#issuecomment-415855861 +func Dial(ctx context.Context, u string, opts *DialOptions) (*Conn, *http.Response, error) { + c, r, err := dial(ctx, u, opts) + if err != nil { + return nil, r, fmt.Errorf("failed to websocket dial: %w", err) + } + return c, r, nil +} + +func (opts *DialOptions) fill() (*DialOptions, error) { + if opts == nil { + opts = &DialOptions{} + } else { + opts = &*opts + } + + if opts.HTTPClient == nil { + opts.HTTPClient = http.DefaultClient + } + if opts.HTTPClient.Timeout > 0 { + return nil, fmt.Errorf("use context for cancellation instead of http.Client.Timeout; see https://github.com/nhooyr/websocket/issues/67") + } + if opts.HTTPHeader == nil { + opts.HTTPHeader = http.Header{} + } + + return opts, nil +} + +func dial(ctx context.Context, u string, opts *DialOptions) (_ *Conn, _ *http.Response, err error) { + opts, err = opts.fill() + if err != nil { + return nil, nil, err + } + + parsedURL, err := url.Parse(u) + if err != nil { + return nil, nil, fmt.Errorf("failed to parse url: %w", err) + } + + switch parsedURL.Scheme { + case "ws": + parsedURL.Scheme = "http" + case "wss": + parsedURL.Scheme = "https" + default: + return nil, nil, fmt.Errorf("unexpected url scheme: %q", parsedURL.Scheme) + } + + req, _ := http.NewRequest("GET", parsedURL.String(), nil) + req = req.WithContext(ctx) + req.Header = opts.HTTPHeader + req.Header.Set("Connection", "Upgrade") + req.Header.Set("Upgrade", "websocket") + req.Header.Set("Sec-WebSocket-Version", "13") + secWebSocketKey, err := secWebSocketKey() + if err != nil { + return nil, nil, fmt.Errorf("failed to generate Sec-WebSocket-Key: %w", err) + } + req.Header.Set("Sec-WebSocket-Key", secWebSocketKey) + if len(opts.Subprotocols) > 0 { + req.Header.Set("Sec-WebSocket-Protocol", strings.Join(opts.Subprotocols, ",")) + } + copts := opts.CompressionMode.opts() + copts.setHeader(req.Header) + + resp, err := opts.HTTPClient.Do(req) + if err != nil { + return nil, nil, fmt.Errorf("failed to send handshake request: %w", err) + } + defer func() { + if err != nil { + // We read a bit of the body for easier debugging. + r := io.LimitReader(resp.Body, 1024) + b, _ := ioutil.ReadAll(r) + resp.Body.Close() + resp.Body = ioutil.NopCloser(bytes.NewReader(b)) + } + }() + + copts, err = verifyServerResponse(req, resp, opts) + if err != nil { + return nil, resp, err + } + + rwc, ok := resp.Body.(io.ReadWriteCloser) + if !ok { + return nil, resp, fmt.Errorf("response body is not a io.ReadWriteCloser: %T", rwc) + } + + c := &Conn{ + subprotocol: resp.Header.Get("Sec-WebSocket-Protocol"), + br: bufpool.GetReader(rwc), + bw: bufpool.GetWriter(rwc), + closer: rwc, + client: true, + copts: copts, + } + c.extractBufioWriterBuf(rwc) + c.init() + + return c, resp, nil +} + +func secWebSocketKey() (string, error) { + b := make([]byte, 16) + _, err := io.ReadFull(rand.Reader, b) + if err != nil { + return "", fmt.Errorf("failed to read random data from rand.Reader: %w", err) + } + return base64.StdEncoding.EncodeToString(b), nil +} + +func verifyServerResponse(r *http.Request, resp *http.Response, opts *DialOptions) (*compressionOptions, error) { + if resp.StatusCode != http.StatusSwitchingProtocols { + return nil, fmt.Errorf("expected handshake response status code %v but got %v", http.StatusSwitchingProtocols, resp.StatusCode) + } + + if !headerContainsToken(resp.Header, "Connection", "Upgrade") { + return nil, fmt.Errorf("websocket protocol violation: Connection header %q does not contain Upgrade", resp.Header.Get("Connection")) + } + + if !headerContainsToken(resp.Header, "Upgrade", "WebSocket") { + return nil, fmt.Errorf("websocket protocol violation: Upgrade header %q does not contain websocket", resp.Header.Get("Upgrade")) + } + + if resp.Header.Get("Sec-WebSocket-Accept") != secWebSocketAccept(r.Header.Get("Sec-WebSocket-Key")) { + return nil, fmt.Errorf("websocket protocol violation: invalid Sec-WebSocket-Accept %q, key %q", + resp.Header.Get("Sec-WebSocket-Accept"), + r.Header.Get("Sec-WebSocket-Key"), + ) + } + + if proto := resp.Header.Get("Sec-WebSocket-Protocol"); proto != "" && !headerContainsToken(r.Header, "Sec-WebSocket-Protocol", proto) { + return nil, fmt.Errorf("websocket protocol violation: unexpected Sec-WebSocket-Protocol from server: %q", proto) + } + + copts, err := verifyServerExtensions(resp.Header, opts.CompressionMode) + if err != nil { + return nil, err + } + + return copts, nil +} + +func verifyServerExtensions(h http.Header, mode CompressionMode) (*compressionOptions, error) { + exts := websocketExtensions(h) + if len(exts) == 0 { + return nil, nil + } + + ext := exts[0] + if ext.name != "permessage-deflate" { + return nil, fmt.Errorf("unexpected extension from server: %q", ext) + } + + if len(exts) > 1 { + return nil, fmt.Errorf("unexpected extra extensions from server: %+v", exts[1:]) + } + + copts := mode.opts() + for _, p := range ext.params { + switch p { + case "client_no_context_takeover": + copts.clientNoContextTakeover = true + continue + case "server_no_context_takeover": + copts.serverNoContextTakeover = true + continue + } + + return nil, fmt.Errorf("unsupported permessage-deflate parameter: %q", p) + } + + return copts, nil +} diff --git a/dial_test.go b/dial_test.go new file mode 100644 index 00000000..391aa1ce --- /dev/null +++ b/dial_test.go @@ -0,0 +1,149 @@ +// +build !js + +package websocket + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + "time" +) + +func TestBadDials(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + url string + opts *DialOptions + }{ + { + name: "badURL", + url: "://noscheme", + }, + { + name: "badURLScheme", + url: "ftp://nhooyr.io", + }, + { + name: "badHTTPClient", + url: "ws://nhooyr.io", + opts: &DialOptions{ + HTTPClient: &http.Client{ + Timeout: time.Minute, + }, + }, + }, + { + name: "badTLS", + url: "wss://totallyfake.nhooyr.io", + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + _, _, err := Dial(ctx, tc.url, tc.opts) + if err == nil { + t.Fatalf("expected non nil error: %+v", err) + } + }) + } +} + +func Test_verifyServerHandshake(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + response func(w http.ResponseWriter) + success bool + }{ + { + name: "badStatus", + response: func(w http.ResponseWriter) { + w.WriteHeader(http.StatusOK) + }, + success: false, + }, + { + name: "badConnection", + response: func(w http.ResponseWriter) { + w.Header().Set("Connection", "???") + w.WriteHeader(http.StatusSwitchingProtocols) + }, + success: false, + }, + { + name: "badUpgrade", + response: func(w http.ResponseWriter) { + w.Header().Set("Connection", "Upgrade") + w.Header().Set("Upgrade", "???") + w.WriteHeader(http.StatusSwitchingProtocols) + }, + success: false, + }, + { + name: "badSecWebSocketAccept", + response: func(w http.ResponseWriter) { + w.Header().Set("Connection", "Upgrade") + w.Header().Set("Upgrade", "websocket") + w.Header().Set("Sec-WebSocket-Accept", "xd") + w.WriteHeader(http.StatusSwitchingProtocols) + }, + success: false, + }, + { + name: "badSecWebSocketProtocol", + response: func(w http.ResponseWriter) { + w.Header().Set("Connection", "Upgrade") + w.Header().Set("Upgrade", "websocket") + w.Header().Set("Sec-WebSocket-Protocol", "xd") + w.WriteHeader(http.StatusSwitchingProtocols) + }, + success: false, + }, + { + name: "success", + response: func(w http.ResponseWriter) { + w.Header().Set("Connection", "Upgrade") + w.Header().Set("Upgrade", "websocket") + w.WriteHeader(http.StatusSwitchingProtocols) + }, + success: true, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + w := httptest.NewRecorder() + tc.response(w) + resp := w.Result() + + r := httptest.NewRequest("GET", "/", nil) + key, err := secWebSocketKey() + if err != nil { + t.Fatal(err) + } + r.Header.Set("Sec-WebSocket-Key", key) + + if resp.Header.Get("Sec-WebSocket-Accept") == "" { + resp.Header.Set("Sec-WebSocket-Accept", secWebSocketAccept(key)) + } + + _, err = verifyServerResponse(r, resp, &DialOptions{}) + if (err == nil) != tc.success { + t.Fatalf("unexpected error: %+v", err) + } + }) + } +} diff --git a/doc.go b/doc.go index 804665fb..5285a780 100644 --- a/doc.go +++ b/doc.go @@ -1,6 +1,6 @@ // +build !js -// Package websocket is a minimal and idiomatic implementation of the WebSocket protocol. +// Package websocket implements the RFC 6455 WebSocket protocol. // // https://tools.ietf.org/html/rfc6455 // diff --git a/frame.go b/frame.go deleted file mode 100644 index e4bf931a..00000000 --- a/frame.go +++ /dev/null @@ -1,445 +0,0 @@ -package websocket - -import ( - "encoding/binary" - "errors" - "fmt" - "io" - "math" - "math/bits" -) - -//go:generate stringer -type=opcode,MessageType,StatusCode -output=frame_stringer.go - -// opcode represents a WebSocket Opcode. -type opcode int - -// opcode constants. -const ( - opContinuation opcode = iota - opText - opBinary - // 3 - 7 are reserved for further non-control frames. - _ - _ - _ - _ - _ - opClose - opPing - opPong - // 11-16 are reserved for further control frames. -) - -func (o opcode) controlOp() bool { - switch o { - case opClose, opPing, opPong: - return true - } - return false -} - -// MessageType represents the type of a WebSocket message. -// See https://tools.ietf.org/html/rfc6455#section-5.6 -type MessageType int - -// MessageType constants. -const ( - // MessageText is for UTF-8 encoded text messages like JSON. - MessageText MessageType = iota + 1 - // MessageBinary is for binary messages like Protobufs. - MessageBinary -) - -// First byte contains fin, rsv1, rsv2, rsv3. -// Second byte contains mask flag and payload len. -// Next 8 bytes are the maximum extended payload length. -// Last 4 bytes are the mask key. -// https://tools.ietf.org/html/rfc6455#section-5.2 -const maxHeaderSize = 1 + 1 + 8 + 4 - -// header represents a WebSocket frame header. -// See https://tools.ietf.org/html/rfc6455#section-5.2 -type header struct { - fin bool - rsv1 bool - rsv2 bool - rsv3 bool - opcode opcode - - payloadLength int64 - - masked bool - maskKey uint32 -} - -func makeWriteHeaderBuf() []byte { - return make([]byte, maxHeaderSize) -} - -// bytes returns the bytes of the header. -// See https://tools.ietf.org/html/rfc6455#section-5.2 -func writeHeader(b []byte, h header) []byte { - if b == nil { - b = makeWriteHeaderBuf() - } - - b = b[:2] - b[0] = 0 - - if h.fin { - b[0] |= 1 << 7 - } - if h.rsv1 { - b[0] |= 1 << 6 - } - if h.rsv2 { - b[0] |= 1 << 5 - } - if h.rsv3 { - b[0] |= 1 << 4 - } - - b[0] |= byte(h.opcode) - - switch { - case h.payloadLength < 0: - panic(fmt.Sprintf("websocket: invalid header: negative length: %v", h.payloadLength)) - case h.payloadLength <= 125: - b[1] = byte(h.payloadLength) - case h.payloadLength <= math.MaxUint16: - b[1] = 126 - b = b[:len(b)+2] - binary.BigEndian.PutUint16(b[len(b)-2:], uint16(h.payloadLength)) - default: - b[1] = 127 - b = b[:len(b)+8] - binary.BigEndian.PutUint64(b[len(b)-8:], uint64(h.payloadLength)) - } - - if h.masked { - b[1] |= 1 << 7 - b = b[:len(b)+4] - binary.LittleEndian.PutUint32(b[len(b)-4:], h.maskKey) - } - - return b -} - -func makeReadHeaderBuf() []byte { - return make([]byte, maxHeaderSize-2) -} - -// readHeader reads a header from the reader. -// See https://tools.ietf.org/html/rfc6455#section-5.2 -func readHeader(b []byte, r io.Reader) (header, error) { - if b == nil { - b = makeReadHeaderBuf() - } - - // We read the first two bytes first so that we know - // exactly how long the header is. - b = b[:2] - _, err := io.ReadFull(r, b) - if err != nil { - return header{}, err - } - - var h header - h.fin = b[0]&(1<<7) != 0 - h.rsv1 = b[0]&(1<<6) != 0 - h.rsv2 = b[0]&(1<<5) != 0 - h.rsv3 = b[0]&(1<<4) != 0 - - h.opcode = opcode(b[0] & 0xf) - - var extra int - - h.masked = b[1]&(1<<7) != 0 - if h.masked { - extra += 4 - } - - payloadLength := b[1] &^ (1 << 7) - switch { - case payloadLength < 126: - h.payloadLength = int64(payloadLength) - case payloadLength == 126: - extra += 2 - case payloadLength == 127: - extra += 8 - } - - if extra == 0 { - return h, nil - } - - b = b[:extra] - _, err = io.ReadFull(r, b) - if err != nil { - return header{}, err - } - - switch { - case payloadLength == 126: - h.payloadLength = int64(binary.BigEndian.Uint16(b)) - b = b[2:] - case payloadLength == 127: - h.payloadLength = int64(binary.BigEndian.Uint64(b)) - if h.payloadLength < 0 { - return header{}, fmt.Errorf("header with negative payload length: %v", h.payloadLength) - } - b = b[8:] - } - - if h.masked { - h.maskKey = binary.LittleEndian.Uint32(b) - } - - return h, nil -} - -// StatusCode represents a WebSocket status code. -// https://tools.ietf.org/html/rfc6455#section-7.4 -type StatusCode int - -// These codes were retrieved from: -// https://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number -// -// The defined constants only represent the status codes registered with IANA. -// The 4000-4999 range of status codes is reserved for arbitrary use by applications. -const ( - StatusNormalClosure StatusCode = 1000 - StatusGoingAway StatusCode = 1001 - StatusProtocolError StatusCode = 1002 - StatusUnsupportedData StatusCode = 1003 - - // 1004 is reserved and so not exported. - statusReserved StatusCode = 1004 - - // StatusNoStatusRcvd cannot be sent in a close message. - // It is reserved for when a close message is received without - // an explicit status. - StatusNoStatusRcvd StatusCode = 1005 - - // StatusAbnormalClosure is only exported for use with Wasm. - // In non Wasm Go, the returned error will indicate whether the connection was closed or not or what happened. - StatusAbnormalClosure StatusCode = 1006 - - StatusInvalidFramePayloadData StatusCode = 1007 - StatusPolicyViolation StatusCode = 1008 - StatusMessageTooBig StatusCode = 1009 - StatusMandatoryExtension StatusCode = 1010 - StatusInternalError StatusCode = 1011 - StatusServiceRestart StatusCode = 1012 - StatusTryAgainLater StatusCode = 1013 - StatusBadGateway StatusCode = 1014 - - // StatusTLSHandshake is only exported for use with Wasm. - // In non Wasm Go, the returned error will indicate whether there was a TLS handshake failure. - StatusTLSHandshake StatusCode = 1015 -) - -// CloseError represents a WebSocket close frame. -// It is returned by Conn's methods when a WebSocket close frame is received from -// the peer. -// You will need to use the https://golang.org/pkg/errors/#As function, new in Go 1.13, -// to check for this error. See the CloseError example. -type CloseError struct { - Code StatusCode - Reason string -} - -func (ce CloseError) Error() string { - return fmt.Sprintf("status = %v and reason = %q", ce.Code, ce.Reason) -} - -// CloseStatus is a convenience wrapper around errors.As to grab -// the status code from a *CloseError. If the passed error is nil -// or not a *CloseError, the returned StatusCode will be -1. -func CloseStatus(err error) StatusCode { - var ce CloseError - if errors.As(err, &ce) { - return ce.Code - } - return -1 -} - -func parseClosePayload(p []byte) (CloseError, error) { - if len(p) == 0 { - return CloseError{ - Code: StatusNoStatusRcvd, - }, nil - } - - if len(p) < 2 { - return CloseError{}, fmt.Errorf("close payload %q too small, cannot even contain the 2 byte status code", p) - } - - ce := CloseError{ - Code: StatusCode(binary.BigEndian.Uint16(p)), - Reason: string(p[2:]), - } - - if !validWireCloseCode(ce.Code) { - return CloseError{}, fmt.Errorf("invalid status code %v", ce.Code) - } - - return ce, nil -} - -// See http://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number -// and https://tools.ietf.org/html/rfc6455#section-7.4.1 -func validWireCloseCode(code StatusCode) bool { - switch code { - case statusReserved, StatusNoStatusRcvd, StatusAbnormalClosure, StatusTLSHandshake: - return false - } - - if code >= StatusNormalClosure && code <= StatusBadGateway { - return true - } - if code >= 3000 && code <= 4999 { - return true - } - - return false -} - -const maxControlFramePayload = 125 - -func (ce CloseError) bytes() ([]byte, error) { - if len(ce.Reason) > maxControlFramePayload-2 { - return nil, fmt.Errorf("reason string max is %v but got %q with length %v", maxControlFramePayload-2, ce.Reason, len(ce.Reason)) - } - if !validWireCloseCode(ce.Code) { - return nil, fmt.Errorf("status code %v cannot be set", ce.Code) - } - - buf := make([]byte, 2+len(ce.Reason)) - binary.BigEndian.PutUint16(buf, uint16(ce.Code)) - copy(buf[2:], ce.Reason) - return buf, nil -} - -// fastMask applies the WebSocket masking algorithm to p -// with the given key. -// See https://tools.ietf.org/html/rfc6455#section-5.3 -// -// The returned value is the correctly rotated key to -// to continue to mask/unmask the message. -// -// It is optimized for LittleEndian and expects the key -// to be in little endian. -// -// See https://github.com/golang/go/issues/31586 -func mask(key uint32, b []byte) uint32 { - if len(b) >= 8 { - key64 := uint64(key)<<32 | uint64(key) - - // At some point in the future we can clean these unrolled loops up. - // See https://github.com/golang/go/issues/31586#issuecomment-487436401 - - // Then we xor until b is less than 128 bytes. - for len(b) >= 128 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^key64) - v = binary.LittleEndian.Uint64(b[8:16]) - binary.LittleEndian.PutUint64(b[8:16], v^key64) - v = binary.LittleEndian.Uint64(b[16:24]) - binary.LittleEndian.PutUint64(b[16:24], v^key64) - v = binary.LittleEndian.Uint64(b[24:32]) - binary.LittleEndian.PutUint64(b[24:32], v^key64) - v = binary.LittleEndian.Uint64(b[32:40]) - binary.LittleEndian.PutUint64(b[32:40], v^key64) - v = binary.LittleEndian.Uint64(b[40:48]) - binary.LittleEndian.PutUint64(b[40:48], v^key64) - v = binary.LittleEndian.Uint64(b[48:56]) - binary.LittleEndian.PutUint64(b[48:56], v^key64) - v = binary.LittleEndian.Uint64(b[56:64]) - binary.LittleEndian.PutUint64(b[56:64], v^key64) - v = binary.LittleEndian.Uint64(b[64:72]) - binary.LittleEndian.PutUint64(b[64:72], v^key64) - v = binary.LittleEndian.Uint64(b[72:80]) - binary.LittleEndian.PutUint64(b[72:80], v^key64) - v = binary.LittleEndian.Uint64(b[80:88]) - binary.LittleEndian.PutUint64(b[80:88], v^key64) - v = binary.LittleEndian.Uint64(b[88:96]) - binary.LittleEndian.PutUint64(b[88:96], v^key64) - v = binary.LittleEndian.Uint64(b[96:104]) - binary.LittleEndian.PutUint64(b[96:104], v^key64) - v = binary.LittleEndian.Uint64(b[104:112]) - binary.LittleEndian.PutUint64(b[104:112], v^key64) - v = binary.LittleEndian.Uint64(b[112:120]) - binary.LittleEndian.PutUint64(b[112:120], v^key64) - v = binary.LittleEndian.Uint64(b[120:128]) - binary.LittleEndian.PutUint64(b[120:128], v^key64) - b = b[128:] - } - - // Then we xor until b is less than 64 bytes. - for len(b) >= 64 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^key64) - v = binary.LittleEndian.Uint64(b[8:16]) - binary.LittleEndian.PutUint64(b[8:16], v^key64) - v = binary.LittleEndian.Uint64(b[16:24]) - binary.LittleEndian.PutUint64(b[16:24], v^key64) - v = binary.LittleEndian.Uint64(b[24:32]) - binary.LittleEndian.PutUint64(b[24:32], v^key64) - v = binary.LittleEndian.Uint64(b[32:40]) - binary.LittleEndian.PutUint64(b[32:40], v^key64) - v = binary.LittleEndian.Uint64(b[40:48]) - binary.LittleEndian.PutUint64(b[40:48], v^key64) - v = binary.LittleEndian.Uint64(b[48:56]) - binary.LittleEndian.PutUint64(b[48:56], v^key64) - v = binary.LittleEndian.Uint64(b[56:64]) - binary.LittleEndian.PutUint64(b[56:64], v^key64) - b = b[64:] - } - - // Then we xor until b is less than 32 bytes. - for len(b) >= 32 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^key64) - v = binary.LittleEndian.Uint64(b[8:16]) - binary.LittleEndian.PutUint64(b[8:16], v^key64) - v = binary.LittleEndian.Uint64(b[16:24]) - binary.LittleEndian.PutUint64(b[16:24], v^key64) - v = binary.LittleEndian.Uint64(b[24:32]) - binary.LittleEndian.PutUint64(b[24:32], v^key64) - b = b[32:] - } - - // Then we xor until b is less than 16 bytes. - for len(b) >= 16 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^key64) - v = binary.LittleEndian.Uint64(b[8:16]) - binary.LittleEndian.PutUint64(b[8:16], v^key64) - b = b[16:] - } - - // Then we xor until b is less than 8 bytes. - for len(b) >= 8 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^key64) - b = b[8:] - } - } - - // Then we xor until b is less than 4 bytes. - for len(b) >= 4 { - v := binary.LittleEndian.Uint32(b) - binary.LittleEndian.PutUint32(b, v^key) - b = b[4:] - } - - // xor remaining bytes. - for i := range b { - b[i] ^= byte(key) - key = bits.RotateLeft32(key, -8) - } - - return key -} diff --git a/frame_test.go b/frame_test.go deleted file mode 100644 index 571e68fc..00000000 --- a/frame_test.go +++ /dev/null @@ -1,457 +0,0 @@ -// +build !js - -package websocket - -import ( - "bytes" - "encoding/binary" - "io" - "math" - "math/bits" - "math/rand" - "strconv" - "strings" - "testing" - "time" - _ "unsafe" - - "github.com/gobwas/ws" - "github.com/google/go-cmp/cmp" - _ "github.com/gorilla/websocket" - - "nhooyr.io/websocket/internal/assert" -) - -func init() { - rand.Seed(time.Now().UnixNano()) -} - -func randBool() bool { - return rand.Intn(1) == 0 -} - -func TestHeader(t *testing.T) { - t.Parallel() - - t.Run("eof", func(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - bytes []byte - }{ - { - "start", - []byte{0xff}, - }, - { - "middle", - []byte{0xff, 0xff, 0xff}, - }, - } - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - b := bytes.NewBuffer(tc.bytes) - _, err := readHeader(nil, b) - if io.ErrUnexpectedEOF != err { - t.Fatalf("expected %v but got: %v", io.ErrUnexpectedEOF, err) - } - }) - } - }) - - t.Run("writeNegativeLength", func(t *testing.T) { - t.Parallel() - - defer func() { - r := recover() - if r == nil { - t.Fatal("failed to induce panic in writeHeader with negative payload length") - } - }() - - writeHeader(nil, header{ - payloadLength: -1, - }) - }) - - t.Run("readNegativeLength", func(t *testing.T) { - t.Parallel() - - b := writeHeader(nil, header{ - payloadLength: 1<<16 + 1, - }) - - // Make length negative - b[2] |= 1 << 7 - - r := bytes.NewReader(b) - _, err := readHeader(nil, r) - if err == nil { - t.Fatalf("unexpected error value: %+v", err) - } - }) - - t.Run("lengths", func(t *testing.T) { - t.Parallel() - - lengths := []int{ - 124, - 125, - 126, - 4096, - 16384, - 65535, - 65536, - 65537, - 131072, - } - - for _, n := range lengths { - n := n - t.Run(strconv.Itoa(n), func(t *testing.T) { - t.Parallel() - - testHeader(t, header{ - payloadLength: int64(n), - }) - }) - } - }) - - t.Run("fuzz", func(t *testing.T) { - t.Parallel() - - for i := 0; i < 10000; i++ { - h := header{ - fin: randBool(), - rsv1: randBool(), - rsv2: randBool(), - rsv3: randBool(), - opcode: opcode(rand.Intn(1 << 4)), - - masked: randBool(), - payloadLength: rand.Int63(), - } - - if h.masked { - h.maskKey = rand.Uint32() - } - - testHeader(t, h) - } - }) -} - -func testHeader(t *testing.T, h header) { - b := writeHeader(nil, h) - r := bytes.NewReader(b) - h2, err := readHeader(nil, r) - if err != nil { - t.Logf("header: %#v", h) - t.Logf("bytes: %b", b) - t.Fatalf("failed to read header: %v", err) - } - - if !cmp.Equal(h, h2, cmp.AllowUnexported(header{})) { - t.Logf("header: %#v", h) - t.Logf("bytes: %b", b) - t.Fatalf("parsed and read header differ: %v", cmp.Diff(h, h2, cmp.AllowUnexported(header{}))) - } -} - -func TestCloseError(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - ce CloseError - success bool - }{ - { - name: "normal", - ce: CloseError{ - Code: StatusNormalClosure, - Reason: strings.Repeat("x", maxControlFramePayload-2), - }, - success: true, - }, - { - name: "bigReason", - ce: CloseError{ - Code: StatusNormalClosure, - Reason: strings.Repeat("x", maxControlFramePayload-1), - }, - success: false, - }, - { - name: "bigCode", - ce: CloseError{ - Code: math.MaxUint16, - Reason: strings.Repeat("x", maxControlFramePayload-2), - }, - success: false, - }, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - _, err := tc.ce.bytes() - if (err == nil) != tc.success { - t.Fatalf("unexpected error value: %+v", err) - } - }) - } -} - -func Test_parseClosePayload(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - p []byte - success bool - ce CloseError - }{ - { - name: "normal", - p: append([]byte{0x3, 0xE8}, []byte("hello")...), - success: true, - ce: CloseError{ - Code: StatusNormalClosure, - Reason: "hello", - }, - }, - { - name: "nothing", - success: true, - ce: CloseError{ - Code: StatusNoStatusRcvd, - }, - }, - { - name: "oneByte", - p: []byte{0}, - success: false, - }, - { - name: "badStatusCode", - p: []byte{0x17, 0x70}, - success: false, - }, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - ce, err := parseClosePayload(tc.p) - if (err == nil) != tc.success { - t.Fatalf("unexpected expected error value: %+v", err) - } - - if tc.success && tc.ce != ce { - t.Fatalf("unexpected close error: %v", cmp.Diff(tc.ce, ce)) - } - }) - } -} - -func Test_validWireCloseCode(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - code StatusCode - valid bool - }{ - { - name: "normal", - code: StatusNormalClosure, - valid: true, - }, - { - name: "noStatus", - code: StatusNoStatusRcvd, - valid: false, - }, - { - name: "3000", - code: 3000, - valid: true, - }, - { - name: "4999", - code: 4999, - valid: true, - }, - { - name: "unknown", - code: 5000, - valid: false, - }, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - if valid := validWireCloseCode(tc.code); tc.valid != valid { - t.Fatalf("expected %v for %v but got %v", tc.valid, tc.code, valid) - } - }) - } -} - -func Test_mask(t *testing.T) { - t.Parallel() - - key := []byte{0xa, 0xb, 0xc, 0xff} - key32 := binary.LittleEndian.Uint32(key) - p := []byte{0xa, 0xb, 0xc, 0xf2, 0xc} - gotKey32 := mask(key32, p) - - if exp := []byte{0, 0, 0, 0x0d, 0x6}; !cmp.Equal(exp, p) { - t.Fatalf("unexpected mask: %v", cmp.Diff(exp, p)) - } - - if exp := bits.RotateLeft32(key32, -8); !cmp.Equal(exp, gotKey32) { - t.Fatalf("unexpected mask key: %v", cmp.Diff(exp, gotKey32)) - } -} - -func basicMask(maskKey [4]byte, pos int, b []byte) int { - for i := range b { - b[i] ^= maskKey[pos&3] - pos++ - } - return pos & 3 -} - -//go:linkname gorillaMaskBytes github.com/gorilla/websocket.maskBytes -func gorillaMaskBytes(key [4]byte, pos int, b []byte) int - -func Benchmark_mask(b *testing.B) { - sizes := []int{ - 2, - 3, - 4, - 8, - 16, - 32, - 128, - 512, - 4096, - 16384, - } - - fns := []struct { - name string - fn func(b *testing.B, key [4]byte, p []byte) - }{ - { - name: "basic", - fn: func(b *testing.B, key [4]byte, p []byte) { - for i := 0; i < b.N; i++ { - basicMask(key, 0, p) - } - }, - }, - - { - name: "nhooyr", - fn: func(b *testing.B, key [4]byte, p []byte) { - key32 := binary.LittleEndian.Uint32(key[:]) - b.ResetTimer() - - for i := 0; i < b.N; i++ { - mask(key32, p) - } - }, - }, - { - name: "gorilla", - fn: func(b *testing.B, key [4]byte, p []byte) { - for i := 0; i < b.N; i++ { - gorillaMaskBytes(key, 0, p) - } - }, - }, - { - name: "gobwas", - fn: func(b *testing.B, key [4]byte, p []byte) { - for i := 0; i < b.N; i++ { - ws.Cipher(p, key, 0) - } - }, - }, - } - - var key [4]byte - _, err := rand.Read(key[:]) - if err != nil { - b.Fatalf("failed to populate mask key: %v", err) - } - - for _, size := range sizes { - p := make([]byte, size) - - b.Run(strconv.Itoa(size), func(b *testing.B) { - for _, fn := range fns { - b.Run(fn.name, func(b *testing.B) { - b.SetBytes(int64(size)) - - fn.fn(b, key, p) - }) - } - }) - } -} - -func TestCloseStatus(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - in error - exp StatusCode - }{ - { - name: "nil", - in: nil, - exp: -1, - }, - { - name: "io.EOF", - in: io.EOF, - exp: -1, - }, - { - name: "StatusInternalError", - in: CloseError{ - Code: StatusInternalError, - }, - exp: StatusInternalError, - }, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - err := assert.Equalf(tc.exp, CloseStatus(tc.in), "unexpected close status") - if err != nil { - t.Fatal(err) - } - }) - } -} diff --git a/handshake.go b/handshake.go deleted file mode 100644 index 03331039..00000000 --- a/handshake.go +++ /dev/null @@ -1,637 +0,0 @@ -// +build !js - -package websocket - -import ( - "bufio" - "bytes" - "context" - "crypto/rand" - "crypto/sha1" - "encoding/base64" - "errors" - "fmt" - "io" - "io/ioutil" - "net/http" - "net/textproto" - "net/url" - "strings" - "sync" -) - -// AcceptOptions represents the options available to pass to Accept. -type AcceptOptions struct { - // Subprotocols lists the websocket subprotocols that Accept will negotiate with a client. - // The empty subprotocol will always be negotiated as per RFC 6455. If you would like to - // reject it, close the connection if c.Subprotocol() == "". - Subprotocols []string - - // InsecureSkipVerify disables Accept's origin verification - // behaviour. By default Accept only allows the handshake to - // succeed if the javascript that is initiating the handshake - // is on the same domain as the server. This is to prevent CSRF - // attacks when secure data is stored in a cookie as there is no same - // origin policy for WebSockets. In other words, javascript from - // any domain can perform a WebSocket dial on an arbitrary server. - // This dial will include cookies which means the arbitrary javascript - // can perform actions as the authenticated user. - // - // See https://stackoverflow.com/a/37837709/4283659 - // - // The only time you need this is if your javascript is running on a different domain - // than your WebSocket server. - // Think carefully about whether you really need this option before you use it. - // If you do, remember that if you store secure data in cookies, you wil need to verify the - // Origin header yourself otherwise you are exposing yourself to a CSRF attack. - InsecureSkipVerify bool - - // Compression sets the compression options. - // By default, compression is disabled. - // See docs on the CompressionOptions type. - Compression *CompressionOptions -} - -func verifyClientRequest(w http.ResponseWriter, r *http.Request) error { - if !r.ProtoAtLeast(1, 1) { - err := fmt.Errorf("websocket protocol violation: handshake request must be at least HTTP/1.1: %q", r.Proto) - http.Error(w, err.Error(), http.StatusBadRequest) - return err - } - - if !headerContainsToken(r.Header, "Connection", "Upgrade") { - err := fmt.Errorf("websocket protocol violation: Connection header %q does not contain Upgrade", r.Header.Get("Connection")) - http.Error(w, err.Error(), http.StatusBadRequest) - return err - } - - if !headerContainsToken(r.Header, "Upgrade", "WebSocket") { - err := fmt.Errorf("websocket protocol violation: Upgrade header %q does not contain websocket", r.Header.Get("Upgrade")) - http.Error(w, err.Error(), http.StatusBadRequest) - return err - } - - if r.Method != "GET" { - err := fmt.Errorf("websocket protocol violation: handshake request method is not GET but %q", r.Method) - http.Error(w, err.Error(), http.StatusBadRequest) - return err - } - - if r.Header.Get("Sec-WebSocket-Version") != "13" { - err := fmt.Errorf("unsupported websocket protocol version (only 13 is supported): %q", r.Header.Get("Sec-WebSocket-Version")) - http.Error(w, err.Error(), http.StatusBadRequest) - return err - } - - if r.Header.Get("Sec-WebSocket-Key") == "" { - err := errors.New("websocket protocol violation: missing Sec-WebSocket-Key") - http.Error(w, err.Error(), http.StatusBadRequest) - return err - } - - return nil -} - -// Accept accepts a WebSocket handshake from a client and upgrades the -// the connection to a WebSocket. -// -// Accept will reject the handshake if the Origin domain is not the same as the Host unless -// the InsecureSkipVerify option is set. In other words, by default it does not allow -// cross origin requests. -// -// If an error occurs, Accept will always write an appropriate response so you do not -// have to. -func Accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, error) { - c, err := accept(w, r, opts) - if err != nil { - return nil, fmt.Errorf("failed to accept websocket connection: %w", err) - } - return c, nil -} - -func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, error) { - if opts == nil { - opts = &AcceptOptions{} - } - - err := verifyClientRequest(w, r) - if err != nil { - return nil, err - } - - if !opts.InsecureSkipVerify { - err = authenticateOrigin(r) - if err != nil { - http.Error(w, err.Error(), http.StatusForbidden) - return nil, err - } - } - - hj, ok := w.(http.Hijacker) - if !ok { - err = errors.New("passed ResponseWriter does not implement http.Hijacker") - http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented) - return nil, err - } - - w.Header().Set("Upgrade", "websocket") - w.Header().Set("Connection", "Upgrade") - - handleSecWebSocketKey(w, r) - - subproto := selectSubprotocol(r, opts.Subprotocols) - if subproto != "" { - w.Header().Set("Sec-WebSocket-Protocol", subproto) - } - - var copts *CompressionOptions - if opts.Compression != nil { - copts, err = negotiateCompression(r.Header, opts.Compression) - if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) - return nil, err - } - if copts != nil { - copts.setHeader(w.Header(), false) - } - } - - w.WriteHeader(http.StatusSwitchingProtocols) - - netConn, brw, err := hj.Hijack() - if err != nil { - err = fmt.Errorf("failed to hijack connection: %w", err) - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return nil, err - } - - // https://github.com/golang/go/issues/32314 - b, _ := brw.Reader.Peek(brw.Reader.Buffered()) - brw.Reader.Reset(io.MultiReader(bytes.NewReader(b), netConn)) - - c := &Conn{ - subprotocol: w.Header().Get("Sec-WebSocket-Protocol"), - br: brw.Reader, - bw: brw.Writer, - closer: netConn, - copts: copts, - } - c.init() - - return c, nil -} - -func headerContainsToken(h http.Header, key, token string) bool { - key = textproto.CanonicalMIMEHeaderKey(key) - - token = strings.ToLower(token) - match := func(t string) bool { - return t == token - } - - for _, v := range h[key] { - if searchHeaderTokens(v, match) { - return true - } - } - - return false -} - -// readCompressionExtensionHeader extracts compression extension info from h. -// The standard says we should support multiple compression extension configurations -// from the client but we don't need to as there is only a single deflate extension -// and we support every configuration without error so we only need to check the first -// and thus preferred configuration. -func readCompressionExtensionHeader(h http.Header) (xWebkitDeflateFrame bool, params []string, ok bool) { - match := func(t string) bool { - vals := strings.Split(t, ";") - for i := range vals { - vals[i] = strings.TrimSpace(vals[i]) - } - params = vals[1:] - - if vals[0] == "permessage-deflate" { - return true - } - - // See https://bugs.webkit.org/show_bug.cgi?id=115504 - if vals[0] == "x-webkit-deflate-frame" { - xWebkitDeflateFrame = true - return true - } - - return false - } - - key := textproto.CanonicalMIMEHeaderKey("Sec-WebSocket-Extensions") - for _, v := range h[key] { - if searchHeaderTokens(v, match) { - return xWebkitDeflateFrame, params, true - } - } - - return false, nil, false -} - -func searchHeaderTokens(v string, match func(val string) bool) bool { - v = strings.ToLower(v) - v = strings.TrimSpace(v) - - for _, v2 := range strings.Split(v, ",") { - v2 = strings.TrimSpace(v2) - if match(v2) { - return true - } - } - - return false -} - -func selectSubprotocol(r *http.Request, subprotocols []string) string { - for _, sp := range subprotocols { - if headerContainsToken(r.Header, "Sec-WebSocket-Protocol", sp) { - return sp - } - } - return "" -} - -var keyGUID = []byte("258EAFA5-E914-47DA-95CA-C5AB0DC85B11") - -func handleSecWebSocketKey(w http.ResponseWriter, r *http.Request) { - key := r.Header.Get("Sec-WebSocket-Key") - w.Header().Set("Sec-WebSocket-Accept", secWebSocketAccept(key)) -} - -func secWebSocketAccept(secWebSocketKey string) string { - h := sha1.New() - h.Write([]byte(secWebSocketKey)) - h.Write(keyGUID) - - return base64.StdEncoding.EncodeToString(h.Sum(nil)) -} - -func authenticateOrigin(r *http.Request) error { - origin := r.Header.Get("Origin") - if origin == "" { - return nil - } - u, err := url.Parse(origin) - if err != nil { - return fmt.Errorf("failed to parse Origin header %q: %w", origin, err) - } - if !strings.EqualFold(u.Host, r.Host) { - return fmt.Errorf("request Origin %q is not authorized for Host %q", origin, r.Host) - } - return nil -} - -// DialOptions represents the options available to pass to Dial. -type DialOptions struct { - // HTTPClient is the http client used for the handshake. - // Its Transport must return writable bodies - // for WebSocket handshakes. - // http.Transport does this correctly beginning with Go 1.12. - HTTPClient *http.Client - - // HTTPHeader specifies the HTTP headers included in the handshake request. - HTTPHeader http.Header - - // Subprotocols lists the subprotocols to negotiate with the server. - Subprotocols []string - - // Compression sets the compression options. - // By default, compression is disabled. - // See docs on the CompressionOptions type. - Compression *CompressionOptions -} - -// CompressionOptions describes the available compression options. -// -// See https://tools.ietf.org/html/rfc7692 -// -// The NoContextTakeover variables control whether a flate.Writer or flate.Reader is allocated -// for every connection (context takeover) versus shared from a pool (no context takeover). -// -// The advantage to context takeover is more efficient compression as the sliding window from previous -// messages will be used instead of being reset between every message. -// -// The advantage to no context takeover is that the flate structures are allocated as needed -// and shared between connections instead of giving each connection a fixed flate.Writer and -// flate.Reader. -// -// See https://www.igvita.com/2013/11/27/configuring-and-optimizing-websocket-compression. -// -// Enabling compression will increase memory and CPU usage and should -// be profiled before enabling in production. -// See https://github.com/gorilla/websocket/issues/203 -// -// This API is experimental and subject to change. -type CompressionOptions struct { - // ClientNoContextTakeover controls whether the client should use context takeover. - // See docs on CompressionOptions for discussion regarding context takeover. - // - // If set by the server, will guarantee that the client does not use context takeover. - ClientNoContextTakeover bool - - // ServerNoContextTakeover controls whether the server should use context takeover. - // See docs on CompressionOptions for discussion regarding context takeover. - // - // If set by the client, will guarantee that the server does not use context takeover. - ServerNoContextTakeover bool - - // Level controls the compression level used. - // Defaults to flate.BestSpeed. - Level int - - // Threshold controls the minimum message size in bytes before compression is used. - // Must not be greater than 4096 as that is the write buffer's size. - // - // Defaults to 256. - Threshold int - - // This is used for supporting Safari as it still uses x-webkit-deflate-frame. - // See negotiateCompression. - xWebkitDeflateFrame bool -} - -// Dial performs a WebSocket handshake on the given url with the given options. -// The response is the WebSocket handshake response from the server. -// If an error occurs, the returned response may be non nil. However, you can only -// read the first 1024 bytes of its body. -// -// You never need to close the resp.Body yourself. -// -// This function requires at least Go 1.12 to succeed as it uses a new feature -// in net/http to perform WebSocket handshakes and get a writable body -// from the transport. See https://github.com/golang/go/issues/26937#issuecomment-415855861 -func Dial(ctx context.Context, u string, opts *DialOptions) (*Conn, *http.Response, error) { - c, r, err := dial(ctx, u, opts) - if err != nil { - return nil, r, fmt.Errorf("failed to websocket dial: %w", err) - } - return c, r, nil -} - -func (opts *DialOptions) ensure() (*DialOptions, error) { - if opts == nil { - opts = &DialOptions{} - } else { - opts = &*opts - } - - if opts.HTTPClient == nil { - opts.HTTPClient = http.DefaultClient - } - if opts.HTTPClient.Timeout > 0 { - return nil, fmt.Errorf("use context for cancellation instead of http.Client.Timeout; see https://github.com/nhooyr/websocket/issues/67") - } - if opts.HTTPHeader == nil { - opts.HTTPHeader = http.Header{} - } - - return opts, nil -} - -func dial(ctx context.Context, u string, opts *DialOptions) (_ *Conn, _ *http.Response, err error) { - opts, err = opts.ensure() - if err != nil { - return nil, nil, err - } - - parsedURL, err := url.Parse(u) - if err != nil { - return nil, nil, fmt.Errorf("failed to parse url: %w", err) - } - - switch parsedURL.Scheme { - case "ws": - parsedURL.Scheme = "http" - case "wss": - parsedURL.Scheme = "https" - default: - return nil, nil, fmt.Errorf("unexpected url scheme: %q", parsedURL.Scheme) - } - - req, _ := http.NewRequest("GET", parsedURL.String(), nil) - req = req.WithContext(ctx) - req.Header = opts.HTTPHeader - req.Header.Set("Connection", "Upgrade") - req.Header.Set("Upgrade", "websocket") - req.Header.Set("Sec-WebSocket-Version", "13") - secWebSocketKey, err := makeSecWebSocketKey() - if err != nil { - return nil, nil, fmt.Errorf("failed to generate Sec-WebSocket-Key: %w", err) - } - req.Header.Set("Sec-WebSocket-Key", secWebSocketKey) - if len(opts.Subprotocols) > 0 { - req.Header.Set("Sec-WebSocket-Protocol", strings.Join(opts.Subprotocols, ",")) - } - if opts.Compression != nil { - opts.Compression.setHeader(req.Header, true) - } - - resp, err := opts.HTTPClient.Do(req) - if err != nil { - return nil, nil, fmt.Errorf("failed to send handshake request: %w", err) - } - defer func() { - if err != nil { - // We read a bit of the body for easier debugging. - r := io.LimitReader(resp.Body, 1024) - b, _ := ioutil.ReadAll(r) - resp.Body.Close() - resp.Body = ioutil.NopCloser(bytes.NewReader(b)) - } - }() - - copts, err := verifyServerResponse(req, resp, opts) - if err != nil { - return nil, resp, err - } - - rwc, ok := resp.Body.(io.ReadWriteCloser) - if !ok { - return nil, resp, fmt.Errorf("response body is not a io.ReadWriteCloser: %T", resp.Body) - } - - c := &Conn{ - subprotocol: resp.Header.Get("Sec-WebSocket-Protocol"), - br: getBufioReader(rwc), - bw: getBufioWriter(rwc), - closer: rwc, - client: true, - copts: copts, - } - c.extractBufioWriterBuf(rwc) - c.init() - - return c, resp, nil -} - -func verifyServerResponse(r *http.Request, resp *http.Response, opts *DialOptions) (*CompressionOptions, error) { - if resp.StatusCode != http.StatusSwitchingProtocols { - return nil, fmt.Errorf("expected handshake response status code %v but got %v", http.StatusSwitchingProtocols, resp.StatusCode) - } - - if !headerContainsToken(resp.Header, "Connection", "Upgrade") { - return nil, fmt.Errorf("websocket protocol violation: Connection header %q does not contain Upgrade", resp.Header.Get("Connection")) - } - - if !headerContainsToken(resp.Header, "Upgrade", "WebSocket") { - return nil, fmt.Errorf("websocket protocol violation: Upgrade header %q does not contain websocket", resp.Header.Get("Upgrade")) - } - - if resp.Header.Get("Sec-WebSocket-Accept") != secWebSocketAccept(r.Header.Get("Sec-WebSocket-Key")) { - return nil, fmt.Errorf("websocket protocol violation: invalid Sec-WebSocket-Accept %q, key %q", - resp.Header.Get("Sec-WebSocket-Accept"), - r.Header.Get("Sec-WebSocket-Key"), - ) - } - - if proto := resp.Header.Get("Sec-WebSocket-Protocol"); proto != "" && !headerContainsToken(r.Header, "Sec-WebSocket-Protocol", proto) { - return nil, fmt.Errorf("websocket protocol violation: unexpected Sec-WebSocket-Protocol from server: %q", proto) - } - - var copts *CompressionOptions - if opts.Compression != nil { - var err error - copts, err = negotiateCompression(resp.Header, opts.Compression) - if err != nil { - return nil, err - } - } - - return copts, nil -} - -// The below pools can only be used by the client because http.Hijacker will always -// have a bufio.Reader/Writer for us so it doesn't make sense to use a pool on top. - -var bufioReaderPool = sync.Pool{ - New: func() interface{} { - return bufio.NewReader(nil) - }, -} - -func getBufioReader(r io.Reader) *bufio.Reader { - br := bufioReaderPool.Get().(*bufio.Reader) - br.Reset(r) - return br -} - -func returnBufioReader(br *bufio.Reader) { - bufioReaderPool.Put(br) -} - -var bufioWriterPool = sync.Pool{ - New: func() interface{} { - return bufio.NewWriter(nil) - }, -} - -func getBufioWriter(w io.Writer) *bufio.Writer { - bw := bufioWriterPool.Get().(*bufio.Writer) - bw.Reset(w) - return bw -} - -func returnBufioWriter(bw *bufio.Writer) { - bufioWriterPool.Put(bw) -} - -func makeSecWebSocketKey() (string, error) { - b := make([]byte, 16) - _, err := io.ReadFull(rand.Reader, b) - if err != nil { - return "", fmt.Errorf("failed to read random data from rand.Reader: %w", err) - } - return base64.StdEncoding.EncodeToString(b), nil -} - -func negotiateCompression(h http.Header, copts *CompressionOptions) (*CompressionOptions, error) { - xWebkitDeflateFrame, params, ok := readCompressionExtensionHeader(h) - if !ok { - return nil, nil - } - - // Ensures our changes do not modify the real compression options. - copts = &*copts - copts.xWebkitDeflateFrame = xWebkitDeflateFrame - - // We are the client if the header contains the accept header, meaning its from the server. - client := h.Get("Sec-WebSocket-Accept") == "" - - if copts.xWebkitDeflateFrame { - // The other endpoint dictates whether or not we can - // use context takeover on our side. We cannot force it. - // Likewise, we tell the other side so we can force that. - if client { - copts.ClientNoContextTakeover = false - } else { - copts.ServerNoContextTakeover = false - } - } - - for _, p := range params { - switch p { - case "client_no_context_takeover": - copts.ClientNoContextTakeover = true - continue - case "server_no_context_takeover": - copts.ServerNoContextTakeover = true - continue - case "client_max_window_bits", "server-max-window-bits": - if !client { - // If we are the server, we are allowed to ignore these parameters. - // However, if we are the client, we must obey them but because of - // https://github.com/golang/go/issues/3155 we cannot. - continue - } - case "no_context_takeover": - if copts.xWebkitDeflateFrame { - if client { - copts.ClientNoContextTakeover = true - } else { - copts.ServerNoContextTakeover = true - } - continue - } - - // We explicitly fail on x-webkit-deflate-frame's max_window_bits parameter instead - // of ignoring it as the draft spec is unclear. It says the server can ignore it - // but the server has no way of signalling to the client it was ignored as parameters - // are set one way. - // Thus us ignoring it would make the client think we understood it which would cause issues. - // See https://tools.ietf.org/html/draft-tyoshino-hybi-websocket-perframe-deflate-06#section-4.1 - // - // Either way, we're only implementing this for webkit which never sends the max_window_bits - // parameter so we don't need to worry about it. - } - - return nil, fmt.Errorf("unsupported permessage-deflate parameter: %q", p) - } - - return copts, nil -} - -func (copts *CompressionOptions) setHeader(h http.Header, client bool) { - var s string - if !copts.xWebkitDeflateFrame { - s := "permessage-deflate" - if copts.ClientNoContextTakeover { - s += "; client_no_context_takeover" - } - if copts.ServerNoContextTakeover { - s += "; server_no_context_takeover" - } - } else { - s = "x-webkit-deflate-frame" - // We can only set no context takeover for the peer. - if client && copts.ServerNoContextTakeover || !client && copts.ClientNoContextTakeover { - s += "; no_context_takeover" - } - } - h.Set("Sec-WebSocket-Extensions", s) -} diff --git a/internal/assert/assert.go b/internal/assert/assert.go index e57abfd9..372d5465 100644 --- a/internal/assert/assert.go +++ b/internal/assert/assert.go @@ -1,8 +1,8 @@ package assert import ( - "fmt" "reflect" + "testing" "github.com/google/go-cmp/cmp" ) @@ -53,11 +53,15 @@ func structTypes(v reflect.Value, m map[reflect.Type]struct{}) { } } -// Equalf compares exp to act and if they are not equal, returns -// an error describing an error. -func Equalf(exp, act interface{}, f string, v ...interface{}) error { - if diff := cmpDiff(exp, act); diff != "" { - return fmt.Errorf(f+": %v", append(v, diff)...) +func Equalf(t *testing.T, exp, act interface{}, f string, v ...interface{}) { + t.Helper() + diff := cmpDiff(exp, act) + if diff != "" { + t.Fatalf(f+": %v", append(v, diff)...) } - return nil +} + +func Success(t *testing.T, err error) { + t.Helper() + Equalf(t, error(nil), err, "unexpected failure") } diff --git a/internal/atomicint/atomicint.go b/internal/atomicint/atomicint.go new file mode 100644 index 00000000..668b3b4b --- /dev/null +++ b/internal/atomicint/atomicint.go @@ -0,0 +1,32 @@ +package atomicint + +import ( + "fmt" + "sync/atomic" +) + +// See https://github.com/nhooyr/websocket/issues/153 +type Int64 struct { + v int64 +} + +func (v *Int64) Load() int64 { + return atomic.LoadInt64(&v.v) +} + +func (v *Int64) Store(i int64) { + atomic.StoreInt64(&v.v, i) +} + +func (v *Int64) String() string { + return fmt.Sprint(v.Load()) +} + +// Increment increments the value and returns the new value. +func (v *Int64) Increment(delta int64) int64 { + return atomic.AddInt64(&v.v, delta) +} + +func (v *Int64) CAS(old, new int64) (swapped bool) { + return atomic.CompareAndSwapInt64(&v.v, old, new) +} diff --git a/internal/bpool/bpool.go b/internal/bufpool/buf.go similarity index 95% rename from internal/bpool/bpool.go rename to internal/bufpool/buf.go index 4266c236..324a17e1 100644 --- a/internal/bpool/bpool.go +++ b/internal/bufpool/buf.go @@ -1,4 +1,4 @@ -package bpool +package bufpool import ( "bytes" diff --git a/internal/bpool/bpool_test.go b/internal/bufpool/buf_test.go similarity index 97% rename from internal/bpool/bpool_test.go rename to internal/bufpool/buf_test.go index 5dfe56e6..42a2fea7 100644 --- a/internal/bpool/bpool_test.go +++ b/internal/bufpool/buf_test.go @@ -1,4 +1,4 @@ -package bpool +package bufpool import ( "strconv" diff --git a/internal/bufpool/bufio.go b/internal/bufpool/bufio.go new file mode 100644 index 00000000..875bbf4b --- /dev/null +++ b/internal/bufpool/bufio.go @@ -0,0 +1,40 @@ +package bufpool + +import ( + "bufio" + "io" + "sync" +) + +var readerPool = sync.Pool{ + New: func() interface{} { + return bufio.NewReader(nil) + }, +} + +func GetReader(r io.Reader) *bufio.Reader { + br := readerPool.Get().(*bufio.Reader) + br.Reset(r) + return br +} + +func PutReader(br *bufio.Reader) { + readerPool.Put(br) +} + +var writerPool = sync.Pool{ + New: func() interface{} { + return bufio.NewWriter(nil) + }, +} + +func GetWriter(w io.Writer) *bufio.Writer { + bw := writerPool.Get().(*bufio.Writer) + bw.Reset(w) + return bw +} + +func PutWriter(bw *bufio.Writer) { + writerPool.Put(bw) +} + diff --git a/internal/wsframe/frame.go b/internal/wsframe/frame.go new file mode 100644 index 00000000..50ff8c11 --- /dev/null +++ b/internal/wsframe/frame.go @@ -0,0 +1,194 @@ +package wsframe + +import ( + "encoding/binary" + "fmt" + "io" + "math" +) + +// Opcode represents a WebSocket Opcode. +type Opcode int + +// Opcode constants. +const ( + OpContinuation Opcode = iota + OpText + OpBinary + // 3 - 7 are reserved for further non-control frames. + _ + _ + _ + _ + _ + OpClose + OpPing + OpPong + // 11-16 are reserved for further control frames. +) + +func (o Opcode) Control() bool { + switch o { + case OpClose, OpPing, OpPong: + return true + } + return false +} + +func (o Opcode) Data() bool { + switch o { + case OpText, OpBinary: + return true + } + return false +} + +// First byte contains fin, rsv1, rsv2, rsv3. +// Second byte contains mask flag and payload len. +// Next 8 bytes are the maximum extended payload length. +// Last 4 bytes are the mask key. +// https://tools.ietf.org/html/rfc6455#section-5.2 +const maxHeaderSize = 1 + 1 + 8 + 4 + +// Header represents a WebSocket frame Header. +// See https://tools.ietf.org/html/rfc6455#section-5.2 +type Header struct { + Fin bool + RSV1 bool + RSV2 bool + RSV3 bool + Opcode Opcode + + PayloadLength int64 + + Masked bool + MaskKey uint32 +} + +// bytes returns the bytes of the Header. +// See https://tools.ietf.org/html/rfc6455#section-5.2 +func (h Header) Bytes(b []byte) []byte { + if b == nil { + b = make([]byte, maxHeaderSize) + } + + b = b[:2] + b[0] = 0 + + if h.Fin { + b[0] |= 1 << 7 + } + if h.RSV1 { + b[0] |= 1 << 6 + } + if h.RSV2 { + b[0] |= 1 << 5 + } + if h.RSV3 { + b[0] |= 1 << 4 + } + + b[0] |= byte(h.Opcode) + + switch { + case h.PayloadLength < 0: + panic(fmt.Sprintf("websocket: invalid Header: negative length: %v", h.PayloadLength)) + case h.PayloadLength <= 125: + b[1] = byte(h.PayloadLength) + case h.PayloadLength <= math.MaxUint16: + b[1] = 126 + b = b[:len(b)+2] + binary.BigEndian.PutUint16(b[len(b)-2:], uint16(h.PayloadLength)) + default: + b[1] = 127 + b = b[:len(b)+8] + binary.BigEndian.PutUint64(b[len(b)-8:], uint64(h.PayloadLength)) + } + + if h.Masked { + b[1] |= 1 << 7 + b = b[:len(b)+4] + binary.LittleEndian.PutUint32(b[len(b)-4:], h.MaskKey) + } + + return b +} + +func MakeReadHeaderBuf() []byte { + return make([]byte, maxHeaderSize-2) +} + +// ReadHeader reads a Header from the reader. +// See https://tools.ietf.org/html/rfc6455#section-5.2 +func ReadHeader(r io.Reader, b []byte) (Header, error) { + // We read the first two bytes first so that we know + // exactly how long the Header is. + b = b[:2] + _, err := io.ReadFull(r, b) + if err != nil { + return Header{}, err + } + + var h Header + h.Fin = b[0]&(1<<7) != 0 + h.RSV1 = b[0]&(1<<6) != 0 + h.RSV2 = b[0]&(1<<5) != 0 + h.RSV3 = b[0]&(1<<4) != 0 + + h.Opcode = Opcode(b[0] & 0xf) + + var extra int + + h.Masked = b[1]&(1<<7) != 0 + if h.Masked { + extra += 4 + } + + payloadLength := b[1] &^ (1 << 7) + switch { + case payloadLength < 126: + h.PayloadLength = int64(payloadLength) + case payloadLength == 126: + extra += 2 + case payloadLength == 127: + extra += 8 + } + + if extra == 0 { + return h, nil + } + + b = b[:extra] + _, err = io.ReadFull(r, b) + if err != nil { + return Header{}, err + } + + switch { + case payloadLength == 126: + h.PayloadLength = int64(binary.BigEndian.Uint16(b)) + b = b[2:] + case payloadLength == 127: + h.PayloadLength = int64(binary.BigEndian.Uint64(b)) + if h.PayloadLength < 0 { + return Header{}, fmt.Errorf("Header with negative payload length: %v", h.PayloadLength) + } + b = b[8:] + } + + if h.Masked { + h.MaskKey = binary.LittleEndian.Uint32(b) + } + + return h, nil +} + +const MaxControlFramePayload = 125 + +func ParseClosePayload(p []byte) (uint16, string, error) { + if len(p) < 2 { + return 0, "", fmt.Errorf("close payload %q too small, cannot even contain the 2 byte status code", p) + } + + return binary.BigEndian.Uint16(p), string(p[2:]), nil +} diff --git a/frame_stringer.go b/internal/wsframe/frame_stringer.go similarity index 90% rename from frame_stringer.go rename to internal/wsframe/frame_stringer.go index 72b865fc..b2e7f423 100644 --- a/frame_stringer.go +++ b/internal/wsframe/frame_stringer.go @@ -1,6 +1,6 @@ -// Code generated by "stringer -type=opcode,MessageType,StatusCode -output=frame_stringer.go"; DO NOT EDIT. +// Code generated by "stringer -type=Opcode,MessageType,StatusCode -output=frame_stringer.go"; DO NOT EDIT. -package websocket +package wsframe import "strconv" @@ -8,12 +8,12 @@ func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} - _ = x[opContinuation-0] - _ = x[opText-1] - _ = x[opBinary-2] - _ = x[opClose-8] - _ = x[opPing-9] - _ = x[opPong-10] + _ = x[OpContinuation-0] + _ = x[OpText-1] + _ = x[OpBinary-2] + _ = x[OpClose-8] + _ = x[OpPing-9] + _ = x[OpPong-10] } const ( @@ -26,7 +26,7 @@ var ( _opcode_index_1 = [...]uint8{0, 7, 13, 19} ) -func (i opcode) String() string { +func (i Opcode) String() string { switch { case 0 <= i && i <= 2: return _opcode_name_0[_opcode_index_0[i]:_opcode_index_0[i+1]] @@ -34,7 +34,7 @@ func (i opcode) String() string { i -= 8 return _opcode_name_1[_opcode_index_1[i]:_opcode_index_1[i+1]] default: - return "opcode(" + strconv.FormatInt(int64(i), 10) + ")" + return "Opcode(" + strconv.FormatInt(int64(i), 10) + ")" } } func _() { diff --git a/internal/wsframe/frame_test.go b/internal/wsframe/frame_test.go new file mode 100644 index 00000000..d6b66e7e --- /dev/null +++ b/internal/wsframe/frame_test.go @@ -0,0 +1,157 @@ +// +build !js + +package wsframe + +import ( + "bytes" + "io" + "math/rand" + "strconv" + "testing" + "time" + _ "unsafe" + + "github.com/google/go-cmp/cmp" + _ "github.com/gorilla/websocket" +) + +func init() { + rand.Seed(time.Now().UnixNano()) +} + +func randBool() bool { + return rand.Intn(1) == 0 +} + +func TestHeader(t *testing.T) { + t.Parallel() + + t.Run("eof", func(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + bytes []byte + }{ + { + "start", + []byte{0xff}, + }, + { + "middle", + []byte{0xff, 0xff, 0xff}, + }, + } + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + b := bytes.NewBuffer(tc.bytes) + _, err := ReadHeader(nil, b) + if io.ErrUnexpectedEOF != err { + t.Fatalf("expected %v but got: %v", io.ErrUnexpectedEOF, err) + } + }) + } + }) + + t.Run("writeNegativeLength", func(t *testing.T) { + t.Parallel() + + defer func() { + r := recover() + if r == nil { + t.Fatal("failed to induce panic in writeHeader with negative payload length") + } + }() + + Header{ + PayloadLength: -1, + }.Bytes(nil) + }) + + t.Run("readNegativeLength", func(t *testing.T) { + t.Parallel() + + b := Header{ + PayloadLength: 1<<16 + 1, + }.Bytes(nil) + + // Make length negative + b[2] |= 1 << 7 + + r := bytes.NewReader(b) + _, err := ReadHeader(nil, r) + if err == nil { + t.Fatalf("unexpected error value: %+v", err) + } + }) + + t.Run("lengths", func(t *testing.T) { + t.Parallel() + + lengths := []int{ + 124, + 125, + 126, + 4096, + 16384, + 65535, + 65536, + 65537, + 131072, + } + + for _, n := range lengths { + n := n + t.Run(strconv.Itoa(n), func(t *testing.T) { + t.Parallel() + + testHeader(t, Header{ + PayloadLength: int64(n), + }) + }) + } + }) + + t.Run("fuzz", func(t *testing.T) { + t.Parallel() + + for i := 0; i < 10000; i++ { + h := Header{ + Fin: randBool(), + RSV1: randBool(), + RSV2: randBool(), + RSV3: randBool(), + Opcode: Opcode(rand.Intn(1 << 4)), + + Masked: randBool(), + PayloadLength: rand.Int63(), + } + + if h.Masked { + h.MaskKey = rand.Uint32() + } + + testHeader(t, h) + } + }) +} + +func testHeader(t *testing.T, h Header) { + b := h.Bytes(nil) + r := bytes.NewReader(b) + h2, err := ReadHeader(r, nil) + if err != nil { + t.Logf("Header: %#v", h) + t.Logf("bytes: %b", b) + t.Fatalf("failed to read Header: %v", err) + } + + if !cmp.Equal(h, h2, cmp.AllowUnexported(Header{})) { + t.Logf("Header: %#v", h) + t.Logf("bytes: %b", b) + t.Fatalf("parsed and read Header differ: %v", cmp.Diff(h, h2, cmp.AllowUnexported(Header{}))) + } +} diff --git a/internal/wsframe/mask.go b/internal/wsframe/mask.go new file mode 100644 index 00000000..2da4c11d --- /dev/null +++ b/internal/wsframe/mask.go @@ -0,0 +1,128 @@ +package wsframe + +import ( + "encoding/binary" + "math/bits" +) + +// Mask applies the WebSocket masking algorithm to p +// with the given key. +// See https://tools.ietf.org/html/rfc6455#section-5.3 +// +// The returned value is the correctly rotated key to +// to continue to mask/unmask the message. +// +// It is optimized for LittleEndian and expects the key +// to be in little endian. +// +// See https://github.com/golang/go/issues/31586 +func Mask(key uint32, b []byte) uint32 { + if len(b) >= 8 { + key64 := uint64(key)<<32 | uint64(key) + + // At some point in the future we can clean these unrolled loops up. + // See https://github.com/golang/go/issues/31586#issuecomment-487436401 + + // Then we xor until b is less than 128 bytes. + for len(b) >= 128 { + v := binary.LittleEndian.Uint64(b) + binary.LittleEndian.PutUint64(b, v^key64) + v = binary.LittleEndian.Uint64(b[8:16]) + binary.LittleEndian.PutUint64(b[8:16], v^key64) + v = binary.LittleEndian.Uint64(b[16:24]) + binary.LittleEndian.PutUint64(b[16:24], v^key64) + v = binary.LittleEndian.Uint64(b[24:32]) + binary.LittleEndian.PutUint64(b[24:32], v^key64) + v = binary.LittleEndian.Uint64(b[32:40]) + binary.LittleEndian.PutUint64(b[32:40], v^key64) + v = binary.LittleEndian.Uint64(b[40:48]) + binary.LittleEndian.PutUint64(b[40:48], v^key64) + v = binary.LittleEndian.Uint64(b[48:56]) + binary.LittleEndian.PutUint64(b[48:56], v^key64) + v = binary.LittleEndian.Uint64(b[56:64]) + binary.LittleEndian.PutUint64(b[56:64], v^key64) + v = binary.LittleEndian.Uint64(b[64:72]) + binary.LittleEndian.PutUint64(b[64:72], v^key64) + v = binary.LittleEndian.Uint64(b[72:80]) + binary.LittleEndian.PutUint64(b[72:80], v^key64) + v = binary.LittleEndian.Uint64(b[80:88]) + binary.LittleEndian.PutUint64(b[80:88], v^key64) + v = binary.LittleEndian.Uint64(b[88:96]) + binary.LittleEndian.PutUint64(b[88:96], v^key64) + v = binary.LittleEndian.Uint64(b[96:104]) + binary.LittleEndian.PutUint64(b[96:104], v^key64) + v = binary.LittleEndian.Uint64(b[104:112]) + binary.LittleEndian.PutUint64(b[104:112], v^key64) + v = binary.LittleEndian.Uint64(b[112:120]) + binary.LittleEndian.PutUint64(b[112:120], v^key64) + v = binary.LittleEndian.Uint64(b[120:128]) + binary.LittleEndian.PutUint64(b[120:128], v^key64) + b = b[128:] + } + + // Then we xor until b is less than 64 bytes. + for len(b) >= 64 { + v := binary.LittleEndian.Uint64(b) + binary.LittleEndian.PutUint64(b, v^key64) + v = binary.LittleEndian.Uint64(b[8:16]) + binary.LittleEndian.PutUint64(b[8:16], v^key64) + v = binary.LittleEndian.Uint64(b[16:24]) + binary.LittleEndian.PutUint64(b[16:24], v^key64) + v = binary.LittleEndian.Uint64(b[24:32]) + binary.LittleEndian.PutUint64(b[24:32], v^key64) + v = binary.LittleEndian.Uint64(b[32:40]) + binary.LittleEndian.PutUint64(b[32:40], v^key64) + v = binary.LittleEndian.Uint64(b[40:48]) + binary.LittleEndian.PutUint64(b[40:48], v^key64) + v = binary.LittleEndian.Uint64(b[48:56]) + binary.LittleEndian.PutUint64(b[48:56], v^key64) + v = binary.LittleEndian.Uint64(b[56:64]) + binary.LittleEndian.PutUint64(b[56:64], v^key64) + b = b[64:] + } + + // Then we xor until b is less than 32 bytes. + for len(b) >= 32 { + v := binary.LittleEndian.Uint64(b) + binary.LittleEndian.PutUint64(b, v^key64) + v = binary.LittleEndian.Uint64(b[8:16]) + binary.LittleEndian.PutUint64(b[8:16], v^key64) + v = binary.LittleEndian.Uint64(b[16:24]) + binary.LittleEndian.PutUint64(b[16:24], v^key64) + v = binary.LittleEndian.Uint64(b[24:32]) + binary.LittleEndian.PutUint64(b[24:32], v^key64) + b = b[32:] + } + + // Then we xor until b is less than 16 bytes. + for len(b) >= 16 { + v := binary.LittleEndian.Uint64(b) + binary.LittleEndian.PutUint64(b, v^key64) + v = binary.LittleEndian.Uint64(b[8:16]) + binary.LittleEndian.PutUint64(b[8:16], v^key64) + b = b[16:] + } + + // Then we xor until b is less than 8 bytes. + for len(b) >= 8 { + v := binary.LittleEndian.Uint64(b) + binary.LittleEndian.PutUint64(b, v^key64) + b = b[8:] + } + } + + // Then we xor until b is less than 4 bytes. + for len(b) >= 4 { + v := binary.LittleEndian.Uint32(b) + binary.LittleEndian.PutUint32(b, v^key) + b = b[4:] + } + + // xor remaining bytes. + for i := range b { + b[i] ^= byte(key) + key = bits.RotateLeft32(key, -8) + } + + return key +} diff --git a/internal/wsframe/mask_test.go b/internal/wsframe/mask_test.go new file mode 100644 index 00000000..fbd29892 --- /dev/null +++ b/internal/wsframe/mask_test.go @@ -0,0 +1,118 @@ +package wsframe_test + +import ( + "crypto/rand" + "encoding/binary" + "github.com/gobwas/ws" + "github.com/google/go-cmp/cmp" + "math/bits" + "nhooyr.io/websocket/internal/wsframe" + "strconv" + "testing" + _ "unsafe" +) + +func Test_mask(t *testing.T) { + t.Parallel() + + key := []byte{0xa, 0xb, 0xc, 0xff} + key32 := binary.LittleEndian.Uint32(key) + p := []byte{0xa, 0xb, 0xc, 0xf2, 0xc} + gotKey32 := wsframe.Mask(key32, p) + + if exp := []byte{0, 0, 0, 0x0d, 0x6}; !cmp.Equal(exp, p) { + t.Fatalf("unexpected mask: %v", cmp.Diff(exp, p)) + } + + if exp := bits.RotateLeft32(key32, -8); !cmp.Equal(exp, gotKey32) { + t.Fatalf("unexpected mask key: %v", cmp.Diff(exp, gotKey32)) + } +} + +func basicMask(maskKey [4]byte, pos int, b []byte) int { + for i := range b { + b[i] ^= maskKey[pos&3] + pos++ + } + return pos & 3 +} + +//go:linkname gorillaMaskBytes github.com/gorilla/websocket.maskBytes +func gorillaMaskBytes(key [4]byte, pos int, b []byte) int + +func Benchmark_mask(b *testing.B) { + sizes := []int{ + 2, + 3, + 4, + 8, + 16, + 32, + 128, + 512, + 4096, + 16384, + } + + fns := []struct { + name string + fn func(b *testing.B, key [4]byte, p []byte) + }{ + { + name: "basic", + fn: func(b *testing.B, key [4]byte, p []byte) { + for i := 0; i < b.N; i++ { + basicMask(key, 0, p) + } + }, + }, + + { + name: "nhooyr", + fn: func(b *testing.B, key [4]byte, p []byte) { + key32 := binary.LittleEndian.Uint32(key[:]) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + wsframe.Mask(key32, p) + } + }, + }, + { + name: "gorilla", + fn: func(b *testing.B, key [4]byte, p []byte) { + for i := 0; i < b.N; i++ { + gorillaMaskBytes(key, 0, p) + } + }, + }, + { + name: "gobwas", + fn: func(b *testing.B, key [4]byte, p []byte) { + for i := 0; i < b.N; i++ { + ws.Cipher(p, key, 0) + } + }, + }, + } + + var key [4]byte + _, err := rand.Read(key[:]) + if err != nil { + b.Fatalf("failed to populate mask key: %v", err) + } + + for _, size := range sizes { + p := make([]byte, size) + + b.Run(strconv.Itoa(size), func(b *testing.B) { + for _, fn := range fns { + b.Run(fn.name, func(b *testing.B) { + b.SetBytes(int64(size)) + + fn.fn(b, key, p) + }) + } + }) + } +} diff --git a/js_test.go b/js_test.go new file mode 100644 index 00000000..80af7896 --- /dev/null +++ b/js_test.go @@ -0,0 +1,50 @@ +package websocket_test + +import ( + "context" + "fmt" + "net/http" + "nhooyr.io/websocket/internal/wsecho" + "os" + "os/exec" + "strings" + "testing" + "time" + + "nhooyr.io/websocket" +) + +func TestJS(t *testing.T) { + t.Parallel() + + s, closeFn := testServer(t, func(w http.ResponseWriter, r *http.Request) error { + c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + Subprotocols: []string{"echo"}, + InsecureSkipVerify: true, + }) + if err != nil { + return err + } + defer c.Close(websocket.StatusInternalError, "") + + err = wsecho.Loop(r.Context(), c) + if websocket.CloseStatus(err) != websocket.StatusNormalClosure { + return err + } + return nil + }, false) + defer closeFn() + + wsURL := strings.Replace(s.URL, "http", "ws", 1) + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + cmd := exec.CommandContext(ctx, "go", "test", "-exec=wasmbrowsertest", "./...") + cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm", fmt.Sprintf("WS_ECHO_SERVER_URL=%v", wsURL)) + + b, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("wasm test binary failed: %v:\n%s", err, b) + } +} diff --git a/conn_common.go b/netconn.go similarity index 60% rename from conn_common.go rename to netconn.go index 1247df6e..74a2c7c1 100644 --- a/conn_common.go +++ b/netconn.go @@ -1,6 +1,3 @@ -// This file contains *Conn symbols relevant to both -// Wasm and non Wasm builds. - package websocket import ( @@ -10,7 +7,6 @@ import ( "math" "net" "sync" - "sync/atomic" "time" ) @@ -169,77 +165,3 @@ func (c *netConn) SetReadDeadline(t time.Time) error { return nil } -// CloseRead will start a goroutine to read from the connection until it is closed or a data message -// is received. If a data message is received, the connection will be closed with StatusPolicyViolation. -// Since CloseRead reads from the connection, it will respond to ping, pong and close frames. -// After calling this method, you cannot read any data messages from the connection. -// The returned context will be cancelled when the connection is closed. -// -// Use this when you do not want to read data messages from the connection anymore but will -// want to write messages to it. -func (c *Conn) CloseRead(ctx context.Context) context.Context { - c.isReadClosed.Store(1) - - ctx, cancel := context.WithCancel(ctx) - go func() { - defer cancel() - // We use the unexported reader method so that we don't get the read closed error. - c.reader(ctx, true) - // Either the connection is already closed since there was a read error - // or the context was cancelled or a message was read and we should close - // the connection. - c.Close(StatusPolicyViolation, "unexpected data message") - }() - return ctx -} - -// SetReadLimit sets the max number of bytes to read for a single message. -// It applies to the Reader and Read methods. -// -// By default, the connection has a message read limit of 32768 bytes. -// -// When the limit is hit, the connection will be closed with StatusMessageTooBig. -func (c *Conn) SetReadLimit(n int64) { - c.msgReadLimit.Store(n) -} - -func (c *Conn) setCloseErr(err error) { - c.closeErrOnce.Do(func() { - c.closeErr = fmt.Errorf("websocket closed: %w", err) - }) -} - -// See https://github.com/nhooyr/websocket/issues/153 -type atomicInt64 struct { - v int64 -} - -func (v *atomicInt64) Load() int64 { - return atomic.LoadInt64(&v.v) -} - -func (v *atomicInt64) Store(i int64) { - atomic.StoreInt64(&v.v, i) -} - -func (v *atomicInt64) String() string { - return fmt.Sprint(v.Load()) -} - -// Increment increments the value and returns the new value. -func (v *atomicInt64) Increment(delta int64) int64 { - return atomic.AddInt64(&v.v, delta) -} - -func (v *atomicInt64) CAS(old, new int64) (swapped bool) { - return atomic.CompareAndSwapInt64(&v.v, old, new) -} - -func (c *Conn) isClosed() bool { - select { - case <-c.closed: - return true - default: - return false - } -} diff --git a/reader.go b/reader.go new file mode 100644 index 00000000..fe716569 --- /dev/null +++ b/reader.go @@ -0,0 +1,31 @@ +package websocket + +import ( + "bufio" + "context" + "io" + "nhooyr.io/websocket/internal/atomicint" + "nhooyr.io/websocket/internal/wsframe" + "strings" +) + +type reader struct { + // Acquired before performing any sort of read operation. + readLock chan struct{} + + c *Conn + + deflateReader io.Reader + br *bufio.Reader + + readClosed *atomicint.Int64 + readHeaderBuf []byte + controlPayloadBuf []byte + + msgCtx context.Context + msgCompressed bool + frameHeader wsframe.Header + frameMaskKey uint32 + frameEOF bool + deflateTail strings.Reader +} diff --git a/websocket_js_test.go b/websocket_js_test.go deleted file mode 100644 index 9b7bb813..00000000 --- a/websocket_js_test.go +++ /dev/null @@ -1,52 +0,0 @@ -package websocket_test - -import ( - "context" - "net/http" - "os" - "testing" - "time" - - "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/assert" -) - -func TestConn(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) - defer cancel() - - c, resp, err := websocket.Dial(ctx, os.Getenv("WS_ECHO_SERVER_URL"), &websocket.DialOptions{ - Subprotocols: []string{"echo"}, - }) - if err != nil { - t.Fatal(err) - } - defer c.Close(websocket.StatusInternalError, "") - - err = assertSubprotocol(c, "echo") - if err != nil { - t.Fatal(err) - } - - err = assert.Equalf(&http.Response{}, resp, "unexpected http response") - if err != nil { - t.Fatal(err) - } - - err = assertJSONEcho(ctx, c, 1024) - if err != nil { - t.Fatal(err) - } - - err = assertEcho(ctx, c, websocket.MessageBinary, 1024) - if err != nil { - t.Fatal(err) - } - - err = c.Close(websocket.StatusNormalClosure, "") - if err != nil { - t.Fatal(err) - } -} diff --git a/writer.go b/writer.go new file mode 100644 index 00000000..b31d57ad --- /dev/null +++ b/writer.go @@ -0,0 +1,5 @@ +package websocket + +type writer struct { + +} diff --git a/websocket_js.go b/ws_js.go similarity index 88% rename from websocket_js.go rename to ws_js.go index d27809cf..4c067430 100644 --- a/websocket_js.go +++ b/ws_js.go @@ -1,3 +1,5 @@ +// +build js + package websocket // import "nhooyr.io/websocket" import ( @@ -7,12 +9,13 @@ import ( "fmt" "io" "net/http" + "nhooyr.io/websocket/internal/atomicint" "reflect" "runtime" "sync" "syscall/js" - "nhooyr.io/websocket/internal/bpool" + "nhooyr.io/websocket/internal/bufpool" "nhooyr.io/websocket/internal/wsjs" ) @@ -21,10 +24,10 @@ type Conn struct { ws wsjs.WebSocket // read limit for a message in bytes. - msgReadLimit *atomicInt64 + msgReadLimit *atomicint.Int64 closingMu sync.Mutex - isReadClosed *atomicInt64 + isReadClosed *atomicint.Int64 closeOnce sync.Once closed chan struct{} closeErrOnce sync.Once @@ -56,17 +59,20 @@ func (c *Conn) init() { c.closed = make(chan struct{}) c.readSignal = make(chan struct{}, 1) - c.msgReadLimit = &atomicInt64{} + c.msgReadLimit = &atomicint.Int64{} c.msgReadLimit.Store(32768) - c.isReadClosed = &atomicInt64{} + c.isReadClosed = &atomicint.Int64{} c.releaseOnClose = c.ws.OnClose(func(e wsjs.CloseEvent) { err := CloseError{ Code: StatusCode(e.Code), Reason: e.Reason, } - c.close(fmt.Errorf("received close: %w", err), e.WasClean) + // We do not know if we sent or received this close as + // its possible the browser triggered it without us + // explicitly sending it. + c.close(err, e.WasClean) c.releaseOnClose() c.releaseOnMessage() @@ -288,11 +294,6 @@ func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) { return typ, bytes.NewReader(p), nil } -// Only implemented for use by *Conn.CloseRead in conn_common.go -func (c *Conn) reader(ctx context.Context, _ bool) { - c.read(ctx) -} - // Writer returns a writer to write a WebSocket data message to the connection. // It buffers the entire message in memory and then sends it when the writer // is closed. @@ -301,7 +302,7 @@ func (c *Conn) Writer(ctx context.Context, typ MessageType) (io.WriteCloser, err c: c, ctx: ctx, typ: typ, - b: bpool.Get(), + b: bufpool.Get(), }, nil } @@ -331,7 +332,7 @@ func (w writer) Close() error { return errors.New("cannot close closed writer") } w.closed = true - defer bpool.Put(w.b) + defer bufpool.Put(w.b) err := w.c.Write(w.ctx, w.typ, w.b.Bytes()) if err != nil { @@ -339,3 +340,34 @@ func (w writer) Close() error { } return nil } + +func (c *Conn) CloseRead(ctx context.Context) context.Context { + c.isReadClosed.Store(1) + + ctx, cancel := context.WithCancel(ctx) + go func() { + defer cancel() + c.read(ctx) + c.Close(StatusPolicyViolation, "unexpected data message") + }() + return ctx +} + +func (c *Conn) SetReadLimit(n int64) { + c.msgReadLimit.Store(n) +} + +func (c *Conn) setCloseErr(err error) { + c.closeErrOnce.Do(func() { + c.closeErr = fmt.Errorf("websocket closed: %w", err) + }) +} + +func (c *Conn) isClosed() bool { + select { + case <-c.closed: + return true + default: + return false + } +} diff --git a/ws_js_test.go b/ws_js_test.go new file mode 100644 index 00000000..abd950c7 --- /dev/null +++ b/ws_js_test.go @@ -0,0 +1,22 @@ +package websocket + +func TestEcho(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + c, resp, err := websocket.Dial(ctx, os.Getenv("WS_ECHO_SERVER_URL"), &websocket.DialOptions{ + Subprotocols: []string{"echo"}, + }) + assert.Success(t, err) + defer c.Close(websocket.StatusInternalError, "") + + assertSubprotocol(t, c, "echo") + assert.Equalf(t, &http.Response{}, resp, "unexpected http response") + assertJSONEcho(t, ctx, c, 1024) + assertEcho(t, ctx, c, websocket.MessageBinary, 1024) + + err = c.Close(websocket.StatusNormalClosure, "") + assert.Success(t, err) +} diff --git a/wsjson/wsjson.go b/wsjson/wsjson.go index fe935fa1..9fa8b54c 100644 --- a/wsjson/wsjson.go +++ b/wsjson/wsjson.go @@ -5,9 +5,8 @@ import ( "context" "encoding/json" "fmt" - "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/bpool" + "nhooyr.io/websocket/internal/bufpool" ) // Read reads a json message from c into v. @@ -31,8 +30,8 @@ func read(ctx context.Context, c *websocket.Conn, v interface{}) error { return fmt.Errorf("unexpected frame type for json (expected %v): %v", websocket.MessageText, typ) } - b := bpool.Get() - defer bpool.Put(b) + b := bufpool.Get() + defer bufpool.Put(b) _, err = b.ReadFrom(r) if err != nil { diff --git a/wspb/wspb.go b/wspb/wspb.go index 3c9e0f76..52ddcd57 100644 --- a/wspb/wspb.go +++ b/wspb/wspb.go @@ -9,7 +9,7 @@ import ( "github.com/golang/protobuf/proto" "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/bpool" + "nhooyr.io/websocket/internal/bufpool" ) // Read reads a protobuf message from c into v. @@ -33,8 +33,8 @@ func read(ctx context.Context, c *websocket.Conn, v proto.Message) error { return fmt.Errorf("unexpected frame type for protobuf (expected %v): %v", websocket.MessageBinary, typ) } - b := bpool.Get() - defer bpool.Put(b) + b := bufpool.Get() + defer bufpool.Put(b) _, err = b.ReadFrom(r) if err != nil { @@ -61,10 +61,10 @@ func Write(ctx context.Context, c *websocket.Conn, v proto.Message) error { } func write(ctx context.Context, c *websocket.Conn, v proto.Message) error { - b := bpool.Get() + b := bufpool.Get() pb := proto.NewBuffer(b.Bytes()) defer func() { - bpool.Put(bytes.NewBuffer(pb.Bytes())) + bufpool.Put(bytes.NewBuffer(pb.Bytes())) }() err := pb.Marshal(v) From d0a80496108cf7cdd4e20c24e4689cd5934b5b89 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 18 Nov 2019 22:52:18 -0500 Subject: [PATCH 220/519] Rewrite core Too many improvements and changes to list. Will include a detailed changelog for release. --- accept.go | 63 +- assert_test.go | 14 + autobahn_test.go | 252 ++ close.go | 158 +- close_test.go | 9 +- compress.go | 86 + conn.go | 1133 +------- conn_export_test.go | 129 - conn_test.go | 2382 +---------------- dial.go | 78 +- dial_test.go | 2 +- example_echo_test.go | 3 +- internal/wsframe/mask.go => frame.go | 162 +- .../wsframe/mask_test.go => frame_test.go | 108 +- internal/assert/assert.go | 40 +- internal/atomicint/atomicint.go | 32 - internal/bufpool/buf.go | 6 +- internal/bufpool/bufio.go | 40 - internal/errd/errd.go | 11 + internal/wsecho/wsecho.go | 55 - internal/wsframe/frame.go | 194 -- internal/wsframe/frame_stringer.go | 91 - internal/wsframe/frame_test.go | 157 -- internal/wsgrace/wsgrace.go | 50 - js_test.go | 50 - read.go | 479 ++++ reader.go | 31 - write.go | 348 +++ writer.go | 5 - ws_js.go | 12 +- wsjson/wsjson.go | 2 + 31 files changed, 1844 insertions(+), 4338 deletions(-) create mode 100644 autobahn_test.go delete mode 100644 conn_export_test.go rename internal/wsframe/mask.go => frame.go (57%) rename internal/wsframe/mask_test.go => frame_test.go (51%) delete mode 100644 internal/atomicint/atomicint.go delete mode 100644 internal/bufpool/bufio.go create mode 100644 internal/errd/errd.go delete mode 100644 internal/wsecho/wsecho.go delete mode 100644 internal/wsframe/frame.go delete mode 100644 internal/wsframe/frame_stringer.go delete mode 100644 internal/wsframe/frame_test.go delete mode 100644 internal/wsgrace/wsgrace.go delete mode 100644 js_test.go create mode 100644 read.go delete mode 100644 reader.go create mode 100644 write.go delete mode 100644 writer.go diff --git a/accept.go b/accept.go index 5ff2ea41..2028d4b2 100644 --- a/accept.go +++ b/accept.go @@ -60,10 +60,15 @@ func Accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, return c, nil } -func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, error) { +func (opts *AcceptOptions) ensure() *AcceptOptions { if opts == nil { - opts = &AcceptOptions{} + return &AcceptOptions{} } + return opts +} + +func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, error) { + opts = opts.ensure() err := verifyClientRequest(w, r) if err != nil { @@ -114,31 +119,14 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, b, _ := brw.Reader.Peek(brw.Reader.Buffered()) brw.Reader.Reset(io.MultiReader(bytes.NewReader(b), netConn)) - c := &Conn{ + return newConn(connConfig{ subprotocol: w.Header().Get("Sec-WebSocket-Protocol"), + rwc: netConn, + client: false, + copts: copts, br: brw.Reader, bw: brw.Writer, - closer: netConn, - copts: copts, - } - c.init() - - return c, nil -} - -func authenticateOrigin(r *http.Request) error { - origin := r.Header.Get("Origin") - if origin == "" { - return nil - } - u, err := url.Parse(origin) - if err != nil { - return fmt.Errorf("failed to parse Origin header %q: %w", origin, err) - } - if !strings.EqualFold(u.Host, r.Host) { - return fmt.Errorf("request Origin %q is not authorized for Host %q", origin, r.Host) - } - return nil + }), nil } func verifyClientRequest(w http.ResponseWriter, r *http.Request) error { @@ -181,15 +169,37 @@ func verifyClientRequest(w http.ResponseWriter, r *http.Request) error { return nil } +func authenticateOrigin(r *http.Request) error { + origin := r.Header.Get("Origin") + if origin == "" { + return nil + } + u, err := url.Parse(origin) + if err != nil { + return fmt.Errorf("failed to parse Origin header %q: %w", origin, err) + } + if !strings.EqualFold(u.Host, r.Host) { + return fmt.Errorf("request Origin %q is not authorized for Host %q", origin, r.Host) + } + return nil +} + func handleSecWebSocketKey(w http.ResponseWriter, r *http.Request) { key := r.Header.Get("Sec-WebSocket-Key") w.Header().Set("Sec-WebSocket-Accept", secWebSocketAccept(key)) } func selectSubprotocol(r *http.Request, subprotocols []string) string { + cps := headerTokens(r.Header, "Sec-WebSocket-Protocol") + if len(cps) == 0 { + return "" + } + for _, sp := range subprotocols { - if headerContainsToken(r.Header, "Sec-WebSocket-Protocol", sp) { - return sp + for _, cp := range cps { + if strings.EqualFold(sp, cp) { + return cp + } } } return "" @@ -266,7 +276,6 @@ func acceptWebkitDeflate(w http.ResponseWriter, ext websocketExtension, mode Com return copts, nil } - func headerContainsToken(h http.Header, key, token string) bool { token = strings.ToLower(token) diff --git a/assert_test.go b/assert_test.go index af300999..0cc9dfe3 100644 --- a/assert_test.go +++ b/assert_test.go @@ -23,6 +23,8 @@ func randBytes(n int) []byte { } func assertJSONEcho(t *testing.T, ctx context.Context, c *websocket.Conn, n int) { + t.Helper() + exp := randString(n) err := wsjson.Write(ctx, c, exp) assert.Success(t, err) @@ -35,6 +37,8 @@ func assertJSONEcho(t *testing.T, ctx context.Context, c *websocket.Conn, n int) } func assertJSONRead(t *testing.T, ctx context.Context, c *websocket.Conn, exp interface{}) { + t.Helper() + var act interface{} err := wsjson.Read(ctx, c, &act) assert.Success(t, err) @@ -56,6 +60,8 @@ func randString(n int) string { } func assertEcho(t *testing.T, ctx context.Context, c *websocket.Conn, typ websocket.MessageType, n int) { + t.Helper() + p := randBytes(n) err := c.Write(ctx, typ, p) assert.Success(t, err) @@ -68,5 +74,13 @@ func assertEcho(t *testing.T, ctx context.Context, c *websocket.Conn, typ websoc } func assertSubprotocol(t *testing.T, c *websocket.Conn, exp string) { + t.Helper() + assert.Equalf(t, exp, c.Subprotocol(), "unexpected subprotocol") } + +func assertCloseStatus(t *testing.T, exp websocket.StatusCode, err error) { + t.Helper() + + assert.Equalf(t, exp, websocket.CloseStatus(err), "unexpected status code") +} diff --git a/autobahn_test.go b/autobahn_test.go new file mode 100644 index 00000000..27f8a1b4 --- /dev/null +++ b/autobahn_test.go @@ -0,0 +1,252 @@ +package websocket_test + +import ( + "context" + "encoding/json" + "fmt" + "io/ioutil" + "net" + "net/http" + "net/http/httptest" + "nhooyr.io/websocket" + "os" + "os/exec" + "strconv" + "strings" + "testing" + "time" +) + +func TestAutobahn(t *testing.T) { + // This test contains the old autobahn test suite tests that use the + // python binary. The approach is clunky and slow so new tests + // have been written in pure Go in websocket_test.go. + // These have been kept for correctness purposes and are occasionally ran. + if os.Getenv("AUTOBAHN") == "" { + t.Skip("Set $AUTOBAHN to run tests against the autobahn test suite") + } + + t.Run("server", testServerAutobahnPython) + t.Run("client", testClientAutobahnPython) +} + +// https://github.com/crossbario/autobahn-python/tree/master/wstest +func testServerAutobahnPython(t *testing.T) { + t.Parallel() + + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + Subprotocols: []string{"echo"}, + }) + if err != nil { + t.Logf("server handshake failed: %+v", err) + return + } + echoLoop(r.Context(), c) + })) + defer s.Close() + + spec := map[string]interface{}{ + "outdir": "ci/out/wstestServerReports", + "servers": []interface{}{ + map[string]interface{}{ + "agent": "main", + "url": strings.Replace(s.URL, "http", "ws", 1), + }, + }, + "cases": []string{"*"}, + // We skip the UTF-8 handling tests as there isn't any reason to reject invalid UTF-8, just + // more performance overhead. 7.5.1 is the same. + "exclude-cases": []string{"6.*", "7.5.1"}, + } + specFile, err := ioutil.TempFile("", "websocketFuzzingClient.json") + if err != nil { + t.Fatalf("failed to create temp file for fuzzingclient.json: %v", err) + } + defer specFile.Close() + + e := json.NewEncoder(specFile) + e.SetIndent("", "\t") + err = e.Encode(spec) + if err != nil { + t.Fatalf("failed to write spec: %v", err) + } + + err = specFile.Close() + if err != nil { + t.Fatalf("failed to close file: %v", err) + } + + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, time.Minute*10) + defer cancel() + + args := []string{"--mode", "fuzzingclient", "--spec", specFile.Name()} + wstest := exec.CommandContext(ctx, "wstest", args...) + out, err := wstest.CombinedOutput() + if err != nil { + t.Fatalf("failed to run wstest: %v\nout:\n%s", err, out) + } + + checkWSTestIndex(t, "./ci/out/wstestServerReports/index.json") +} + +func unusedListenAddr() (string, error) { + l, err := net.Listen("tcp", "localhost:0") + if err != nil { + return "", err + } + l.Close() + return l.Addr().String(), nil +} + +// https://github.com/crossbario/autobahn-python/blob/master/wstest/testee_client_aio.py +func testClientAutobahnPython(t *testing.T) { + t.Parallel() + + if os.Getenv("AUTOBAHN_PYTHON") == "" { + t.Skip("Set $AUTOBAHN_PYTHON to test against the python autobahn test suite") + } + + serverAddr, err := unusedListenAddr() + if err != nil { + t.Fatalf("failed to get unused listen addr for wstest: %v", err) + } + + wsServerURL := "ws://" + serverAddr + + spec := map[string]interface{}{ + "url": wsServerURL, + "outdir": "ci/out/wstestClientReports", + "cases": []string{"*"}, + // See TestAutobahnServer for the reasons why we exclude these. + "exclude-cases": []string{"6.*", "7.5.1"}, + } + specFile, err := ioutil.TempFile("", "websocketFuzzingServer.json") + if err != nil { + t.Fatalf("failed to create temp file for fuzzingserver.json: %v", err) + } + defer specFile.Close() + + e := json.NewEncoder(specFile) + e.SetIndent("", "\t") + err = e.Encode(spec) + if err != nil { + t.Fatalf("failed to write spec: %v", err) + } + + err = specFile.Close() + if err != nil { + t.Fatalf("failed to close file: %v", err) + } + + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, time.Minute*10) + defer cancel() + + args := []string{"--mode", "fuzzingserver", "--spec", specFile.Name(), + // Disables some server that runs as part of fuzzingserver mode. + // See https://github.com/crossbario/autobahn-testsuite/blob/058db3a36b7c3a1edf68c282307c6b899ca4857f/autobahntestsuite/autobahntestsuite/wstest.py#L124 + "--webport=0", + } + wstest := exec.CommandContext(ctx, "wstest", args...) + err = wstest.Start() + if err != nil { + t.Fatal(err) + } + defer func() { + err := wstest.Process.Kill() + if err != nil { + t.Error(err) + } + }() + + // Let it come up. + time.Sleep(time.Second * 5) + + var cases int + func() { + c, _, err := websocket.Dial(ctx, wsServerURL+"/getCaseCount", nil) + if err != nil { + t.Fatal(err) + } + defer c.Close(websocket.StatusInternalError, "") + + _, r, err := c.Reader(ctx) + if err != nil { + t.Fatal(err) + } + b, err := ioutil.ReadAll(r) + if err != nil { + t.Fatal(err) + } + cases, err = strconv.Atoi(string(b)) + if err != nil { + t.Fatal(err) + } + + c.Close(websocket.StatusNormalClosure, "") + }() + + for i := 1; i <= cases; i++ { + func() { + ctx, cancel := context.WithTimeout(ctx, time.Second*45) + defer cancel() + + c, _, err := websocket.Dial(ctx, fmt.Sprintf(wsServerURL+"/runCase?case=%v&agent=main", i), nil) + if err != nil { + t.Fatal(err) + } + echoLoop(ctx, c) + }() + } + + c, _, err := websocket.Dial(ctx, fmt.Sprintf(wsServerURL+"/updateReports?agent=main"), nil) + if err != nil { + t.Fatal(err) + } + c.Close(websocket.StatusNormalClosure, "") + + checkWSTestIndex(t, "./ci/out/wstestClientReports/index.json") +} + +func checkWSTestIndex(t *testing.T, path string) { + wstestOut, err := ioutil.ReadFile(path) + if err != nil { + t.Fatalf("failed to read index.json: %v", err) + } + + var indexJSON map[string]map[string]struct { + Behavior string `json:"behavior"` + BehaviorClose string `json:"behaviorClose"` + } + err = json.Unmarshal(wstestOut, &indexJSON) + if err != nil { + t.Fatalf("failed to unmarshal index.json: %v", err) + } + + var failed bool + for _, tests := range indexJSON { + for test, result := range tests { + switch result.Behavior { + case "OK", "NON-STRICT", "INFORMATIONAL": + default: + failed = true + t.Errorf("test %v failed", test) + } + switch result.BehaviorClose { + case "OK", "INFORMATIONAL": + default: + failed = true + t.Errorf("bad close behaviour for test %v", test) + } + } + } + + if failed { + path = strings.Replace(path, ".json", ".html", 1) + if os.Getenv("CI") == "" { + t.Errorf("wstest found failure, see %q (output as an artifact in CI)", path) + } + } +} diff --git a/close.go b/close.go index 4f48f1b3..b1bc50e9 100644 --- a/close.go +++ b/close.go @@ -5,7 +5,9 @@ import ( "encoding/binary" "errors" "fmt" - "nhooyr.io/websocket/internal/wsframe" + "log" + "nhooyr.io/websocket/internal/bufpool" + "time" ) // StatusCode represents a WebSocket status code. @@ -74,6 +76,87 @@ func CloseStatus(err error) StatusCode { return -1 } +// Close closes the WebSocket connection with the given status code and reason. +// +// It will write a WebSocket close frame with a timeout of 5s and then wait 5s for +// the peer to send a close frame. +// Thus, it implements the full WebSocket close handshake. +// All data messages received from the peer during the close handshake +// will be discarded. +// +// The connection can only be closed once. Additional calls to Close +// are no-ops. +// +// The maximum length of reason must be 125 bytes otherwise an internal +// error will be sent to the peer. For this reason, you should avoid +// sending a dynamic reason. +// +// Close will unblock all goroutines interacting with the connection once +// complete. +func (c *Conn) Close(code StatusCode, reason string) error { + err := c.closeHandshake(code, reason) + if err != nil { + return fmt.Errorf("failed to close websocket: %w", err) + } + return nil +} + +func (c *Conn) closeHandshake(code StatusCode, reason string) error { + err := c.cw.sendClose(code, reason) + if err != nil { + return err + } + + return c.cr.waitClose() +} + +func (cw *connWriter) error(code StatusCode, err error) { + cw.c.setCloseErr(err) + cw.sendClose(code, err.Error()) + cw.c.close(nil) +} + +func (cw *connWriter) sendClose(code StatusCode, reason string) error { + ce := CloseError{ + Code: code, + Reason: reason, + } + + cw.c.setCloseErr(fmt.Errorf("sent close frame: %w", ce)) + + var p []byte + if ce.Code != StatusNoStatusRcvd { + p = ce.bytes() + } + + return cw.control(context.Background(), opClose, p) +} + +func (cr *connReader) waitClose() error { + defer cr.c.close(nil) + + return nil + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + err := cr.mu.Lock(ctx) + if err != nil { + return err + } + defer cr.mu.Unlock() + + b := bufpool.Get() + buf := b.Bytes() + buf = buf[:cap(buf)] + defer bufpool.Put(b) + + for { + // TODO + return nil + } +} + func parseClosePayload(p []byte) (CloseError, error) { if len(p) == 0 { return CloseError{ @@ -81,14 +164,13 @@ func parseClosePayload(p []byte) (CloseError, error) { }, nil } - code, reason, err := wsframe.ParseClosePayload(p) - if err != nil { - return CloseError{}, err + if len(p) < 2 { + return CloseError{}, fmt.Errorf("close payload %q too small, cannot even contain the 2 byte status code", p) } ce := CloseError{ - Code: StatusCode(code), - Reason: reason, + Code: StatusCode(binary.BigEndian.Uint16(p)), + Reason: string(p[2:]), } if !validWireCloseCode(ce.Code) { @@ -116,11 +198,25 @@ func validWireCloseCode(code StatusCode) bool { return false } -func (ce CloseError) bytes() ([]byte, error) { - // TODO move check into frame write - if len(ce.Reason) > wsframe.MaxControlFramePayload-2 { - return nil, fmt.Errorf("reason string max is %v but got %q with length %v", wsframe.MaxControlFramePayload-2, ce.Reason, len(ce.Reason)) +func (ce CloseError) bytes() []byte { + p, err := ce.bytesErr() + if err != nil { + log.Printf("websocket: failed to marshal close frame: %+v", err) + ce = CloseError{ + Code: StatusInternalError, + } + p, _ = ce.bytesErr() } + return p +} + +const maxCloseReason = maxControlPayload - 2 + +func (ce CloseError) bytesErr() ([]byte, error) { + if len(ce.Reason) > maxCloseReason { + return nil, fmt.Errorf("reason string max is %v but got %q with length %v", maxCloseReason, ce.Reason, len(ce.Reason)) + } + if !validWireCloseCode(ce.Code) { return nil, fmt.Errorf("status code %v cannot be set", ce.Code) } @@ -131,44 +227,16 @@ func (ce CloseError) bytes() ([]byte, error) { return buf, nil } -// CloseRead will start a goroutine to read from the connection until it is closed or a data message -// is received. If a data message is received, the connection will be closed with StatusPolicyViolation. -// Since CloseRead reads from the connection, it will respond to ping, pong and close frames. -// After calling this method, you cannot read any data messages from the connection. -// The returned context will be cancelled when the connection is closed. -// -// Use this when you do not want to read data messages from the connection anymore but will -// want to write messages to it. -func (c *Conn) CloseRead(ctx context.Context) context.Context { - c.isReadClosed.Store(1) - - ctx, cancel := context.WithCancel(ctx) - go func() { - defer cancel() - // We use the unexported reader method so that we don't get the read closed error. - c.reader(ctx, true) - // Either the connection is already closed since there was a read error - // or the context was cancelled or a message was read and we should close - // the connection. - c.Close(StatusPolicyViolation, "unexpected data message") - }() - return ctx -} - -// SetReadLimit sets the max number of bytes to read for a single message. -// It applies to the Reader and Read methods. -// -// By default, the connection has a message read limit of 32768 bytes. -// -// When the limit is hit, the connection will be closed with StatusMessageTooBig. -func (c *Conn) SetReadLimit(n int64) { - c.msgReadLimit.Store(n) +func (c *Conn) setCloseErr(err error) { + c.closeMu.Lock() + c.setCloseErrNoLock(err) + c.closeMu.Unlock() } -func (c *Conn) setCloseErr(err error) { - c.closeErrOnce.Do(func() { +func (c *Conn) setCloseErrNoLock(err error) { + if c.closeErr == nil { c.closeErr = fmt.Errorf("websocket closed: %w", err) - }) + } } func (c *Conn) isClosed() bool { diff --git a/close_test.go b/close_test.go index 78096d7e..ee10cd3f 100644 --- a/close_test.go +++ b/close_test.go @@ -5,7 +5,6 @@ import ( "io" "math" "nhooyr.io/websocket/internal/assert" - "nhooyr.io/websocket/internal/wsframe" "strings" "testing" ) @@ -22,7 +21,7 @@ func TestCloseError(t *testing.T) { name: "normal", ce: CloseError{ Code: StatusNormalClosure, - Reason: strings.Repeat("x", wsframe.MaxControlFramePayload-2), + Reason: strings.Repeat("x", maxCloseReason), }, success: true, }, @@ -30,7 +29,7 @@ func TestCloseError(t *testing.T) { name: "bigReason", ce: CloseError{ Code: StatusNormalClosure, - Reason: strings.Repeat("x", wsframe.MaxControlFramePayload-1), + Reason: strings.Repeat("x", maxCloseReason+1), }, success: false, }, @@ -38,7 +37,7 @@ func TestCloseError(t *testing.T) { name: "bigCode", ce: CloseError{ Code: math.MaxUint16, - Reason: strings.Repeat("x", wsframe.MaxControlFramePayload-2), + Reason: strings.Repeat("x", maxCloseReason), }, success: false, }, @@ -49,7 +48,7 @@ func TestCloseError(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - _, err := tc.ce.bytes() + _, err := tc.ce.bytesErr() if (err == nil) != tc.success { t.Fatalf("unexpected error value: %+v", err) } diff --git a/compress.go b/compress.go index 5b5fdce5..9e075430 100644 --- a/compress.go +++ b/compress.go @@ -3,7 +3,10 @@ package websocket import ( + "compress/flate" + "io" "net/http" + "sync" ) // CompressionMode controls the modes available RFC 7692's deflate extension. @@ -76,3 +79,86 @@ func (copts *compressionOptions) setHeader(h http.Header) { // we need to add them back otherwise flate.Reader keeps // trying to return more bytes. const deflateMessageTail = "\x00\x00\xff\xff" + +func (c *Conn) writeNoContextTakeOver() bool { + return c.client && c.copts.clientNoContextTakeover || !c.client && c.copts.serverNoContextTakeover +} + +func (c *Conn) readNoContextTakeOver() bool { + return !c.client && c.copts.clientNoContextTakeover || c.client && c.copts.serverNoContextTakeover +} + +type trimLastFourBytesWriter struct { + w io.Writer + tail []byte +} + +func (tw *trimLastFourBytesWriter) reset() { + tw.tail = tw.tail[:0] +} + +func (tw *trimLastFourBytesWriter) Write(p []byte) (int, error) { + extra := len(tw.tail) + len(p) - 4 + + if extra <= 0 { + tw.tail = append(tw.tail, p...) + return len(p), nil + } + + // Now we need to write as many extra bytes as we can from the previous tail. + if extra > len(tw.tail) { + extra = len(tw.tail) + } + if extra > 0 { + _, err := tw.w.Write(tw.tail[:extra]) + if err != nil { + return 0, err + } + tw.tail = tw.tail[extra:] + } + + // If p is less than or equal to 4 bytes, + // all of it is is part of the tail. + if len(p) <= 4 { + tw.tail = append(tw.tail, p...) + return len(p), nil + } + + // Otherwise, only the last 4 bytes are. + tw.tail = append(tw.tail, p[len(p)-4:]...) + + p = p[:len(p)-4] + n, err := tw.w.Write(p) + return n + 4, err +} + +var flateReaderPool sync.Pool + +func getFlateReader(r io.Reader) io.Reader { + fr, ok := flateReaderPool.Get().(io.Reader) + if !ok { + return flate.NewReader(r) + } + fr.(flate.Resetter).Reset(r, nil) + return fr +} + +func putFlateReader(fr io.Reader) { + flateReaderPool.Put(fr) +} + +var flateWriterPool sync.Pool + +func getFlateWriter(w io.Writer) *flate.Writer { + fw, ok := flateWriterPool.Get().(*flate.Writer) + if !ok { + fw, _ = flate.NewWriter(w, flate.BestSpeed) + return fw + } + fw.Reset(w) + return fw +} + +func putFlateWriter(w *flate.Writer) { + flateWriterPool.Put(w) +} diff --git a/conn.go b/conn.go index 791d9b4c..e3f24171 100644 --- a/conn.go +++ b/conn.go @@ -4,25 +4,14 @@ package websocket import ( "bufio" - "compress/flate" "context" - "crypto/rand" - "encoding/binary" "errors" "fmt" "io" - "io/ioutil" - "log" - "nhooyr.io/websocket/internal/atomicint" - "nhooyr.io/websocket/internal/wsframe" "runtime" "strconv" - "strings" "sync" "sync/atomic" - "time" - - "nhooyr.io/websocket/internal/bufpool" ) // MessageType represents the type of a WebSocket message. @@ -51,91 +40,54 @@ const ( // This applies to the Read methods in the wsjson/wspb subpackages as well. type Conn struct { subprotocol string - fw *flate.Writer - bw *bufio.Writer - // writeBuf is used for masking, its the buffer in bufio.Writer. - // Only used by the client for masking the bytes in the buffer. - writeBuf []byte - closer io.Closer - client bool - copts *compressionOptions - - closeOnce sync.Once - closeErrOnce sync.Once - closeErr error - closed chan struct{} - closing *atomicint.Int64 - closeReceived error + rwc io.ReadWriteCloser + client bool + copts *compressionOptions - // messageWriter state. - // writeMsgLock is acquired to write a data message. - writeMsgLock chan struct{} - // writeFrameLock is acquired to write a single frame. - // Effectively meaning whoever holds it gets to write to bw. - writeFrameLock chan struct{} - writeHeaderBuf []byte - writeHeader *header - // read limit for a message in bytes. - msgReadLimit *atomicint.Int64 + cr connReader + cw connWriter - // Used to ensure a previous writer is not used after being closed. - activeWriter atomic.Value - // messageWriter state. - writeMsgOpcode opcode - writeMsgCtx context.Context + closed chan struct{} - setReadTimeout chan context.Context - setWriteTimeout chan context.Context + closeMu sync.Mutex + closeErr error + closeHandshakeErr error - pingCounter *atomicint.Int64 + pingCounter int32 activePingsMu sync.Mutex activePings map[string]chan<- struct{} - - logf func(format string, v ...interface{}) } -func (c *Conn) init() { - c.closed = make(chan struct{}) - c.closing = &atomicint.Int64{} - - c.msgReadLimit = &atomicint.Int64{} - c.msgReadLimit.Store(32768) +type connConfig struct { + subprotocol string + rwc io.ReadWriteCloser + client bool + copts *compressionOptions - c.writeMsgLock = make(chan struct{}, 1) - c.writeFrameLock = make(chan struct{}, 1) + bw *bufio.Writer + br *bufio.Reader +} - c.readFrameLock = make(chan struct{}, 1) - c.readLock = make(chan struct{}, 1) - c.payloadReader = framePayloadReader{c} +func newConn(cfg connConfig) *Conn { + c := &Conn{} + c.subprotocol = cfg.subprotocol + c.rwc = cfg.rwc + c.client = cfg.client + c.copts = cfg.copts - c.setReadTimeout = make(chan context.Context) - c.setWriteTimeout = make(chan context.Context) + c.cr.init(c, cfg.br) + c.cw.init(c, cfg.bw) - c.pingCounter = &atomicint.Int64{} + c.closed = make(chan struct{}) c.activePings = make(map[string]chan<- struct{}) - c.writeHeaderBuf = makeWriteHeaderBuf() - c.writeHeader = &header{} - c.readHeaderBuf = makeReadHeaderBuf() - c.isReadClosed = &atomicint.Int64{} - c.controlPayloadBuf = make([]byte, maxControlFramePayload) - runtime.SetFinalizer(c, func(c *Conn) { c.close(errors.New("connection garbage collected")) }) - c.logf = log.Printf - - if c.copts != nil { - if !c.readNoContextTakeOver() { - c.fr = getFlateReader(c.payloadReader) - } - if !c.writeNoContextTakeOver() { - c.fw = getFlateWriter(c.bw) - } - } - go c.timeoutLoop() + + return c } // Subprotocol returns the negotiated subprotocol. @@ -145,38 +97,25 @@ func (c *Conn) Subprotocol() string { } func (c *Conn) close(err error) { - c.closeOnce.Do(func() { - runtime.SetFinalizer(c, nil) + c.closeMu.Lock() + defer c.closeMu.Unlock() - c.setCloseErr(err) - close(c.closed) - - // Have to close after c.closed is closed to ensure any goroutine that wakes up - // from the connection being closed also sees that c.closed is closed and returns - // closeErr. - c.closer.Close() + if c.isClosed() { + return + } + close(c.closed) + runtime.SetFinalizer(c, nil) + c.setCloseErrNoLock(err) - // By acquiring the locks, we ensure no goroutine will touch the bufio reader or writer - // and we can safely return them. - // Whenever a caller holds this lock and calls close, it ensures to release the lock to prevent - // a deadlock. - // As of now, this is in writeFrame, readFramePayload and readHeader. - c.readFrameLock <- struct{}{} - if c.client { - returnBufioReader(c.br) - } - if c.fr != nil { - putFlateReader(c.fr) - } + // Have to close after c.closed is closed to ensure any goroutine that wakes up + // from the connection being closed also sees that c.closed is closed and returns + // closeErr. + c.rwc.Close() - c.writeFrameLock <- struct{}{} - if c.client { - returnBufioWriter(c.bw) - } - if c.fw != nil { - putFlateWriter(c.fw) - } - }) + go func() { + c.cr.close() + c.cw.close() + }() } func (c *Conn) timeoutLoop() { @@ -188,20 +127,13 @@ func (c *Conn) timeoutLoop() { case <-c.closed: return - case writeCtx = <-c.setWriteTimeout: - case readCtx = <-c.setReadTimeout: + case writeCtx = <-c.cw.timeout: + case readCtx = <-c.cr.timeout: case <-readCtx.Done(): c.setCloseErr(fmt.Errorf("read timed out: %w", readCtx.Err())) - // Guaranteed to eventually close the connection since we can only ever send - // one close frame. - go func() { - c.exportedClose(StatusPolicyViolation, "read timed out", true) - // Ensure the connection closes, i.e if we already sent a close frame and timed out - // to read the peer's close frame. - c.close(nil) - }() - readCtx = context.Background() + c.cw.error(StatusPolicyViolation, errors.New("timed out")) + return case <-writeCtx.Done(): c.close(fmt.Errorf("write timed out: %w", writeCtx.Err())) return @@ -209,843 +141,8 @@ func (c *Conn) timeoutLoop() { } } -func (c *Conn) acquireLock(ctx context.Context, lock chan struct{}) error { - select { - case <-ctx.Done(): - var err error - switch lock { - case c.writeFrameLock, c.writeMsgLock: - err = fmt.Errorf("could not acquire write lock: %v", ctx.Err()) - case c.readFrameLock, c.readLock: - err = fmt.Errorf("could not acquire read lock: %v", ctx.Err()) - default: - panic(fmt.Sprintf("websocket: failed to acquire unknown lock: %v", ctx.Err())) - } - c.close(err) - return ctx.Err() - case <-c.closed: - return c.closeErr - case lock <- struct{}{}: - return nil - } -} - -func (c *Conn) releaseLock(lock chan struct{}) { - // Allow multiple releases. - select { - case <-lock: - default: - } -} - -func (c *Conn) readTillMsg(ctx context.Context) (header, error) { - for { - h, err := c.readFrameHeader(ctx) - if err != nil { - return header{}, err - } - - if (h.rsv1 && (c.copts == nil || h.opcode.controlOp() || h.opcode == opContinuation)) || h.rsv2 || h.rsv3 { - err := fmt.Errorf("received header with rsv bits set: %v:%v:%v", h.rsv1, h.rsv2, h.rsv3) - c.exportedClose(StatusProtocolError, err.Error(), false) - return header{}, err - } - - if h.opcode.controlOp() { - err = c.handleControl(ctx, h) - if err != nil { - // Pass through CloseErrors when receiving a close frame. - if h.opcode == opClose && CloseStatus(err) != -1 { - return header{}, err - } - return header{}, fmt.Errorf("failed to handle control frame %v: %w", h.opcode, err) - } - continue - } - - switch h.opcode { - case opBinary, opText, opContinuation: - return h, nil - default: - err := fmt.Errorf("received unknown opcode %v", h.opcode) - c.exportedClose(StatusProtocolError, err.Error(), false) - return header{}, err - } - } -} - -func (c *Conn) readFrameHeader(ctx context.Context) (_ header, err error) { - wrap := func(err error) error { - return fmt.Errorf("failed to read frame header: %w", err) - } - defer func() { - if err != nil { - err = wrap(err) - } - }() - - err = c.acquireLock(ctx, c.readFrameLock) - if err != nil { - return header{}, err - } - defer c.releaseLock(c.readFrameLock) - - select { - case <-c.closed: - return header{}, c.closeErr - case c.setReadTimeout <- ctx: - } - - h, err := readHeader(c.readHeaderBuf, c.br) - if err != nil { - select { - case <-c.closed: - return header{}, c.closeErr - case <-ctx.Done(): - err = ctx.Err() - default: - } - c.releaseLock(c.readFrameLock) - c.close(wrap(err)) - return header{}, err - } - - select { - case <-c.closed: - return header{}, c.closeErr - case c.setReadTimeout <- context.Background(): - } - - return h, nil -} - -func (c *Conn) handleControl(ctx context.Context, h header) error { - if h.payloadLength > maxControlFramePayload { - err := fmt.Errorf("received too big control frame at %v bytes", h.payloadLength) - c.exportedClose(StatusProtocolError, err.Error(), false) - return err - } - - if !h.fin { - err := errors.New("received fragmented control frame") - c.exportedClose(StatusProtocolError, err.Error(), false) - return err - } - - ctx, cancel := context.WithTimeout(ctx, time.Second*5) - defer cancel() - - b := c.controlPayloadBuf[:h.payloadLength] - _, err := c.readFramePayload(ctx, b) - if err != nil { - return err - } - - if h.masked { - mask(h.maskKey, b) - } - - switch h.opcode { - case opPing: - return c.writeControl(ctx, opPong, b) - case opPong: - c.activePingsMu.Lock() - pong, ok := c.activePings[string(b)] - c.activePingsMu.Unlock() - if ok { - close(pong) - } - return nil - case opClose: - ce, err := parseClosePayload(b) - if err != nil { - err = fmt.Errorf("received invalid close payload: %w", err) - c.exportedClose(StatusProtocolError, err.Error(), false) - c.closeReceived = err - return err - } - - err = fmt.Errorf("received close: %w", ce) - c.closeReceived = err - c.writeClose(b, err, false) - - if ctx.Err() != nil { - // The above close probably has been returned by the peer in response - // to our read timing out so we have to return the read timed out error instead. - return fmt.Errorf("read timed out: %w", ctx.Err()) - } - - return err - default: - panic(fmt.Sprintf("websocket: unexpected control opcode: %#v", h)) - } -} - -// Reader waits until there is a WebSocket data message to read -// from the connection. -// It returns the type of the message and a reader to read it. -// The passed context will also bound the reader. -// Ensure you read to EOF otherwise the connection will hang. -// -// All returned errors will cause the connection -// to be closed so you do not need to write your own error message. -// This applies to the Read methods in the wsjson/wspb subpackages as well. -// -// You must read from the connection for control frames to be handled. -// Thus if you expect messages to take a long time to be responded to, -// you should handle such messages async to reading from the connection -// to ensure control frames are promptly handled. -// -// If you do not expect any data messages from the peer, call CloseRead. -// -// Only one Reader may be open at a time. -// -// If you need a separate timeout on the Reader call and then the message -// Read, use time.AfterFunc to cancel the context passed in early. -// See https://github.com/nhooyr/websocket/issues/87#issue-451703332 -// Most users should not need this. -func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) { - if c.isReadClosed.Load() == 1 { - return 0, nil, errors.New("websocket connection read closed") - } - - typ, r, err := c.reader(ctx, true) - if err != nil { - return 0, nil, fmt.Errorf("failed to get reader: %w", err) - } - return typ, r, nil -} - -func (c *Conn) reader(ctx context.Context, lock bool) (MessageType, io.Reader, error) { - if lock { - err := c.acquireLock(ctx, c.readLock) - if err != nil { - return 0, nil, err - } - defer c.releaseLock(c.readLock) - } - - if c.activeReader != nil && !c.readerFrameEOF { - // The only way we know for sure the previous reader is not yet complete is - // if there is an active frame not yet fully read. - // Otherwise, a user may have read the last byte but not the EOF if the EOF - // is in the next frame so we check for that below. - return 0, nil, errors.New("previous message not read to completion") - } - - h, err := c.readTillMsg(ctx) - if err != nil { - return 0, nil, err - } - - if c.activeReader != nil && !c.activeReader.eof() { - if h.opcode != opContinuation { - err := errors.New("received new data message without finishing the previous message") - c.exportedClose(StatusProtocolError, err.Error(), false) - return 0, nil, err - } - - if !h.fin || h.payloadLength > 0 { - return 0, nil, fmt.Errorf("previous message not read to completion") - } - - c.activeReader = nil - - h, err = c.readTillMsg(ctx) - if err != nil { - return 0, nil, err - } - } else if h.opcode == opContinuation { - err := errors.New("received continuation frame not after data or text frame") - c.exportedClose(StatusProtocolError, err.Error(), false) - return 0, nil, err - } - - c.readerMsgCtx = ctx - c.readerMsgHeader = h - - c.readerPayloadCompressed = h.rsv1 - - if c.readerPayloadCompressed { - c.readerCompressTail.Reset(deflateMessageTail) - } - - c.readerFrameEOF = false - c.readerMaskKey = h.maskKey - c.readMsgLeft = c.msgReadLimit.Load() - - r := &messageReader{ - c: c, - } - c.activeReader = r - if c.readerPayloadCompressed && c.readNoContextTakeOver() { - c.fr = getFlateReader(c.payloadReader) - } - return MessageType(h.opcode), r, nil -} - -type framePayloadReader struct { - c *Conn -} - -func (r framePayloadReader) Read(p []byte) (int, error) { - if r.c.readerFrameEOF { - if r.c.readerPayloadCompressed && r.c.readerMsgHeader.fin { - n, _ := r.c.readerCompressTail.Read(p) - return n, nil - } - - h, err := r.c.readTillMsg(r.c.readerMsgCtx) - if err != nil { - return 0, err - } - - if h.opcode != opContinuation { - err := errors.New("received new data message without finishing the previous message") - r.c.exportedClose(StatusProtocolError, err.Error(), false) - return 0, err - } - - r.c.readerMsgHeader = h - r.c.readerFrameEOF = false - r.c.readerMaskKey = h.maskKey - } - - h := r.c.readerMsgHeader - if int64(len(p)) > h.payloadLength { - p = p[:h.payloadLength] - } - - n, err := r.c.readFramePayload(r.c.readerMsgCtx, p) - - h.payloadLength -= int64(n) - if h.masked { - r.c.readerMaskKey = mask(r.c.readerMaskKey, p) - } - r.c.readerMsgHeader = h - - if err != nil { - return n, err - } - - if h.payloadLength == 0 { - r.c.readerFrameEOF = true - - if h.fin && !r.c.readerPayloadCompressed { - return n, io.EOF - } - } - - return n, nil -} - -// messageReader enables reading a data frame from the WebSocket connection. -type messageReader struct { - c *Conn -} - -func (r *messageReader) eof() bool { - return r.c.activeReader != r -} - -// Read reads as many bytes as possible into p. -func (r *messageReader) Read(p []byte) (int, error) { - return r.exportedRead(p, true) -} - -func (r *messageReader) exportedRead(p []byte, lock bool) (int, error) { - n, err := r.read(p, lock) - if err != nil { - // Have to return io.EOF directly for now, we cannot wrap as errors.Is - // isn't used widely yet. - if errors.Is(err, io.EOF) { - return n, io.EOF - } - return n, fmt.Errorf("failed to read: %w", err) - } - return n, nil -} - -func (r *messageReader) readUnlocked(p []byte) (int, error) { - return r.exportedRead(p, false) -} - -func (r *messageReader) read(p []byte, lock bool) (int, error) { - if lock { - // If we cannot acquire the read lock, then - // there is either a concurrent read or the close handshake - // is proceeding. - select { - case r.c.readLock <- struct{}{}: - defer r.c.releaseLock(r.c.readLock) - default: - if r.c.closing.Load() == 1 { - <-r.c.closed - return 0, r.c.closeErr - } - return 0, errors.New("concurrent read detected") - } - } - - if r.eof() { - return 0, errors.New("cannot use EOFed reader") - } - - if r.c.readMsgLeft <= 0 { - err := fmt.Errorf("read limited at %v bytes", r.c.msgReadLimit) - r.c.exportedClose(StatusMessageTooBig, err.Error(), false) - return 0, err - } - - if int64(len(p)) > r.c.readMsgLeft { - p = p[:r.c.readMsgLeft] - } - - pr := io.Reader(r.c.payloadReader) - if r.c.readerPayloadCompressed { - pr = r.c.fr - } - - n, err := pr.Read(p) - - r.c.readMsgLeft -= int64(n) - - if r.c.readerFrameEOF && r.c.readerMsgHeader.fin { - if r.c.readerPayloadCompressed && r.c.readNoContextTakeOver() { - putFlateReader(r.c.fr) - r.c.fr = nil - } - r.c.activeReader = nil - if err == nil { - err = io.EOF - } - } - - return n, err -} - -func (c *Conn) readFramePayload(ctx context.Context, p []byte) (_ int, err error) { - wrap := func(err error) error { - return fmt.Errorf("failed to read frame payload: %w", err) - } - defer func() { - if err != nil { - err = wrap(err) - } - }() - - err = c.acquireLock(ctx, c.readFrameLock) - if err != nil { - return 0, err - } - defer c.releaseLock(c.readFrameLock) - - select { - case <-c.closed: - return 0, c.closeErr - case c.setReadTimeout <- ctx: - } - - n, err := io.ReadFull(c.br, p) - if err != nil { - select { - case <-c.closed: - return n, c.closeErr - case <-ctx.Done(): - err = ctx.Err() - default: - } - c.releaseLock(c.readFrameLock) - c.close(wrap(err)) - return n, err - } - - select { - case <-c.closed: - return n, c.closeErr - case c.setReadTimeout <- context.Background(): - } - - return n, err -} - -// Read is a convenience method to read a single message from the connection. -// -// See the Reader method if you want to be able to reuse buffers or want to stream a message. -// The docs on Reader apply to this method as well. -func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { - typ, r, err := c.Reader(ctx) - if err != nil { - return 0, nil, err - } - - b, err := ioutil.ReadAll(r) - return typ, b, err -} - -// Writer returns a writer bounded by the context that will write -// a WebSocket message of type dataType to the connection. -// -// You must close the writer once you have written the entire message. -// -// Only one writer can be open at a time, multiple calls will block until the previous writer -// is closed. -func (c *Conn) Writer(ctx context.Context, typ MessageType) (io.WriteCloser, error) { - wc, err := c.writer(ctx, typ) - if err != nil { - return nil, fmt.Errorf("failed to get writer: %w", err) - } - return wc, nil -} - -func (c *Conn) writer(ctx context.Context, typ MessageType) (io.WriteCloser, error) { - err := c.acquireLock(ctx, c.writeMsgLock) - if err != nil { - return nil, err - } - c.writeMsgCtx = ctx - c.writeMsgOpcode = opcode(typ) - w := &messageWriter{ - c: c, - } - c.activeWriter.Store(w) - return w, nil -} - -// Write is a convenience method to write a message to the connection. -// -// See the Writer method if you want to stream a message. -func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { - _, err := c.write(ctx, typ, p) - if err != nil { - return fmt.Errorf("failed to write msg: %w", err) - } - return nil -} - -func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error) { - err := c.acquireLock(ctx, c.writeMsgLock) - if err != nil { - return 0, err - } - defer c.releaseLock(c.writeMsgLock) - - n, err := c.writeFrame(ctx, true, opcode(typ), p) - return n, err -} - -// messageWriter enables writing to a WebSocket connection. -type messageWriter struct { - c *Conn -} - -func (w *messageWriter) closed() bool { - return w != w.c.activeWriter.Load() -} - -// Write writes the given bytes to the WebSocket connection. -func (w *messageWriter) Write(p []byte) (int, error) { - n, err := w.write(p) - if err != nil { - return n, fmt.Errorf("failed to write: %w", err) - } - return n, nil -} - -func (w *messageWriter) write(p []byte) (int, error) { - if w.closed() { - return 0, fmt.Errorf("cannot use closed writer") - } - n, err := w.c.writeFrame(w.c.writeMsgCtx, false, w.c.writeMsgOpcode, p) - if err != nil { - return n, fmt.Errorf("failed to write data frame: %w", err) - } - w.c.writeMsgOpcode = opContinuation - return n, nil -} - -// Close flushes the frame to the connection. -// This must be called for every messageWriter. -func (w *messageWriter) Close() error { - err := w.close() - if err != nil { - return fmt.Errorf("failed to close writer: %w", err) - } - return nil -} - -func (w *messageWriter) close() error { - if w.closed() { - return fmt.Errorf("cannot use closed writer") - } - w.c.activeWriter.Store((*messageWriter)(nil)) - - _, err := w.c.writeFrame(w.c.writeMsgCtx, true, w.c.writeMsgOpcode, nil) - if err != nil { - return fmt.Errorf("failed to write fin frame: %w", err) - } - - w.c.releaseLock(w.c.writeMsgLock) - return nil -} - -func (c *Conn) writeControl(ctx context.Context, opcode opcode, p []byte) error { - ctx, cancel := context.WithTimeout(ctx, time.Second*5) - defer cancel() - - _, err := c.writeFrame(ctx, true, opcode, p) - if err != nil { - return fmt.Errorf("failed to write control frame %v: %w", opcode, err) - } - return nil -} - -// writeFrame handles all writes to the connection. -func (c *Conn) writeFrame(ctx context.Context, fin bool, opcode opcode, p []byte) (int, error) { - err := c.acquireLock(ctx, c.writeFrameLock) - if err != nil { - return 0, err - } - defer c.releaseLock(c.writeFrameLock) - - select { - case <-c.closed: - return 0, c.closeErr - case c.setWriteTimeout <- ctx: - } - - c.writeHeader.fin = fin - c.writeHeader.opcode = opcode - c.writeHeader.masked = c.client - c.writeHeader.payloadLength = int64(len(p)) - - if c.client { - err = binary.Read(rand.Reader, binary.LittleEndian, &c.writeHeader.maskKey) - if err != nil { - return 0, fmt.Errorf("failed to generate masking key: %w", err) - } - } - - n, err := c.realWriteFrame(ctx, *c.writeHeader, p) - if err != nil { - return n, err - } - - // We already finished writing, no need to potentially brick the connection if - // the context expires. - select { - case <-c.closed: - return n, c.closeErr - case c.setWriteTimeout <- context.Background(): - } - - return n, nil -} - -func (c *Conn) realWriteFrame(ctx context.Context, h header, p []byte) (n int, err error) { - defer func() { - if err != nil { - select { - case <-c.closed: - err = c.closeErr - case <-ctx.Done(): - err = ctx.Err() - default: - } - - err = fmt.Errorf("failed to write %v frame: %w", h.opcode, err) - // We need to release the lock first before closing the connection to ensure - // the lock can be acquired inside close to ensure no one can access c.bw. - c.releaseLock(c.writeFrameLock) - c.close(err) - } - }() - - headerBytes := writeHeader(c.writeHeaderBuf, h) - _, err = c.bw.Write(headerBytes) - if err != nil { - return 0, err - } - - if c.client { - maskKey := h.maskKey - for len(p) > 0 { - if c.bw.Available() == 0 { - err = c.bw.Flush() - if err != nil { - return n, err - } - } - - // Start of next write in the buffer. - i := c.bw.Buffered() - - p2 := p - if len(p) > c.bw.Available() { - p2 = p[:c.bw.Available()] - } - - n2, err := c.bw.Write(p2) - if err != nil { - return n, err - } - - maskKey = mask(maskKey, c.writeBuf[i:i+n2]) - - p = p[n2:] - n += n2 - } - } else { - n, err = c.bw.Write(p) - if err != nil { - return n, err - } - } - - if h.fin { - err = c.bw.Flush() - if err != nil { - return n, err - } - } - - return n, nil -} - -// Close closes the WebSocket connection with the given status code and reason. -// -// It will write a WebSocket close frame with a timeout of 5s and then wait 5s for -// the peer to send a close frame. -// Thus, it implements the full WebSocket close handshake. -// All data messages received from the peer during the close handshake -// will be discarded. -// -// The connection can only be closed once. Additional calls to Close -// are no-ops. -// -// The maximum length of reason must be 125 bytes otherwise an internal -// error will be sent to the peer. For this reason, you should avoid -// sending a dynamic reason. -// -// Close will unblock all goroutines interacting with the connection once -// complete. -func (c *Conn) Close(code StatusCode, reason string) error { - err := c.exportedClose(code, reason, true) - var ec errClosing - if errors.As(err, &ec) { - <-c.closed - // We wait until the connection closes. - // We use writeClose and not exportedClose to avoid a second failed to marshal close frame error. - err = c.writeClose(nil, ec.ce, true) - } - if err != nil { - return fmt.Errorf("failed to close websocket connection: %w", err) - } - return nil -} - -func (c *Conn) exportedClose(code StatusCode, reason string, handshake bool) error { - ce := CloseError{ - Code: code, - Reason: reason, - } - - // This function also will not wait for a close frame from the peer like the RFC - // wants because that makes no sense and I don't think anyone actually follows that. - // Definitely worth seeing what popular browsers do later. - p, err := ce.bytes() - if err != nil { - c.logf("websocket: failed to marshal close frame: %+v", err) - ce = CloseError{ - Code: StatusInternalError, - } - p, _ = ce.bytes() - } - - return c.writeClose(p, fmt.Errorf("sent close: %w", ce), handshake) -} - -type errClosing struct { - ce error -} - -func (e errClosing) Error() string { - return "already closing connection" -} - -func (c *Conn) writeClose(p []byte, ce error, handshake bool) error { - if c.isClosed() { - return fmt.Errorf("tried to close with %q but connection already closed: %w", ce, c.closeErr) - } - - if !c.closing.CAS(0, 1) { - // Normally, we would want to wait until the connection is closed, - // at least for when a user calls into Close, so we handle that case in - // the exported Close function. - // - // But for internal library usage, we always want to return early, e.g. - // if we are performing a close handshake and the peer sends their close frame, - // we do not want to block here waiting for c.closed to close because it won't, - // at least not until we return since the gorouine that will close it is this one. - return errClosing{ - ce: ce, - } - } - - // No matter what happens next, close error should be set. - c.setCloseErr(ce) - defer c.close(nil) - - err := c.writeControl(context.Background(), opClose, p) - if err != nil { - return err - } - - if handshake { - err = c.waitClose() - if CloseStatus(err) == -1 { - // waitClose exited not due to receiving a close frame. - return fmt.Errorf("failed to wait for peer close frame: %w", err) - } - } - - return nil -} - -func (c *Conn) waitClose() error { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - defer cancel() - - err := c.acquireLock(ctx, c.readLock) - if err != nil { - return err - } - defer c.releaseLock(c.readLock) - - if c.closeReceived != nil { - // goroutine reading just received the close. - return c.closeReceived - } - - b := bufpool.Get() - buf := b.Bytes() - buf = buf[:cap(buf)] - defer bufpool.Put(b) - - for { - if c.activeReader == nil || c.readerFrameEOF { - _, _, err := c.reader(ctx, false) - if err != nil { - return fmt.Errorf("failed to get reader: %w", err) - } - } - - r := readerFunc(c.activeReader.readUnlocked) - _, err = io.CopyBuffer(ioutil.Discard, r, buf) - if err != nil { - return err - } - } +func (c *Conn) deflateNegotiated() bool { + return c.copts != nil } // Ping sends a ping to the peer and waits for a pong. @@ -1056,9 +153,9 @@ func (c *Conn) waitClose() error { // // TCP Keepalives should suffice for most use cases. func (c *Conn) Ping(ctx context.Context) error { - p := c.pingCounter.Increment(1) + p := atomic.AddInt32(&c.pingCounter, 1) - err := c.ping(ctx, strconv.FormatInt(p, 10)) + err := c.ping(ctx, strconv.Itoa(int(p))) if err != nil { return fmt.Errorf("failed to ping: %w", err) } @@ -1078,7 +175,7 @@ func (c *Conn) ping(ctx context.Context, p string) error { c.activePingsMu.Unlock() }() - err := c.writeControl(ctx, opPing, []byte(p)) + err := c.cw.control(ctx, opPing, []byte(p)) if err != nil { return err } @@ -1095,109 +192,37 @@ func (c *Conn) ping(ctx context.Context, p string) error { } } -type readerFunc func(p []byte) (int, error) - -func (f readerFunc) Read(p []byte) (int, error) { - return f(p) -} - -type writerFunc func(p []byte) (int, error) - -func (f writerFunc) Write(p []byte) (int, error) { - return f(p) -} - -// extractBufioWriterBuf grabs the []byte backing a *bufio.Writer -// and stores it in c.writeBuf. -func (c *Conn) extractBufioWriterBuf(w io.Writer) { - c.bw.Reset(writerFunc(func(p2 []byte) (int, error) { - c.writeBuf = p2[:cap(p2)] - return len(p2), nil - })) - - c.bw.WriteByte(0) - c.bw.Flush() - - c.bw.Reset(w) -} - -var flateWriterPool = &sync.Pool{ - New: func() interface{} { - w, _ := flate.NewWriter(nil, flate.BestSpeed) - return w - }, -} - -func getFlateWriter(w io.Writer) *flate.Writer { - fw := flateWriterPool.Get().(*flate.Writer) - fw.Reset(w) - return fw -} - -func putFlateWriter(w *flate.Writer) { - flateWriterPool.Put(w) +type mu struct { + once sync.Once + ch chan struct{} } -var flateReaderPool = &sync.Pool{ - New: func() interface{} { - return flate.NewReader(nil) - }, -} - -func getFlateReader(r io.Reader) io.Reader { - fr := flateReaderPool.Get().(io.Reader) - fr.(flate.Resetter).Reset(r, nil) - return fr -} - -func putFlateReader(fr io.Reader) { - flateReaderPool.Put(fr) -} - -func (c *Conn) writeNoContextTakeOver() bool { - return c.client && c.copts.clientNoContextTakeover || !c.client && c.copts.serverNoContextTakeover -} - -func (c *Conn) readNoContextTakeOver() bool { - return !c.client && c.copts.clientNoContextTakeover || c.client && c.copts.serverNoContextTakeover -} - -type trimLastFourBytesWriter struct { - w io.Writer - tail []byte +func (m *mu) init() { + m.once.Do(func() { + m.ch = make(chan struct{}, 1) + }) } -func (w *trimLastFourBytesWriter) Write(p []byte) (int, error) { - extra := len(w.tail) + len(p) - 4 - - if extra <= 0 { - w.tail = append(w.tail, p...) - return len(p), nil - } - - // Now we need to write as many extra bytes as we can from the previous tail. - if extra > len(w.tail) { - extra = len(w.tail) - } - if extra > 0 { - _, err := w.Write(w.tail[:extra]) - if err != nil { - return 0, err - } - w.tail = w.tail[extra:] +func (m *mu) Lock(ctx context.Context) error { + m.init() + select { + case <-ctx.Done(): + return ctx.Err() + case m.ch <- struct{}{}: + return nil } +} - // If p is less than or equal to 4 bytes, - // all of it is is part of the tail. - if len(p) <= 4 { - w.tail = append(w.tail, p...) - return len(p), nil +func (m *mu) TryLock() bool { + m.init() + select { + case m.ch <- struct{}{}: + return true + default: + return false } +} - // Otherwise, only the last 4 bytes are. - w.tail = append(w.tail, p[len(p)-4:]...) - - p = p[:len(p)-4] - n, err := w.w.Write(p) - return n + 4, err +func (m *mu) Unlock() { + <-m.ch } diff --git a/conn_export_test.go b/conn_export_test.go deleted file mode 100644 index d5f5aa24..00000000 --- a/conn_export_test.go +++ /dev/null @@ -1,129 +0,0 @@ -// +build !js - -package websocket - -import ( - "bufio" - "context" - "fmt" -) - -type ( - Addr = websocketAddr - OpCode int -) - -const ( - OpClose = OpCode(opClose) - OpBinary = OpCode(opBinary) - OpText = OpCode(opText) - OpPing = OpCode(opPing) - OpPong = OpCode(opPong) - OpContinuation = OpCode(opContinuation) -) - -func (c *Conn) SetLogf(fn func(format string, v ...interface{})) { - c.logf = fn -} - -func (c *Conn) ReadFrame(ctx context.Context) (OpCode, []byte, error) { - h, err := c.readFrameHeader(ctx) - if err != nil { - return 0, nil, err - } - b := make([]byte, h.payloadLength) - _, err = c.readFramePayload(ctx, b) - if err != nil { - return 0, nil, err - } - if h.masked { - mask(h.maskKey, b) - } - return OpCode(h.opcode), b, nil -} - -func (c *Conn) WriteFrame(ctx context.Context, fin bool, opc OpCode, p []byte) (int, error) { - return c.writeFrame(ctx, fin, opcode(opc), p) -} - -// header represents a WebSocket frame header. -// See https://tools.ietf.org/html/rfc6455#section-5.2 -type Header struct { - Fin bool - Rsv1 bool - Rsv2 bool - Rsv3 bool - OpCode OpCode - - PayloadLength int64 -} - -func (c *Conn) WriteHeader(ctx context.Context, h Header) error { - headerBytes := writeHeader(c.writeHeaderBuf, header{ - fin: h.Fin, - rsv1: h.Rsv1, - rsv2: h.Rsv2, - rsv3: h.Rsv3, - opcode: opcode(h.OpCode), - payloadLength: h.PayloadLength, - masked: c.client, - }) - _, err := c.bw.Write(headerBytes) - if err != nil { - return fmt.Errorf("failed to write header: %w", err) - } - if h.Fin { - err = c.Flush() - if err != nil { - return err - } - } - return nil -} - -func (c *Conn) PingWithPayload(ctx context.Context, p string) error { - return c.ping(ctx, p) -} - -func (c *Conn) WriteHalfFrame(ctx context.Context) (int, error) { - return c.realWriteFrame(ctx, header{ - fin: true, - opcode: opBinary, - payloadLength: 10, - }, make([]byte, 5)) -} - -func (c *Conn) CloseUnderlyingConn() { - c.closer.Close() -} - -func (c *Conn) Flush() error { - return c.bw.Flush() -} - -func (c CloseError) Bytes() ([]byte, error) { - return c.bytes() -} - -func (c *Conn) BW() *bufio.Writer { - return c.bw -} - -func (c *Conn) WriteClose(ctx context.Context, code StatusCode, reason string) ([]byte, error) { - b, err := CloseError{ - Code: code, - Reason: reason, - }.Bytes() - if err != nil { - return nil, err - } - _, err = c.WriteFrame(ctx, true, OpClose, b) - if err != nil { - return nil, err - } - return b, nil -} - -func ParseClosePayload(p []byte) (CloseError, error) { - return parseClosePayload(p) -} diff --git a/conn_test.go b/conn_test.go index d03a7214..992c8861 100644 --- a/conn_test.go +++ b/conn_test.go @@ -3,969 +3,28 @@ package websocket_test import ( - "bytes" "context" - "encoding/binary" - "encoding/json" - "errors" "fmt" "io" - "io/ioutil" - "math/rand" - "net" "net/http" - "net/http/cookiejar" "net/http/httptest" - "net/url" - "os" - "os/exec" - "reflect" - "strconv" + "nhooyr.io/websocket/internal/assert" "strings" + "sync/atomic" "testing" "time" - "github.com/golang/protobuf/proto" - "github.com/golang/protobuf/ptypes" - "github.com/golang/protobuf/ptypes/timestamp" - "go.uber.org/multierr" - "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/assert" - "nhooyr.io/websocket/internal/wsecho" - "nhooyr.io/websocket/internal/wsgrace" - "nhooyr.io/websocket/wsjson" - "nhooyr.io/websocket/wspb" ) -func init() { - rand.Seed(time.Now().UnixNano()) -} - -func TestHandshake(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - client func(ctx context.Context, url string) error - server func(w http.ResponseWriter, r *http.Request) error - }{ - { - name: "badOrigin", - server: func(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, nil) - if err == nil { - c.Close(websocket.StatusInternalError, "") - return errors.New("expected error regarding bad origin") - } - return assertErrorContains(err, "not authorized") - }, - client: func(ctx context.Context, u string) error { - h := http.Header{} - h.Set("Origin", "http://unauthorized.com") - c, _, err := websocket.Dial(ctx, u, &websocket.DialOptions{ - HTTPHeader: h, - }) - if err == nil { - c.Close(websocket.StatusInternalError, "") - return errors.New("expected handshake failure") - } - return assertErrorContains(err, "403") - }, - }, - { - name: "acceptSecureOrigin", - server: func(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, nil) - if err != nil { - return err - } - c.Close(websocket.StatusNormalClosure, "") - return nil - }, - client: func(ctx context.Context, u string) error { - h := http.Header{} - h.Set("Origin", u) - c, _, err := websocket.Dial(ctx, u, &websocket.DialOptions{ - HTTPHeader: h, - }) - if err != nil { - return err - } - c.Close(websocket.StatusNormalClosure, "") - return nil - }, - }, - { - name: "acceptInsecureOrigin", - server: func(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ - InsecureSkipVerify: true, - }) - if err != nil { - return err - } - c.Close(websocket.StatusNormalClosure, "") - return nil - }, - client: func(ctx context.Context, u string) error { - h := http.Header{} - h.Set("Origin", "https://example.com") - c, _, err := websocket.Dial(ctx, u, &websocket.DialOptions{ - HTTPHeader: h, - }) - if err != nil { - return err - } - c.Close(websocket.StatusNormalClosure, "") - return nil - }, - }, - { - name: "cookies", - server: func(w http.ResponseWriter, r *http.Request) error { - cookie, err := r.Cookie("mycookie") - if err != nil { - return fmt.Errorf("request is missing mycookie: %w", err) - } - err = assert.Equalf("myvalue", cookie.Value, "unexpected cookie value") - if err != nil { - return err - } - c, err := websocket.Accept(w, r, nil) - if err != nil { - return err - } - c.Close(websocket.StatusNormalClosure, "") - return nil - }, - client: func(ctx context.Context, u string) error { - jar, err := cookiejar.New(nil) - if err != nil { - return fmt.Errorf("failed to create cookie jar: %w", err) - } - parsedURL, err := url.Parse(u) - if err != nil { - return fmt.Errorf("failed to parse url: %w", err) - } - parsedURL.Scheme = "http" - jar.SetCookies(parsedURL, []*http.Cookie{ - { - Name: "mycookie", - Value: "myvalue", - }, - }) - hc := &http.Client{ - Jar: jar, - } - c, _, err := websocket.Dial(ctx, u, &websocket.DialOptions{ - HTTPClient: hc, - }) - if err != nil { - return err - } - c.Close(websocket.StatusNormalClosure, "") - return nil - }, - }, - } - - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - s, closeFn := testServer(t, tc.server, false) - defer closeFn() - - wsURL := strings.Replace(s.URL, "http", "ws", 1) - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) - defer cancel() - - err := tc.client(ctx, wsURL) - if err != nil { - t.Fatalf("client failed: %+v", err) - } - }) - } -} - -func TestConn(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - - acceptOpts *websocket.AcceptOptions - server func(ctx context.Context, c *websocket.Conn) error - - dialOpts *websocket.DialOptions - response func(resp *http.Response) error - client func(ctx context.Context, c *websocket.Conn) error - }{ - { - name: "handshake", - acceptOpts: &websocket.AcceptOptions{ - Subprotocols: []string{"myproto"}, - }, - dialOpts: &websocket.DialOptions{ - Subprotocols: []string{"myproto"}, - }, - response: func(resp *http.Response) error { - headers := map[string]string{ - "Connection": "Upgrade", - "Upgrade": "websocket", - "Sec-WebSocket-Protocol": "myproto", - } - for h, exp := range headers { - value := resp.Header.Get(h) - err := assert.Equalf(exp, value, "unexpected value for header %v", h) - if err != nil { - return err - } - } - return nil - }, - }, - { - name: "handshake/defaultSubprotocol", - server: func(ctx context.Context, c *websocket.Conn) error { - return assertSubprotocol(c, "") - }, - client: func(ctx context.Context, c *websocket.Conn) error { - return assertSubprotocol(c, "") - }, - }, - { - name: "handshake/subprotocolPriority", - acceptOpts: &websocket.AcceptOptions{ - Subprotocols: []string{"echo", "lar"}, - }, - server: func(ctx context.Context, c *websocket.Conn) error { - return assertSubprotocol(c, "echo") - }, - dialOpts: &websocket.DialOptions{ - Subprotocols: []string{"poof", "echo"}, - }, - client: func(ctx context.Context, c *websocket.Conn) error { - return assertSubprotocol(c, "echo") - }, - }, - { - name: "closeError", - server: func(ctx context.Context, c *websocket.Conn) error { - return wsjson.Write(ctx, c, "hello") - }, - client: func(ctx context.Context, c *websocket.Conn) error { - err := assertJSONRead(ctx, c, "hello") - if err != nil { - return err - } - - _, _, err = c.Reader(ctx) - return assertCloseStatus(err, websocket.StatusInternalError) - }, - }, - { - name: "netConn", - server: func(ctx context.Context, c *websocket.Conn) error { - nc := websocket.NetConn(ctx, c, websocket.MessageBinary) - defer nc.Close() - - nc.SetWriteDeadline(time.Time{}) - time.Sleep(1) - nc.SetWriteDeadline(time.Now().Add(time.Second * 15)) - - err := assert.Equalf(websocket.Addr{}, nc.LocalAddr(), "net conn local address is not equal to websocket.Addr") - if err != nil { - return err - } - err = assert.Equalf(websocket.Addr{}, nc.RemoteAddr(), "net conn remote address is not equal to websocket.Addr") - if err != nil { - return err - } - - for i := 0; i < 3; i++ { - _, err := nc.Write([]byte("hello")) - if err != nil { - return err - } - } - - return nil - }, - client: func(ctx context.Context, c *websocket.Conn) error { - nc := websocket.NetConn(ctx, c, websocket.MessageBinary) - - nc.SetReadDeadline(time.Time{}) - time.Sleep(1) - nc.SetReadDeadline(time.Now().Add(time.Second * 15)) - - for i := 0; i < 3; i++ { - err := assertNetConnRead(nc, "hello") - if err != nil { - return err - } - } - - // Ensure the close frame is converted to an EOF and multiple read's after all return EOF. - err2 := assertNetConnRead(nc, "hello") - err := assert.Equalf(io.EOF, err2, "unexpected error") - if err != nil { - return err - } - - err2 = assertNetConnRead(nc, "hello") - return assert.Equalf(io.EOF, err2, "unexpected error") - }, - }, - { - name: "netConn/badReadMsgType", - server: func(ctx context.Context, c *websocket.Conn) error { - nc := websocket.NetConn(ctx, c, websocket.MessageBinary) - - nc.SetDeadline(time.Now().Add(time.Second * 15)) - - _, err := nc.Read(make([]byte, 1)) - return assertErrorContains(err, "unexpected frame type") - }, - client: func(ctx context.Context, c *websocket.Conn) error { - err := wsjson.Write(ctx, c, "meow") - if err != nil { - return err - } - - _, _, err = c.Read(ctx) - return assertCloseStatus(err, websocket.StatusUnsupportedData) - }, - }, - { - name: "netConn/badRead", - server: func(ctx context.Context, c *websocket.Conn) error { - nc := websocket.NetConn(ctx, c, websocket.MessageBinary) - defer nc.Close() - - nc.SetDeadline(time.Now().Add(time.Second * 15)) - - _, err2 := nc.Read(make([]byte, 1)) - err := assertCloseStatus(err2, websocket.StatusBadGateway) - if err != nil { - return err - } - - _, err2 = nc.Write([]byte{0xff}) - return assertErrorContains(err2, "websocket closed") - }, - client: func(ctx context.Context, c *websocket.Conn) error { - return c.Close(websocket.StatusBadGateway, "") - }, - }, - { - name: "wsjson/echo", - server: func(ctx context.Context, c *websocket.Conn) error { - return wsjson.Write(ctx, c, "meow") - }, - client: func(ctx context.Context, c *websocket.Conn) error { - return assertJSONRead(ctx, c, "meow") - }, - }, - { - name: "protobuf/echo", - server: func(ctx context.Context, c *websocket.Conn) error { - return wspb.Write(ctx, c, ptypes.DurationProto(100)) - }, - client: func(ctx context.Context, c *websocket.Conn) error { - return assertProtobufRead(ctx, c, ptypes.DurationProto(100)) - }, - }, - { - name: "ping", - server: func(ctx context.Context, c *websocket.Conn) error { - ctx = c.CloseRead(ctx) - - err := c.Ping(ctx) - if err != nil { - return err - } - - err = wsjson.Write(ctx, c, "hi") - if err != nil { - return err - } - - <-ctx.Done() - err = c.Ping(context.Background()) - return assertCloseStatus(err, websocket.StatusNormalClosure) - }, - client: func(ctx context.Context, c *websocket.Conn) error { - // We read a message from the connection and then keep reading until - // the Ping completes. - pingErrc := make(chan error, 1) - go func() { - pingErrc <- c.Ping(ctx) - }() - - // Once this completes successfully, that means they sent their ping and we responded to it. - err := assertJSONRead(ctx, c, "hi") - if err != nil { - return err - } - - // Now we need to ensure we're reading for their pong from our ping. - // Need new var to not race with above goroutine. - ctx2 := c.CloseRead(ctx) - - // Now we wait for our pong. - select { - case err = <-pingErrc: - return err - case <-ctx2.Done(): - return fmt.Errorf("failed to wait for pong: %w", ctx2.Err()) - } - }, - }, - { - name: "readLimit", - server: func(ctx context.Context, c *websocket.Conn) error { - _, _, err2 := c.Read(ctx) - return assertErrorContains(err2, "read limited at 32768 bytes") - }, - client: func(ctx context.Context, c *websocket.Conn) error { - err := c.Write(ctx, websocket.MessageBinary, []byte(strings.Repeat("x", 32769))) - if err != nil { - return err - } - - _, _, err2 := c.Read(ctx) - return assertCloseStatus(err2, websocket.StatusMessageTooBig) - }, - }, - { - name: "wsjson/binary", - server: func(ctx context.Context, c *websocket.Conn) error { - var v interface{} - err2 := wsjson.Read(ctx, c, &v) - return assertErrorContains(err2, "unexpected frame type") - }, - client: func(ctx context.Context, c *websocket.Conn) error { - return wspb.Write(ctx, c, ptypes.DurationProto(100)) - }, - }, - { - name: "wsjson/badRead", - server: func(ctx context.Context, c *websocket.Conn) error { - var v interface{} - err2 := wsjson.Read(ctx, c, &v) - return assertErrorContains(err2, "failed to unmarshal json") - }, - client: func(ctx context.Context, c *websocket.Conn) error { - return c.Write(ctx, websocket.MessageText, []byte("notjson")) - }, - }, - { - name: "wsjson/badWrite", - server: func(ctx context.Context, c *websocket.Conn) error { - _, _, err2 := c.Read(ctx) - return assertCloseStatus(err2, websocket.StatusNormalClosure) - }, - client: func(ctx context.Context, c *websocket.Conn) error { - err := wsjson.Write(ctx, c, fmt.Println) - return assertErrorContains(err, "failed to encode json") - }, - }, - { - name: "wspb/text", - server: func(ctx context.Context, c *websocket.Conn) error { - var v proto.Message - err := wspb.Read(ctx, c, v) - return assertErrorContains(err, "unexpected frame type") - }, - client: func(ctx context.Context, c *websocket.Conn) error { - return wsjson.Write(ctx, c, "hi") - }, - }, - { - name: "wspb/badRead", - server: func(ctx context.Context, c *websocket.Conn) error { - var v timestamp.Timestamp - err := wspb.Read(ctx, c, &v) - return assertErrorContains(err, "failed to unmarshal protobuf") - }, - client: func(ctx context.Context, c *websocket.Conn) error { - return c.Write(ctx, websocket.MessageBinary, []byte("notpb")) - }, - }, - { - name: "wspb/badWrite", - server: func(ctx context.Context, c *websocket.Conn) error { - _, _, err := c.Read(ctx) - return assertCloseStatus(err, websocket.StatusNormalClosure) - }, - client: func(ctx context.Context, c *websocket.Conn) error { - err := wspb.Write(ctx, c, nil) - return assertErrorIs(proto.ErrNil, err) - }, - }, - { - name: "badClose", - server: func(ctx context.Context, c *websocket.Conn) error { - return c.Close(9999, "") - }, - client: func(ctx context.Context, c *websocket.Conn) error { - _, _, err := c.Read(ctx) - return assertCloseStatus(err, websocket.StatusInternalError) - }, - }, - { - name: "pingTimeout", - server: func(ctx context.Context, c *websocket.Conn) error { - ctx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - err := c.Ping(ctx) - return assertErrorIs(context.DeadlineExceeded, err) - }, - client: func(ctx context.Context, c *websocket.Conn) error { - _, _, err := c.Read(ctx) - err1 := assertErrorContains(err, "connection reset") - err2 := assertErrorIs(io.EOF, err) - if err1 != nil || err2 != nil { - return nil - } - return multierr.Combine(err1, err2) - }, - }, - { - name: "writeTimeout", - server: func(ctx context.Context, c *websocket.Conn) error { - c.Writer(ctx, websocket.MessageBinary) - - ctx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - err := c.Write(ctx, websocket.MessageBinary, []byte("meow")) - return assertErrorIs(context.DeadlineExceeded, err) - }, - client: func(ctx context.Context, c *websocket.Conn) error { - _, _, err := c.Read(ctx) - return assertErrorIs(io.EOF, err) - }, - }, - { - name: "readTimeout", - server: func(ctx context.Context, c *websocket.Conn) error { - ctx, cancel := context.WithTimeout(ctx, time.Second) - defer cancel() - _, _, err := c.Read(ctx) - return assertErrorIs(context.DeadlineExceeded, err) - }, - client: func(ctx context.Context, c *websocket.Conn) error { - _, _, err := c.Read(ctx) - return assertErrorIs(websocket.CloseError{ - Code: websocket.StatusPolicyViolation, - Reason: "read timed out", - }, err) - }, - }, - { - name: "badOpCode", - server: func(ctx context.Context, c *websocket.Conn) error { - _, err := c.WriteFrame(ctx, true, 13, []byte("meow")) - if err != nil { - return err - } - _, _, err = c.Read(ctx) - return assertErrorContains(err, "unknown opcode") - }, - client: func(ctx context.Context, c *websocket.Conn) error { - _, _, err := c.Read(ctx) - return assertErrorContains(err, "unknown opcode") - }, - }, - { - name: "noRsv", - server: func(ctx context.Context, c *websocket.Conn) error { - _, err := c.WriteFrame(ctx, true, 99, []byte("meow")) - if err != nil { - return err - } - _, _, err = c.Read(ctx) - return assertCloseStatus(err, websocket.StatusProtocolError) - }, - client: func(ctx context.Context, c *websocket.Conn) error { - _, _, err := c.Read(ctx) - if err == nil || !strings.Contains(err.Error(), "rsv") { - return fmt.Errorf("expected error that contains rsv: %+v", err) - } - return nil - }, - }, - { - name: "largeControlFrame", - server: func(ctx context.Context, c *websocket.Conn) error { - err := c.WriteHeader(ctx, websocket.Header{ - Fin: true, - OpCode: websocket.OpClose, - PayloadLength: 4096, - }) - if err != nil { - return err - } - _, _, err = c.Read(ctx) - return assertCloseStatus(err, websocket.StatusProtocolError) - }, - client: func(ctx context.Context, c *websocket.Conn) error { - _, _, err := c.Read(ctx) - return assertErrorContains(err, "too big") - }, - }, - { - name: "fragmentedControlFrame", - server: func(ctx context.Context, c *websocket.Conn) error { - _, err := c.WriteFrame(ctx, false, websocket.OpPing, []byte(strings.Repeat("x", 32))) - if err != nil { - return err - } - err = c.Flush() - if err != nil { - return err - } - _, _, err = c.Read(ctx) - return assertCloseStatus(err, websocket.StatusProtocolError) - }, - client: func(ctx context.Context, c *websocket.Conn) error { - _, _, err := c.Read(ctx) - return assertErrorContains(err, "fragmented") - }, - }, - { - name: "invalidClosePayload", - server: func(ctx context.Context, c *websocket.Conn) error { - _, err := c.WriteFrame(ctx, true, websocket.OpClose, []byte{0x17, 0x70}) - if err != nil { - return err - } - _, _, err = c.Read(ctx) - return assertCloseStatus(err, websocket.StatusProtocolError) - }, - client: func(ctx context.Context, c *websocket.Conn) error { - _, _, err := c.Read(ctx) - return assertErrorContains(err, "invalid status code") - }, - }, - { - name: "doubleReader", - server: func(ctx context.Context, c *websocket.Conn) error { - _, r, err := c.Reader(ctx) - if err != nil { - return err - } - p := make([]byte, 10) - _, err = io.ReadFull(r, p) - if err != nil { - return err - } - _, _, err = c.Reader(ctx) - return assertErrorContains(err, "previous message not read to completion") - }, - client: func(ctx context.Context, c *websocket.Conn) error { - err := c.Write(ctx, websocket.MessageBinary, []byte(strings.Repeat("x", 11))) - if err != nil { - return err - } - _, _, err = c.Read(ctx) - return assertCloseStatus(err, websocket.StatusInternalError) - }, - }, - { - name: "doubleFragmentedReader", - server: func(ctx context.Context, c *websocket.Conn) error { - _, r, err := c.Reader(ctx) - if err != nil { - return err - } - p := make([]byte, 10) - _, err = io.ReadFull(r, p) - if err != nil { - return err - } - _, _, err = c.Reader(ctx) - return assertErrorContains(err, "previous message not read to completion") - }, - client: func(ctx context.Context, c *websocket.Conn) error { - w, err := c.Writer(ctx, websocket.MessageBinary) - if err != nil { - return err - } - _, err = w.Write([]byte(strings.Repeat("x", 10))) - if err != nil { - return fmt.Errorf("expected non nil error") - } - err = c.Flush() - if err != nil { - return fmt.Errorf("failed to flush: %w", err) - } - _, err = w.Write([]byte(strings.Repeat("x", 10))) - if err != nil { - return fmt.Errorf("expected non nil error") - } - err = c.Flush() - if err != nil { - return fmt.Errorf("failed to flush: %w", err) - } - _, _, err = c.Read(ctx) - return assertCloseStatus(err, websocket.StatusInternalError) - }, - }, - { - name: "newMessageInFragmentedMessage", - server: func(ctx context.Context, c *websocket.Conn) error { - _, r, err := c.Reader(ctx) - if err != nil { - return err - } - p := make([]byte, 10) - _, err = io.ReadFull(r, p) - if err != nil { - return err - } - _, _, err = c.Reader(ctx) - return assertErrorContains(err, "received new data message without finishing") - }, - client: func(ctx context.Context, c *websocket.Conn) error { - w, err := c.Writer(ctx, websocket.MessageBinary) - if err != nil { - return err - } - _, err = w.Write([]byte(strings.Repeat("x", 10))) - if err != nil { - return fmt.Errorf("expected non nil error") - } - err = c.Flush() - if err != nil { - return fmt.Errorf("failed to flush: %w", err) - } - _, err = c.WriteFrame(ctx, true, websocket.OpBinary, []byte(strings.Repeat("x", 10))) - if err != nil { - return fmt.Errorf("expected non nil error") - } - _, _, err = c.Read(ctx) - return assertErrorContains(err, "received new data message without finishing") - }, - }, - { - name: "continuationFrameWithoutDataFrame", - server: func(ctx context.Context, c *websocket.Conn) error { - _, _, err := c.Reader(ctx) - return assertErrorContains(err, "received continuation frame not after data") - }, - client: func(ctx context.Context, c *websocket.Conn) error { - _, err := c.WriteFrame(ctx, false, websocket.OpContinuation, []byte(strings.Repeat("x", 10))) - return err - }, - }, - { - name: "readBeforeEOF", - server: func(ctx context.Context, c *websocket.Conn) error { - _, r, err := c.Reader(ctx) - if err != nil { - return err - } - var v interface{} - d := json.NewDecoder(r) - err = d.Decode(&v) - if err != nil { - return err - } - err = assert.Equalf("hi", v, "unexpected JSON") - if err != nil { - return err - } - _, b, err := c.Read(ctx) - if err != nil { - return err - } - return assert.Equalf("hi", string(b), "unexpected JSON") - }, - client: func(ctx context.Context, c *websocket.Conn) error { - err := wsjson.Write(ctx, c, "hi") - if err != nil { - return err - } - return c.Write(ctx, websocket.MessageText, []byte("hi")) - }, - }, - { - name: "newMessageInFragmentedMessage2", - server: func(ctx context.Context, c *websocket.Conn) error { - _, r, err := c.Reader(ctx) - if err != nil { - return err - } - p := make([]byte, 11) - _, err = io.ReadFull(r, p) - return assertErrorContains(err, "received new data message without finishing") - }, - client: func(ctx context.Context, c *websocket.Conn) error { - w, err := c.Writer(ctx, websocket.MessageBinary) - if err != nil { - return err - } - _, err = w.Write([]byte(strings.Repeat("x", 10))) - if err != nil { - return fmt.Errorf("expected non nil error") - } - err = c.Flush() - if err != nil { - return fmt.Errorf("failed to flush: %w", err) - } - _, err = c.WriteFrame(ctx, true, websocket.OpBinary, []byte(strings.Repeat("x", 10))) - if err != nil { - return fmt.Errorf("expected non nil error") - } - _, _, err = c.Read(ctx) - return assertCloseStatus(err, websocket.StatusProtocolError) - }, - }, - { - name: "doubleRead", - server: func(ctx context.Context, c *websocket.Conn) error { - _, r, err := c.Reader(ctx) - if err != nil { - return err - } - _, err = ioutil.ReadAll(r) - if err != nil { - return err - } - _, err = r.Read(make([]byte, 1)) - return assertErrorContains(err, "cannot use EOFed reader") - }, - client: func(ctx context.Context, c *websocket.Conn) error { - return c.Write(ctx, websocket.MessageBinary, []byte("hi")) - }, - }, - { - name: "eofInPayload", - server: func(ctx context.Context, c *websocket.Conn) error { - _, _, err := c.Read(ctx) - return assertErrorContains(err, "failed to read frame payload") - }, - client: func(ctx context.Context, c *websocket.Conn) error { - _, err := c.WriteHalfFrame(ctx) - if err != nil { - return err - } - c.CloseUnderlyingConn() - return nil - }, - }, - { - name: "closeHandshake", - server: func(ctx context.Context, c *websocket.Conn) error { - return c.Close(websocket.StatusNormalClosure, "") - }, - client: func(ctx context.Context, c *websocket.Conn) error { - return c.Close(websocket.StatusNormalClosure, "") - }, - }, - { - // Issue #164 - name: "closeHandshake_concurrentRead", - server: func(ctx context.Context, c *websocket.Conn) error { - _, _, err := c.Read(ctx) - return assertCloseStatus(err, websocket.StatusNormalClosure) - }, - client: func(ctx context.Context, c *websocket.Conn) error { - errc := make(chan error, 1) - go func() { - _, _, err := c.Read(ctx) - errc <- err - }() - - err := c.Close(websocket.StatusNormalClosure, "") - if err != nil { - return err - } - - err = <-errc - return assertCloseStatus(err, websocket.StatusNormalClosure) - }, - }, - } - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - // Run random tests over TLS. - tls := rand.Intn(2) == 1 - - s, closeFn := testServer(t, func(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, tc.acceptOpts) - if err != nil { - return err - } - defer c.Close(websocket.StatusInternalError, "") - c.SetLogf(t.Logf) - if tc.server == nil { - return nil - } - return tc.server(r.Context(), c) - }, tls) - defer closeFn() - - wsURL := strings.Replace(s.URL, "http", "ws", 1) - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) - defer cancel() - - opts := tc.dialOpts - if tls { - if opts == nil { - opts = &websocket.DialOptions{} - } - opts.HTTPClient = s.Client() - } - - c, resp, err := websocket.Dial(ctx, wsURL, opts) - if err != nil { - t.Fatal(err) - } - defer c.Close(websocket.StatusInternalError, "") - c.SetLogf(t.Logf) - - if tc.response != nil { - err = tc.response(resp) - if err != nil { - t.Fatalf("response asserter failed: %+v", err) - } - } - - if tc.client != nil { - err = tc.client(ctx, c) - if err != nil { - t.Fatalf("client failed: %+v", err) - } - } - - c.Close(websocket.StatusNormalClosure, "") - }) - } -} - -func testServer(tb testing.TB, fn func(w http.ResponseWriter, r *http.Request) error, tls bool) (s *httptest.Server, closeFn func()) { - h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - err := fn(w, r) - if err != nil { - tb.Errorf("server failed: %+v", err) - } - }) +func testServer(tb testing.TB, fn func(w http.ResponseWriter, r *http.Request), tls bool) (s *httptest.Server, closeFn func()) { + h := http.HandlerFunc(fn) if tls { s = httptest.NewTLSServer(h) } else { s = httptest.NewServer(h) } - closeFn2 := wsgrace.Grace(s.Config) + closeFn2 := wsgrace(s.Config) return s, func() { err := closeFn2() if err != nil { @@ -974,1417 +33,112 @@ func testServer(tb testing.TB, fn func(w http.ResponseWriter, r *http.Request) e } } -func TestAutobahn(t *testing.T) { - t.Parallel() - - run := func(t *testing.T, name string, fn func(ctx context.Context, c *websocket.Conn) error) { - run2 := func(t *testing.T, testingClient bool) { - // Run random tests over TLS. - tls := rand.Intn(2) == 1 - - s, closeFn := testServer(t, func(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ - Subprotocols: []string{"echo"}, - }) - if err != nil { - return err - } - defer c.Close(websocket.StatusInternalError, "") - - ctx := r.Context() - if testingClient { - err = wsecho.Loop(ctx, c) - if err != nil { - t.Logf("failed to wsecho: %+v", err) - } - return nil - } - - c.SetReadLimit(1 << 30) - err = fn(ctx, c) - if err != nil { - return err - } - c.Close(websocket.StatusNormalClosure, "") - return nil - }, tls) - defer closeFn() - - wsURL := strings.Replace(s.URL, "http", "ws", 1) - - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - - opts := &websocket.DialOptions{ - Subprotocols: []string{"echo"}, - } - if tls { - opts.HTTPClient = s.Client() - } - - c, _, err := websocket.Dial(ctx, wsURL, opts) - if err != nil { - t.Fatal(err) - } - defer c.Close(websocket.StatusInternalError, "") - - if testingClient { - c.SetReadLimit(1 << 30) - err = fn(ctx, c) - if err != nil { - t.Fatalf("client failed: %+v", err) - } - c.Close(websocket.StatusNormalClosure, "") - return - } - - err = wsecho.Loop(ctx, c) - if err != nil { - t.Logf("failed to wsecho: %+v", err) - } - } - t.Run(name, func(t *testing.T) { - t.Parallel() +// grace wraps s.Handler to gracefully shutdown WebSocket connections. +// The returned function must be used to close the server instead of s.Close. +func wsgrace(s *http.Server) (closeFn func() error) { + h := s.Handler + var conns int64 + s.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + atomic.AddInt64(&conns, 1) + defer atomic.AddInt64(&conns, -1) - run2(t, true) - }) - } - - // Section 1. - t.Run("echo", func(t *testing.T) { - t.Parallel() - - lengths := []int{ - 0, - 125, - 126, - 127, - 128, - 65535, - 65536, - 65536, - } - run := func(typ websocket.MessageType) { - for i, l := range lengths { - l := l - run(t, fmt.Sprintf("%v/%v", typ, l), func(ctx context.Context, c *websocket.Conn) error { - p := randBytes(l) - if i == len(lengths)-1 { - w, err := c.Writer(ctx, typ) - if err != nil { - return err - } - for i := 0; i < l; { - j := i + 997 - if j > l { - j = l - } - _, err = w.Write(p[i:j]) - if err != nil { - return err - } + ctx, cancel := context.WithTimeout(r.Context(), time.Second*5) + defer cancel() - i = j - } + r = r.WithContext(ctx) - err = w.Close() - if err != nil { - return err - } - } else { - err := c.Write(ctx, typ, p) - if err != nil { - return err - } - } - actTyp, p2, err := c.Read(ctx) - if err != nil { - return err - } - err = assert.Equalf(typ, actTyp, "unexpected message type") - if err != nil { - return err - } - return assert.Equalf(p, p2, "unexpected message") - }) - } - } - - run(websocket.MessageText) - run(websocket.MessageBinary) + h.ServeHTTP(w, r) }) - // Section 2. - t.Run("pingPong", func(t *testing.T) { - t.Parallel() - - run(t, "emptyPayload", func(ctx context.Context, c *websocket.Conn) error { - ctx = c.CloseRead(ctx) - return c.PingWithPayload(ctx, "") - }) - run(t, "smallTextPayload", func(ctx context.Context, c *websocket.Conn) error { - ctx = c.CloseRead(ctx) - return c.PingWithPayload(ctx, "hi") - }) - run(t, "smallBinaryPayload", func(ctx context.Context, c *websocket.Conn) error { - ctx = c.CloseRead(ctx) - p := bytes.Repeat([]byte{0xFE}, 16) - return c.PingWithPayload(ctx, string(p)) - }) - run(t, "largeBinaryPayload", func(ctx context.Context, c *websocket.Conn) error { - ctx = c.CloseRead(ctx) - p := bytes.Repeat([]byte{0xFE}, 125) - return c.PingWithPayload(ctx, string(p)) - }) - run(t, "tooLargeBinaryPayload", func(ctx context.Context, c *websocket.Conn) error { - c.CloseRead(ctx) - p := bytes.Repeat([]byte{0xFE}, 126) - err := c.PingWithPayload(ctx, string(p)) - return assertCloseStatus(err, websocket.StatusProtocolError) - }) - run(t, "streamPingPayload", func(ctx context.Context, c *websocket.Conn) error { - err := assertStreamPing(ctx, c, 125) - if err != nil { - return err - } - return c.Close(websocket.StatusNormalClosure, "") - }) - t.Run("unsolicitedPong", func(t *testing.T) { - t.Parallel() - - var testCases = []struct { - name string - pongPayload string - ping bool - }{ - { - name: "noPayload", - pongPayload: "", - }, - { - name: "payload", - pongPayload: "hi", - }, - { - name: "pongThenPing", - pongPayload: "hi", - ping: true, - }, - } - for _, tc := range testCases { - tc := tc - run(t, tc.name, func(ctx context.Context, c *websocket.Conn) error { - _, err := c.WriteFrame(ctx, true, websocket.OpPong, []byte(tc.pongPayload)) - if err != nil { - return err - } - if tc.ping { - _, err := c.WriteFrame(ctx, true, websocket.OpPing, []byte("meow")) - if err != nil { - return err - } - err = assertReadFrame(ctx, c, websocket.OpPong, []byte("meow")) - if err != nil { - return err - } - } - return c.Close(websocket.StatusNormalClosure, "") - }) - } - }) - run(t, "tenPings", func(ctx context.Context, c *websocket.Conn) error { - ctx = c.CloseRead(ctx) - - for i := 0; i < 10; i++ { - err := c.Ping(ctx) - if err != nil { - return err - } - } + return func() error { + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() - _, err := c.WriteClose(ctx, websocket.StatusNormalClosure, "") - if err != nil { - return err - } - <-ctx.Done() - - err = c.Ping(context.Background()) - return assertCloseStatus(err, websocket.StatusNormalClosure) - }) - - run(t, "tenStreamedPings", func(ctx context.Context, c *websocket.Conn) error { - for i := 0; i < 10; i++ { - err := assertStreamPing(ctx, c, 125) - if err != nil { - return err - } - } - - return c.Close(websocket.StatusNormalClosure, "") - }) - }) - - // Section 3. - // We skip the per octet sending as it will add too much complexity. - t.Run("reserved", func(t *testing.T) { - t.Parallel() - - var testCases = []struct { - name string - header websocket.Header - }{ - { - name: "rsv1", - header: websocket.Header{ - Fin: true, - Rsv1: true, - OpCode: websocket.OpClose, - PayloadLength: 0, - }, - }, - { - name: "rsv2", - header: websocket.Header{ - Fin: true, - Rsv2: true, - OpCode: websocket.OpPong, - PayloadLength: 0, - }, - }, - { - name: "rsv3", - header: websocket.Header{ - Fin: true, - Rsv3: true, - OpCode: websocket.OpBinary, - PayloadLength: 0, - }, - }, - { - name: "rsvAll", - header: websocket.Header{ - Fin: true, - Rsv1: true, - Rsv2: true, - Rsv3: true, - OpCode: websocket.OpText, - PayloadLength: 0, - }, - }, - } - for _, tc := range testCases { - tc := tc - run(t, tc.name, func(ctx context.Context, c *websocket.Conn) error { - err := assertEcho(ctx, c, websocket.MessageText, 4096) - if err != nil { - return err - } - err = c.WriteHeader(ctx, tc.header) - if err != nil { - return err - } - err = c.Flush() - if err != nil { - return err - } - _, err = c.WriteFrame(ctx, true, websocket.OpPing, []byte("wtf")) - if err != nil { - return err - } - return assertReadCloseFrame(ctx, c, websocket.StatusProtocolError) - }) - } - }) - - // Section 4. - t.Run("opcodes", func(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - opcode websocket.OpCode - payload bool - echo bool - ping bool - }{ - // Section 1. - { - name: "3", - opcode: 3, - }, - { - name: "4", - opcode: 4, - payload: true, - }, - { - name: "5", - opcode: 5, - echo: true, - ping: true, - }, - { - name: "6", - opcode: 6, - payload: true, - echo: true, - ping: true, - }, - { - name: "7", - opcode: 7, - payload: true, - echo: true, - ping: true, - }, - - // Section 2. - { - name: "11", - opcode: 11, - }, - { - name: "12", - opcode: 12, - payload: true, - }, - { - name: "13", - opcode: 13, - payload: true, - echo: true, - ping: true, - }, - { - name: "14", - opcode: 14, - payload: true, - echo: true, - ping: true, - }, - { - name: "15", - opcode: 15, - payload: true, - echo: true, - ping: true, - }, - } - for _, tc := range testCases { - tc := tc - run(t, tc.name, func(ctx context.Context, c *websocket.Conn) error { - if tc.echo { - err := assertEcho(ctx, c, websocket.MessageText, 4096) - if err != nil { - return err - } - } - - p := []byte(nil) - if tc.payload { - p = randBytes(rand.Intn(4096) + 1) - } - _, err := c.WriteFrame(ctx, true, tc.opcode, p) - if err != nil { - return err - } - if tc.ping { - _, err = c.WriteFrame(ctx, true, websocket.OpPing, []byte("wtf")) - if err != nil { - return err - } - } - return assertReadCloseFrame(ctx, c, websocket.StatusProtocolError) - }) - } - }) - - // Section 5. - t.Run("fragmentation", func(t *testing.T) { - t.Parallel() - - // 5.1 to 5.8 - testCases := []struct { - name string - opcode websocket.OpCode - success bool - pingInBetween bool - }{ - { - name: "ping", - opcode: websocket.OpPing, - success: false, - }, - { - name: "pong", - opcode: websocket.OpPong, - success: false, - }, - { - name: "text", - opcode: websocket.OpText, - success: true, - }, - { - name: "textPing", - opcode: websocket.OpText, - success: true, - pingInBetween: true, - }, - } - for _, tc := range testCases { - tc := tc - run(t, tc.name, func(ctx context.Context, c *websocket.Conn) error { - p1 := randBytes(16) - _, err := c.WriteFrame(ctx, false, tc.opcode, p1) - if err != nil { - return err - } - err = c.BW().Flush() - if err != nil { - return err - } - if !tc.success { - _, _, err = c.Read(ctx) - return assertCloseStatus(err, websocket.StatusProtocolError) - } - - if tc.pingInBetween { - _, err = c.WriteFrame(ctx, true, websocket.OpPing, p1) - if err != nil { - return err - } - } - - p2 := randBytes(16) - _, err = c.WriteFrame(ctx, true, websocket.OpContinuation, p2) - if err != nil { - return err - } - - err = assertReadFrame(ctx, c, tc.opcode, p1) - if err != nil { - return err - } - - if tc.pingInBetween { - err = assertReadFrame(ctx, c, websocket.OpPong, p1) - if err != nil { - return err - } - } - - return assertReadFrame(ctx, c, websocket.OpContinuation, p2) - }) + err := s.Shutdown(ctx) + if err != nil { + return fmt.Errorf("server shutdown failed: %v", err) } - t.Run("unexpectedContinuation", func(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - fin bool - textFirst bool - }{ - { - name: "fin", - fin: true, - }, - { - name: "noFin", - fin: false, - }, - { - name: "echoFirst", - fin: false, - textFirst: true, - }, - // The rest of the tests in this section get complicated and do not inspire much confidence. - } - - for _, tc := range testCases { - tc := tc - run(t, tc.name, func(ctx context.Context, c *websocket.Conn) error { - if tc.textFirst { - w, err := c.Writer(ctx, websocket.MessageText) - if err != nil { - return err - } - p1 := randBytes(32) - _, err = w.Write(p1) - if err != nil { - return err - } - p2 := randBytes(32) - _, err = w.Write(p2) - if err != nil { - return err - } - err = w.Close() - if err != nil { - return err - } - err = assertReadFrame(ctx, c, websocket.OpText, p1) - if err != nil { - return err - } - err = assertReadFrame(ctx, c, websocket.OpContinuation, p2) - if err != nil { - return err - } - err = assertReadFrame(ctx, c, websocket.OpContinuation, []byte{}) - if err != nil { - return err - } - } - - _, err := c.WriteFrame(ctx, tc.fin, websocket.OpContinuation, randBytes(32)) - if err != nil { - return err - } - err = c.BW().Flush() - if err != nil { - return err - } - - return assertReadCloseFrame(ctx, c, websocket.StatusProtocolError) - }) - } - - run(t, "doubleText", func(ctx context.Context, c *websocket.Conn) error { - p1 := randBytes(32) - _, err := c.WriteFrame(ctx, false, websocket.OpText, p1) - if err != nil { - return err - } - _, err = c.WriteFrame(ctx, true, websocket.OpText, randBytes(32)) - if err != nil { - return err - } - err = assertReadFrame(ctx, c, websocket.OpText, p1) - if err != nil { - return err - } - return assertReadCloseFrame(ctx, c, websocket.StatusProtocolError) - }) - - run(t, "5.19", func(ctx context.Context, c *websocket.Conn) error { - p1 := randBytes(32) - p2 := randBytes(32) - p3 := randBytes(32) - p4 := randBytes(32) - p5 := randBytes(32) - - _, err := c.WriteFrame(ctx, false, websocket.OpText, p1) - if err != nil { - return err - } - _, err = c.WriteFrame(ctx, false, websocket.OpContinuation, p2) - if err != nil { - return err - } - - _, err = c.WriteFrame(ctx, true, websocket.OpPing, p1) - if err != nil { - return err - } - - time.Sleep(time.Second) - - _, err = c.WriteFrame(ctx, false, websocket.OpContinuation, p3) - if err != nil { - return err - } - _, err = c.WriteFrame(ctx, false, websocket.OpContinuation, p4) - if err != nil { - return err - } - - _, err = c.WriteFrame(ctx, true, websocket.OpPing, p1) - if err != nil { - return err - } - - _, err = c.WriteFrame(ctx, true, websocket.OpContinuation, p5) - if err != nil { - return err - } - - err = assertReadFrame(ctx, c, websocket.OpText, p1) - if err != nil { - return err - } - err = assertReadFrame(ctx, c, websocket.OpContinuation, p2) - if err != nil { - return err - } - err = assertReadFrame(ctx, c, websocket.OpPong, p1) - if err != nil { - return err - } - err = assertReadFrame(ctx, c, websocket.OpContinuation, p3) - if err != nil { - return err - } - err = assertReadFrame(ctx, c, websocket.OpContinuation, p4) - if err != nil { - return err - } - err = assertReadFrame(ctx, c, websocket.OpPong, p1) - if err != nil { - return err - } - err = assertReadFrame(ctx, c, websocket.OpContinuation, p5) - if err != nil { - return err - } - err = assertReadFrame(ctx, c, websocket.OpContinuation, []byte{}) - if err != nil { - return err - } - return c.Close(websocket.StatusNormalClosure, "") - }) - }) - }) - - // Section 7 - t.Run("closeHandling", func(t *testing.T) { - t.Parallel() - - // 1.1 - 1.4 is useless. - run(t, "1.5", func(ctx context.Context, c *websocket.Conn) error { - p1 := randBytes(32) - _, err := c.WriteFrame(ctx, false, websocket.OpText, p1) - if err != nil { - return err - } - err = c.Flush() - if err != nil { - return err - } - _, err = c.WriteClose(ctx, websocket.StatusNormalClosure, "") - if err != nil { - return err - } - err = assertReadFrame(ctx, c, websocket.OpText, p1) - if err != nil { - return err - } - return assertReadCloseFrame(ctx, c, websocket.StatusNormalClosure) - }) - - run(t, "1.6", func(ctx context.Context, c *websocket.Conn) error { - // 262144 bytes. - p1 := randBytes(1 << 18) - err := c.Write(ctx, websocket.MessageText, p1) - if err != nil { - return err - } - _, err = c.WriteClose(ctx, websocket.StatusNormalClosure, "") - if err != nil { - return err - } - err = assertReadMessage(ctx, c, websocket.MessageText, p1) - if err != nil { - return err - } - return assertReadCloseFrame(ctx, c, websocket.StatusNormalClosure) - }) - - run(t, "emptyClose", func(ctx context.Context, c *websocket.Conn) error { - _, err := c.WriteFrame(ctx, true, websocket.OpClose, nil) - if err != nil { - return err - } - return assertReadFrame(ctx, c, websocket.OpClose, []byte{}) - }) - - run(t, "badClose", func(ctx context.Context, c *websocket.Conn) error { - _, err := c.WriteFrame(ctx, true, websocket.OpClose, []byte{1}) - if err != nil { - return err - } - return assertReadCloseFrame(ctx, c, websocket.StatusProtocolError) - }) - - run(t, "noReason", func(ctx context.Context, c *websocket.Conn) error { - return c.Close(websocket.StatusNormalClosure, "") - }) - - run(t, "simpleReason", func(ctx context.Context, c *websocket.Conn) error { - return c.Close(websocket.StatusNormalClosure, randString(16)) - }) - - run(t, "maxReason", func(ctx context.Context, c *websocket.Conn) error { - return c.Close(websocket.StatusNormalClosure, randString(123)) - }) - - run(t, "tooBigReason", func(ctx context.Context, c *websocket.Conn) error { - _, err := c.WriteFrame(ctx, true, websocket.OpClose, - append([]byte{0x03, 0xE8}, randString(124)...), - ) - if err != nil { - return err - } - return assertReadCloseFrame(ctx, c, websocket.StatusProtocolError) - }) - - t.Run("validCloses", func(t *testing.T) { - t.Parallel() - - codes := [...]websocket.StatusCode{ - 1000, - 1001, - 1002, - 1003, - 1007, - 1008, - 1009, - 1010, - 1011, - 3000, - 3999, - 4000, - 4999, - } - for _, code := range codes { - run(t, strconv.Itoa(int(code)), func(ctx context.Context, c *websocket.Conn) error { - return c.Close(code, randString(32)) - }) - } - }) - - t.Run("invalidCloseCodes", func(t *testing.T) { - t.Parallel() - - codes := []websocket.StatusCode{ - 0, - 999, - 1004, - 1005, - 1006, - 1016, - 1100, - 2000, - 2999, - 5000, - 65535, - } - for _, code := range codes { - run(t, strconv.Itoa(int(code)), func(ctx context.Context, c *websocket.Conn) error { - p := make([]byte, 2) - binary.BigEndian.PutUint16(p, uint16(code)) - p = append(p, randBytes(32)...) - _, err := c.WriteFrame(ctx, true, websocket.OpClose, p) - if err != nil { - return err - } - return assertReadCloseFrame(ctx, c, websocket.StatusProtocolError) - }) - } - }) - }) - - // Section 9. - t.Run("limits", func(t *testing.T) { - t.Parallel() - - t.Run("unfragmentedEcho", func(t *testing.T) { - t.Parallel() - - lengths := []int{ - 1 << 16, - 1 << 18, - // Anything higher is completely unnecessary. - } - - for _, l := range lengths { - l := l - run(t, strconv.Itoa(l), func(ctx context.Context, c *websocket.Conn) error { - return assertEcho(ctx, c, websocket.MessageBinary, l) - }) - } - }) - - t.Run("fragmentedEcho", func(t *testing.T) { - t.Parallel() - - fragments := []int{ - 64, - 256, - 1 << 10, - 1 << 12, - 1 << 14, - 1 << 16, - } - - for _, l := range fragments { - fragmentLength := l - run(t, strconv.Itoa(fragmentLength), func(ctx context.Context, c *websocket.Conn) error { - w, err := c.Writer(ctx, websocket.MessageText) - if err != nil { - return err - } - b := randBytes(1 << 16) - for i := 0; i < len(b); { - j := i + fragmentLength - if j > len(b) { - j = len(b) - } - - _, err = w.Write(b[i:j]) - if err != nil { - return err - } - - i = j - } - err = w.Close() - if err != nil { - return err - } - - err = assertReadMessage(ctx, c, websocket.MessageText, b) - if err != nil { - return err - } - return c.Close(websocket.StatusNormalClosure, "") - }) - } - }) - - t.Run("latencyEcho", func(t *testing.T) { - t.Parallel() - - lengths := []int{ - 0, - 16, - } - - for _, l := range lengths { - l := l - run(t, strconv.Itoa(l), func(ctx context.Context, c *websocket.Conn) error { - for i := 0; i < 1000; i++ { - err := assertEcho(ctx, c, websocket.MessageBinary, l) - if err != nil { - return err - } - } + t := time.NewTicker(time.Millisecond * 10) + defer t.Stop() + for { + select { + case <-t.C: + if atomic.LoadInt64(&conns) == 0 { return nil - }) - } - }) - }) -} - -func assertCloseStatus(err error, code websocket.StatusCode) error { - var cerr websocket.CloseError - if !errors.As(err, &cerr) { - return fmt.Errorf("no websocket close error in error chain: %+v", err) - } - return assert.Equalf(code, cerr.Code, "unexpected status code") -} - -func assertProtobufRead(ctx context.Context, c *websocket.Conn, exp interface{}) error { - expType := reflect.TypeOf(exp) - actv := reflect.New(expType.Elem()) - act := actv.Interface().(proto.Message) - err := wspb.Read(ctx, c, act) - if err != nil { - return err - } - - return assert.Equalf(exp, act, "unexpected protobuf") -} - -func assertNetConnRead(r io.Reader, exp string) error { - act := make([]byte, len(exp)) - _, err := r.Read(act) - if err != nil { - return err - } - return assert.Equalf(exp, string(act), "unexpected net conn read") -} - -func assertErrorContains(err error, exp string) error { - if err == nil || !strings.Contains(err.Error(), exp) { - return fmt.Errorf("expected error that contains %q but got: %+v", exp, err) - } - return nil -} - -func assertErrorIs(exp, act error) error { - if !errors.Is(act, exp) { - return fmt.Errorf("expected error %+v to be in %+v", exp, act) - } - return nil -} - -func assertReadFrame(ctx context.Context, c *websocket.Conn, opcode websocket.OpCode, p []byte) error { - actOpcode, actP, err := c.ReadFrame(ctx) - if err != nil { - return err - } - err = assert.Equalf(opcode, actOpcode, "unexpected frame opcode with payload %q", actP) - if err != nil { - return err - } - return assert.Equalf(p, actP, "unexpected frame %v payload", opcode) -} - -func assertReadCloseFrame(ctx context.Context, c *websocket.Conn, code websocket.StatusCode) error { - actOpcode, actP, err := c.ReadFrame(ctx) - if err != nil { - return err - } - err = assert.Equalf(websocket.OpClose, actOpcode, "unexpected frame opcode with payload %q", actP) - if err != nil { - return err - } - ce, err := websocket.ParseClosePayload(actP) - if err != nil { - return fmt.Errorf("failed to parse close frame payload: %w", err) - } - return assert.Equalf(ce.Code, code, "unexpected frame close frame code with payload %q", actP) -} - -func assertStreamPing(ctx context.Context, c *websocket.Conn, l int) error { - err := c.WriteHeader(ctx, websocket.Header{ - Fin: true, - OpCode: websocket.OpPing, - PayloadLength: int64(l), - }) - if err != nil { - return err - } - for i := 0; i < l; i++ { - err = c.BW().WriteByte(0xFE) - if err != nil { - return fmt.Errorf("failed to write byte %d: %w", i, err) - } - if i%32 == 0 { - err = c.BW().Flush() - if err != nil { - return fmt.Errorf("failed to flush at byte %d: %w", i, err) + } + case <-ctx.Done(): + return fmt.Errorf("failed to wait for WebSocket connections: %v", ctx.Err()) } } } - err = c.BW().Flush() - if err != nil { - return fmt.Errorf("failed to flush: %v", err) - } - return assertReadFrame(ctx, c, websocket.OpPong, bytes.Repeat([]byte{0xFE}, l)) -} - -func assertReadMessage(ctx context.Context, c *websocket.Conn, typ websocket.MessageType, p []byte) error { - actTyp, actP, err := c.Read(ctx) - if err != nil { - return err - } - err = assert.Equalf(websocket.MessageText, actTyp, "unexpected frame opcode with payload %q", actP) - if err != nil { - return err - } - return assert.Equalf(p, actP, "unexpected frame %v payload", actTyp) -} - -func BenchmarkConn(b *testing.B) { - sizes := []int{ - 2, - 16, - 32, - 512, - 4096, - 16384, - } - - b.Run("write", func(b *testing.B) { - for _, size := range sizes { - b.Run(strconv.Itoa(size), func(b *testing.B) { - b.Run("stream", func(b *testing.B) { - benchConn(b, false, true, size) - }) - b.Run("buffer", func(b *testing.B) { - benchConn(b, false, false, size) - }) - }) - } - }) - - b.Run("echo", func(b *testing.B) { - for _, size := range sizes { - b.Run(strconv.Itoa(size), func(b *testing.B) { - benchConn(b, true, true, size) - }) - } - }) } -func benchConn(b *testing.B, echo, stream bool, size int) { - s, closeFn := testServer(b, func(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, nil) - if err != nil { - return err - } - if echo { - wsecho.Loop(r.Context(), c) - } else { - discardLoop(r.Context(), c) - } - return nil - }, false) - defer closeFn() - - wsURL := strings.Replace(s.URL, "http", "ws", 1) - - ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) - defer cancel() - - c, _, err := websocket.Dial(ctx, wsURL, nil) - if err != nil { - b.Fatal(err) - } +// echoLoop echos every msg received from c until an error +// occurs or the context expires. +// The read limit is set to 1 << 30. +func echoLoop(ctx context.Context, c *websocket.Conn) error { defer c.Close(websocket.StatusInternalError, "") - msg := []byte(strings.Repeat("2", size)) - readBuf := make([]byte, len(msg)) - b.SetBytes(int64(len(msg))) - b.ReportAllocs() - b.ResetTimer() - for i := 0; i < b.N; i++ { - if stream { - w, err := c.Writer(ctx, websocket.MessageText) - if err != nil { - b.Fatal(err) - } - - _, err = w.Write(msg) - if err != nil { - b.Fatal(err) - } - - err = w.Close() - if err != nil { - b.Fatal(err) - } - } else { - err = c.Write(ctx, websocket.MessageText, msg) - if err != nil { - b.Fatal(err) - } - } - - if echo { - _, r, err := c.Reader(ctx) - if err != nil { - b.Fatal(err) - } - - _, err = io.ReadFull(r, readBuf) - if err != nil { - b.Fatal(err) - } - } - } - b.StopTimer() - - c.Close(websocket.StatusNormalClosure, "") -} - -func discardLoop(ctx context.Context, c *websocket.Conn) { - defer c.Close(websocket.StatusInternalError, "") + c.SetReadLimit(1 << 30) ctx, cancel := context.WithTimeout(ctx, time.Minute) defer cancel() - b := make([]byte, 32768) - echo := func() error { - _, r, err := c.Reader(ctx) + b := make([]byte, 32<<10) + for { + typ, r, err := c.Reader(ctx) if err != nil { return err } - _, err = io.CopyBuffer(ioutil.Discard, r, b) + w, err := c.Writer(ctx, typ) if err != nil { return err } - return nil - } - for { - err := echo() + _, err = io.CopyBuffer(w, r, b) if err != nil { - return + return err } - } -} - -func TestAutobahnPython(t *testing.T) { - // This test contains the old autobahn test suite tests that use the - // python binary. The approach is clunky and slow so new tests - // have been written in pure Go in websocket_test.go. - // These have been kept for correctness purposes and are occasionally ran. - if os.Getenv("AUTOBAHN_PYTHON") == "" { - t.Skip("Set $AUTOBAHN_PYTHON to run tests against the python autobahn test suite") - } - - t.Run("server", testServerAutobahnPython) - t.Run("client", testClientAutobahnPython) -} - -// https://github.com/crossbario/autobahn-python/tree/master/wstest -func testServerAutobahnPython(t *testing.T) { - t.Parallel() - s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ - Subprotocols: []string{"echo"}, - }) + err = w.Close() if err != nil { - t.Logf("server handshake failed: %+v", err) - return + return err } - wsecho.Loop(r.Context(), c) - })) - defer s.Close() - - spec := map[string]interface{}{ - "outdir": "ci/out/wstestServerReports", - "servers": []interface{}{ - map[string]interface{}{ - "agent": "main", - "url": strings.Replace(s.URL, "http", "ws", 1), - }, - }, - "cases": []string{"*"}, - // We skip the UTF-8 handling tests as there isn't any reason to reject invalid UTF-8, just - // more performance overhead. 7.5.1 is the same. - // 12.* and 13.* as we do not support compression. - "exclude-cases": []string{"6.*", "7.5.1", "12.*", "13.*"}, - } - specFile, err := ioutil.TempFile("", "websocketFuzzingClient.json") - if err != nil { - t.Fatalf("failed to create temp file for fuzzingclient.json: %v", err) - } - defer specFile.Close() - - e := json.NewEncoder(specFile) - e.SetIndent("", "\t") - err = e.Encode(spec) - if err != nil { - t.Fatalf("failed to write spec: %v", err) - } - - err = specFile.Close() - if err != nil { - t.Fatalf("failed to close file: %v", err) - } - - ctx := context.Background() - ctx, cancel := context.WithTimeout(ctx, time.Minute*10) - defer cancel() - - args := []string{"--mode", "fuzzingclient", "--spec", specFile.Name()} - wstest := exec.CommandContext(ctx, "wstest", args...) - out, err := wstest.CombinedOutput() - if err != nil { - t.Fatalf("failed to run wstest: %v\nout:\n%s", err, out) } - - checkWSTestIndex(t, "./ci/out/wstestServerReports/index.json") } -func unusedListenAddr() (string, error) { - l, err := net.Listen("tcp", "localhost:0") - if err != nil { - return "", err - } - l.Close() - return l.Addr().String(), nil -} - -// https://github.com/crossbario/autobahn-python/blob/master/wstest/testee_client_aio.py -func testClientAutobahnPython(t *testing.T) { +func TestConn(t *testing.T) { t.Parallel() - if os.Getenv("AUTOBAHN_PYTHON") == "" { - t.Skip("Set $AUTOBAHN_PYTHON to test against the python autobahn test suite") - } - - serverAddr, err := unusedListenAddr() - if err != nil { - t.Fatalf("failed to get unused listen addr for wstest: %v", err) - } - - wsServerURL := "ws://" + serverAddr - - spec := map[string]interface{}{ - "url": wsServerURL, - "outdir": "ci/out/wstestClientReports", - "cases": []string{"*"}, - // See TestAutobahnServer for the reasons why we exclude these. - "exclude-cases": []string{"6.*", "7.5.1", "12.*", "13.*"}, - } - specFile, err := ioutil.TempFile("", "websocketFuzzingServer.json") - if err != nil { - t.Fatalf("failed to create temp file for fuzzingserver.json: %v", err) - } - defer specFile.Close() - - e := json.NewEncoder(specFile) - e.SetIndent("", "\t") - err = e.Encode(spec) - if err != nil { - t.Fatalf("failed to write spec: %v", err) - } - - err = specFile.Close() - if err != nil { - t.Fatalf("failed to close file: %v", err) - } - - ctx := context.Background() - ctx, cancel := context.WithTimeout(ctx, time.Minute*10) - defer cancel() - - args := []string{"--mode", "fuzzingserver", "--spec", specFile.Name(), - // Disables some server that runs as part of fuzzingserver mode. - // See https://github.com/crossbario/autobahn-testsuite/blob/058db3a36b7c3a1edf68c282307c6b899ca4857f/autobahntestsuite/autobahntestsuite/wstest.py#L124 - "--webport=0", - } - wstest := exec.CommandContext(ctx, "wstest", args...) - err = wstest.Start() - if err != nil { - t.Fatal(err) - } - defer func() { - err := wstest.Process.Kill() - if err != nil { - t.Error(err) - } - }() - - // Let it come up. - time.Sleep(time.Second * 5) - - var cases int - func() { - c, _, err := websocket.Dial(ctx, wsServerURL+"/getCaseCount", nil) - if err != nil { - t.Fatal(err) - } - defer c.Close(websocket.StatusInternalError, "") - - _, r, err := c.Reader(ctx) - if err != nil { - t.Fatal(err) - } - b, err := ioutil.ReadAll(r) - if err != nil { - t.Fatal(err) - } - cases, err = strconv.Atoi(string(b)) - if err != nil { - t.Fatal(err) - } - - c.Close(websocket.StatusNormalClosure, "") - }() - - for i := 1; i <= cases; i++ { - func() { - ctx, cancel := context.WithTimeout(ctx, time.Second*45) - defer cancel() - - c, _, err := websocket.Dial(ctx, fmt.Sprintf(wsServerURL+"/runCase?case=%v&agent=main", i), nil) - if err != nil { - t.Fatal(err) - } - wsecho.Loop(ctx, c) - }() - } - - c, _, err := websocket.Dial(ctx, fmt.Sprintf(wsServerURL+"/updateReports?agent=main"), nil) - if err != nil { - t.Fatal(err) - } - c.Close(websocket.StatusNormalClosure, "") + t.Run("json", func(t *testing.T) { + s, closeFn := testServer(t, func(w http.ResponseWriter, r *http.Request) { + c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + Subprotocols: []string{"echo"}, + InsecureSkipVerify: true, + }) + assert.Success(t, err) + defer c.Close(websocket.StatusInternalError, "") - checkWSTestIndex(t, "./ci/out/wstestClientReports/index.json") -} + err = echoLoop(r.Context(), c) + assertCloseStatus(t, websocket.StatusNormalClosure, err) + }, false) + defer closeFn() -func checkWSTestIndex(t *testing.T, path string) { - wstestOut, err := ioutil.ReadFile(path) - if err != nil { - t.Fatalf("failed to read index.json: %v", err) - } + wsURL := strings.Replace(s.URL, "http", "ws", 1) - var indexJSON map[string]map[string]struct { - Behavior string `json:"behavior"` - BehaviorClose string `json:"behaviorClose"` - } - err = json.Unmarshal(wstestOut, &indexJSON) - if err != nil { - t.Fatalf("failed to unmarshal index.json: %v", err) - } + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() - var failed bool - for _, tests := range indexJSON { - for test, result := range tests { - switch result.Behavior { - case "OK", "NON-STRICT", "INFORMATIONAL": - default: - failed = true - t.Errorf("test %v failed", test) - } - switch result.BehaviorClose { - case "OK", "INFORMATIONAL": - default: - failed = true - t.Errorf("bad close behaviour for test %v", test) - } - } - } - - if failed { - path = strings.Replace(path, ".json", ".html", 1) - if os.Getenv("CI") == "" { - t.Errorf("wstest found failure, see %q (output as an artifact in CI)", path) - } - } -} - -func TestWASM(t *testing.T) { - t.Parallel() - - s, closeFn := testServer(t, func(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ - Subprotocols: []string{"echo"}, - InsecureSkipVerify: true, - }) - if err != nil { - return err - } - defer c.Close(websocket.StatusInternalError, "") - - err = wsecho.Loop(r.Context(), c) - if websocket.CloseStatus(err) != websocket.StatusNormalClosure { - return err + opts := &websocket.DialOptions{ + Subprotocols: []string{"echo"}, } - return nil - }, false) - defer closeFn() + opts.HTTPClient = s.Client() - wsURL := strings.Replace(s.URL, "http", "ws", 1) - - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() + c, _, err := websocket.Dial(ctx, wsURL, opts) + assert.Success(t, err) - cmd := exec.CommandContext(ctx, "go", "test", "-exec=wasmbrowsertest", "./...") - cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm", fmt.Sprintf("WS_ECHO_SERVER_URL=%v", wsURL)) - - b, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("wasm test binary failed: %v:\n%s", err, b) - } + assertJSONEcho(t, ctx, c, 2) + }) } diff --git a/dial.go b/dial.go index 10088681..8fa0f7ab 100644 --- a/dial.go +++ b/dial.go @@ -1,17 +1,19 @@ package websocket import ( + "bufio" "bytes" "context" "crypto/rand" "encoding/base64" + "errors" "fmt" "io" "io/ioutil" "net/http" "net/url" - "nhooyr.io/websocket/internal/bufpool" "strings" + "sync" ) // DialOptions represents the options available to pass to Dial. @@ -50,7 +52,7 @@ func Dial(ctx context.Context, u string, opts *DialOptions) (*Conn, *http.Respon return c, r, nil } -func (opts *DialOptions) fill() (*DialOptions, error) { +func (opts *DialOptions) ensure() *DialOptions { if opts == nil { opts = &DialOptions{} } else { @@ -60,20 +62,18 @@ func (opts *DialOptions) fill() (*DialOptions, error) { if opts.HTTPClient == nil { opts.HTTPClient = http.DefaultClient } - if opts.HTTPClient.Timeout > 0 { - return nil, fmt.Errorf("use context for cancellation instead of http.Client.Timeout; see https://github.com/nhooyr/websocket/issues/67") - } if opts.HTTPHeader == nil { opts.HTTPHeader = http.Header{} } - return opts, nil + return opts } func dial(ctx context.Context, u string, opts *DialOptions) (_ *Conn, _ *http.Response, err error) { - opts, err = opts.fill() - if err != nil { - return nil, nil, err + opts = opts.ensure() + + if opts.HTTPClient.Timeout > 0 { + return nil, nil, errors.New("use context for cancellation instead of http.Client.Timeout; see https://github.com/nhooyr/websocket/issues/67") } parsedURL, err := url.Parse(u) @@ -104,8 +104,10 @@ func dial(ctx context.Context, u string, opts *DialOptions) (_ *Conn, _ *http.Re if len(opts.Subprotocols) > 0 { req.Header.Set("Sec-WebSocket-Protocol", strings.Join(opts.Subprotocols, ",")) } - copts := opts.CompressionMode.opts() - copts.setHeader(req.Header) + if opts.CompressionMode != CompressionDisabled { + copts := opts.CompressionMode.opts() + copts.setHeader(req.Header) + } resp, err := opts.HTTPClient.Do(req) if err != nil { @@ -121,7 +123,7 @@ func dial(ctx context.Context, u string, opts *DialOptions) (_ *Conn, _ *http.Re } }() - copts, err = verifyServerResponse(req, resp, opts) + copts, err := verifyServerResponse(req, resp) if err != nil { return nil, resp, err } @@ -131,18 +133,14 @@ func dial(ctx context.Context, u string, opts *DialOptions) (_ *Conn, _ *http.Re return nil, resp, fmt.Errorf("response body is not a io.ReadWriteCloser: %T", rwc) } - c := &Conn{ + return newConn(connConfig{ subprotocol: resp.Header.Get("Sec-WebSocket-Protocol"), - br: bufpool.GetReader(rwc), - bw: bufpool.GetWriter(rwc), - closer: rwc, + rwc: rwc, client: true, copts: copts, - } - c.extractBufioWriterBuf(rwc) - c.init() - - return c, resp, nil + br: getBufioReader(rwc), + bw: getBufioWriter(rwc), + }), resp, nil } func secWebSocketKey() (string, error) { @@ -154,7 +152,7 @@ func secWebSocketKey() (string, error) { return base64.StdEncoding.EncodeToString(b), nil } -func verifyServerResponse(r *http.Request, resp *http.Response, opts *DialOptions) (*compressionOptions, error) { +func verifyServerResponse(r *http.Request, resp *http.Response) (*compressionOptions, error) { if resp.StatusCode != http.StatusSwitchingProtocols { return nil, fmt.Errorf("expected handshake response status code %v but got %v", http.StatusSwitchingProtocols, resp.StatusCode) } @@ -178,7 +176,7 @@ func verifyServerResponse(r *http.Request, resp *http.Response, opts *DialOption return nil, fmt.Errorf("websocket protocol violation: unexpected Sec-WebSocket-Protocol from server: %q", proto) } - copts, err := verifyServerExtensions(resp.Header, opts.CompressionMode) + copts, err := verifyServerExtensions(resp.Header) if err != nil { return nil, err } @@ -186,7 +184,7 @@ func verifyServerResponse(r *http.Request, resp *http.Response, opts *DialOption return copts, nil } -func verifyServerExtensions(h http.Header, mode CompressionMode) (*compressionOptions, error) { +func verifyServerExtensions(h http.Header) (*compressionOptions, error) { exts := websocketExtensions(h) if len(exts) == 0 { return nil, nil @@ -201,7 +199,7 @@ func verifyServerExtensions(h http.Header, mode CompressionMode) (*compressionOp return nil, fmt.Errorf("unexpected extra extensions from server: %+v", exts[1:]) } - copts := mode.opts() + copts := &compressionOptions{} for _, p := range ext.params { switch p { case "client_no_context_takeover": @@ -217,3 +215,33 @@ func verifyServerExtensions(h http.Header, mode CompressionMode) (*compressionOp return copts, nil } + +var readerPool sync.Pool + +func getBufioReader(r io.Reader) *bufio.Reader { + br, ok := readerPool.Get().(*bufio.Reader) + if !ok { + return bufio.NewReader(r) + } + br.Reset(r) + return br +} + +func putBufioReader(br *bufio.Reader) { + readerPool.Put(br) +} + +var writerPool sync.Pool + +func getBufioWriter(w io.Writer) *bufio.Writer { + bw, ok := writerPool.Get().(*bufio.Writer) + if !ok { + return bufio.NewWriter(w) + } + bw.Reset(w) + return bw +} + +func putBufioWriter(bw *bufio.Writer) { + writerPool.Put(bw) +} diff --git a/dial_test.go b/dial_test.go index 391aa1ce..5eeb904a 100644 --- a/dial_test.go +++ b/dial_test.go @@ -140,7 +140,7 @@ func Test_verifyServerHandshake(t *testing.T) { resp.Header.Set("Sec-WebSocket-Accept", secWebSocketAccept(key)) } - _, err = verifyServerResponse(r, resp, &DialOptions{}) + _, err = verifyServerResponse(r, resp) if (err == nil) != tc.success { t.Fatalf("unexpected error: %+v", err) } diff --git a/example_echo_test.go b/example_echo_test.go index ecc9b97c..16d003d9 100644 --- a/example_echo_test.go +++ b/example_echo_test.go @@ -4,6 +4,7 @@ package websocket_test import ( "context" + "errors" "fmt" "io" "log" @@ -77,7 +78,7 @@ func echoServer(w http.ResponseWriter, r *http.Request) error { if c.Subprotocol() != "echo" { c.Close(websocket.StatusPolicyViolation, "client must speak the echo subprotocol") - return fmt.Errorf("client does not speak echo sub protocol") + return errors.New("client does not speak echo sub protocol") } l := rate.NewLimiter(rate.Every(time.Millisecond*100), 10) diff --git a/internal/wsframe/mask.go b/frame.go similarity index 57% rename from internal/wsframe/mask.go rename to frame.go index 2da4c11d..0f10d553 100644 --- a/internal/wsframe/mask.go +++ b/frame.go @@ -1,11 +1,167 @@ -package wsframe +package websocket import ( + "bufio" "encoding/binary" + "math" "math/bits" + "nhooyr.io/websocket/internal/errd" ) -// Mask applies the WebSocket masking algorithm to p +// opcode represents a WebSocket opcode. +type opcode int + +// List at https://tools.ietf.org/html/rfc6455#section-11.8. +const ( + opContinuation opcode = iota + opText + opBinary + // 3 - 7 are reserved for further non-control frames. + _ + _ + _ + _ + _ + opClose + opPing + opPong + // 11-16 are reserved for further control frames. +) + +// header represents a WebSocket frame header. +// See https://tools.ietf.org/html/rfc6455#section-5.2. +type header struct { + fin bool + rsv1 bool + rsv2 bool + rsv3 bool + opcode opcode + + payloadLength int64 + + masked bool + maskKey uint32 +} + +// readFrameHeader reads a header from the reader. +// See https://tools.ietf.org/html/rfc6455#section-5.2. +func readFrameHeader(r *bufio.Reader) (_ header, err error) { + defer errd.Wrap(&err, "failed to read frame header") + + b, err := r.ReadByte() + if err != nil { + return header{}, err + } + + var h header + h.fin = b&(1<<7) != 0 + h.rsv1 = b&(1<<6) != 0 + h.rsv2 = b&(1<<5) != 0 + h.rsv3 = b&(1<<4) != 0 + + h.opcode = opcode(b & 0xf) + + b, err = r.ReadByte() + if err != nil { + return header{}, err + } + + h.masked = b&(1<<7) != 0 + + payloadLength := b &^ (1 << 7) + switch { + case payloadLength < 126: + h.payloadLength = int64(payloadLength) + case payloadLength == 126: + var pl uint16 + err = binary.Read(r, binary.BigEndian, &pl) + h.payloadLength = int64(pl) + case payloadLength == 127: + err = binary.Read(r, binary.BigEndian, &h.payloadLength) + } + if err != nil { + return header{}, err + } + + if h.masked { + err = binary.Read(r, binary.LittleEndian, &h.maskKey) + if err != nil { + return header{}, err + } + } + + return h, nil +} + +// maxControlPayload is the maximum length of a control frame payload. +// See https://tools.ietf.org/html/rfc6455#section-5.5. +const maxControlPayload = 125 + +// writeFrameHeader writes the bytes of the header to w. +// See https://tools.ietf.org/html/rfc6455#section-5.2 +func writeFrameHeader(h header, w *bufio.Writer) (err error) { + defer errd.Wrap(&err, "failed to write frame header") + + var b byte + if h.fin { + b |= 1 << 7 + } + if h.rsv1 { + b |= 1 << 6 + } + if h.rsv2 { + b |= 1 << 5 + } + if h.rsv3 { + b |= 1 << 4 + } + + b |= byte(h.opcode) + + err = w.WriteByte(b) + if err != nil { + return err + } + + lengthByte := byte(0) + if h.masked { + lengthByte |= 1 << 7 + } + + switch { + case h.payloadLength > math.MaxUint16: + lengthByte |= 127 + case h.payloadLength > 125: + lengthByte |= 126 + case h.payloadLength >= 0: + lengthByte |= byte(h.payloadLength) + } + err = w.WriteByte(lengthByte) + if err != nil { + return err + } + + switch { + case h.payloadLength > math.MaxUint16: + err = binary.Write(w, binary.BigEndian, h.payloadLength) + case h.payloadLength > 125: + err = binary.Write(w, binary.BigEndian, uint16(h.payloadLength)) + } + if err != nil { + return err + } + + if h.masked { + err = binary.Write(w, binary.LittleEndian, h.maskKey) + if err != nil { + return err + } + } + + return nil +} + +// mask applies the WebSocket masking algorithm to p // with the given key. // See https://tools.ietf.org/html/rfc6455#section-5.3 // @@ -16,7 +172,7 @@ import ( // to be in little endian. // // See https://github.com/golang/go/issues/31586 -func Mask(key uint32, b []byte) uint32 { +func mask(key uint32, b []byte) uint32 { if len(b) >= 8 { key64 := uint64(key)<<32 | uint64(key) diff --git a/internal/wsframe/mask_test.go b/frame_test.go similarity index 51% rename from internal/wsframe/mask_test.go rename to frame_test.go index fbd29892..0ed14aef 100644 --- a/internal/wsframe/mask_test.go +++ b/frame_test.go @@ -1,32 +1,108 @@ -package wsframe_test +// +build !js + +package websocket import ( - "crypto/rand" + "bufio" + "bytes" "encoding/binary" - "github.com/gobwas/ws" - "github.com/google/go-cmp/cmp" "math/bits" - "nhooyr.io/websocket/internal/wsframe" + "nhooyr.io/websocket/internal/assert" "strconv" "testing" + "time" _ "unsafe" + + "github.com/gobwas/ws" + _ "github.com/gorilla/websocket" + "math/rand" ) +func init() { + rand.Seed(time.Now().UnixNano()) +} + +func TestHeader(t *testing.T) { + t.Parallel() + + t.Run("lengths", func(t *testing.T) { + t.Parallel() + + lengths := []int{ + 124, + 125, + 126, + 127, + + 65534, + 65535, + 65536, + 65537, + } + + for _, n := range lengths { + n := n + t.Run(strconv.Itoa(n), func(t *testing.T) { + t.Parallel() + + testHeader(t, header{ + payloadLength: int64(n), + }) + }) + } + }) + + t.Run("fuzz", func(t *testing.T) { + t.Parallel() + + randBool := func() bool { + return rand.Intn(1) == 0 + } + + for i := 0; i < 10000; i++ { + h := header{ + fin: randBool(), + rsv1: randBool(), + rsv2: randBool(), + rsv3: randBool(), + opcode: opcode(rand.Intn(16)), + + masked: randBool(), + maskKey: rand.Uint32(), + payloadLength: rand.Int63(), + } + + testHeader(t, h) + } + }) +} + +func testHeader(t *testing.T, h header) { + b := &bytes.Buffer{} + w := bufio.NewWriter(b) + r := bufio.NewReader(b) + + err := writeFrameHeader(h, w) + assert.Success(t, err) + err = w.Flush() + assert.Success(t, err) + + h2, err := readFrameHeader(r) + assert.Success(t, err) + + assert.Equalf(t, h, h2, "written and read headers differ") +} + func Test_mask(t *testing.T) { t.Parallel() key := []byte{0xa, 0xb, 0xc, 0xff} key32 := binary.LittleEndian.Uint32(key) p := []byte{0xa, 0xb, 0xc, 0xf2, 0xc} - gotKey32 := wsframe.Mask(key32, p) + gotKey32 := mask(key32, p) - if exp := []byte{0, 0, 0, 0x0d, 0x6}; !cmp.Equal(exp, p) { - t.Fatalf("unexpected mask: %v", cmp.Diff(exp, p)) - } - - if exp := bits.RotateLeft32(key32, -8); !cmp.Equal(exp, gotKey32) { - t.Fatalf("unexpected mask key: %v", cmp.Diff(exp, gotKey32)) - } + assert.Equalf(t, []byte{0, 0, 0, 0x0d, 0x6}, p, "unexpected mask") + assert.Equalf(t, bits.RotateLeft32(key32, -8), gotKey32, "unexpected mask key") } func basicMask(maskKey [4]byte, pos int, b []byte) int { @@ -74,7 +150,7 @@ func Benchmark_mask(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - wsframe.Mask(key32, p) + mask(key32, p) } }, }, @@ -98,9 +174,7 @@ func Benchmark_mask(b *testing.B) { var key [4]byte _, err := rand.Read(key[:]) - if err != nil { - b.Fatalf("failed to populate mask key: %v", err) - } + assert.Success(b, err) for _, size := range sizes { p := make([]byte, size) diff --git a/internal/assert/assert.go b/internal/assert/assert.go index 372d5465..1d9aeced 100644 --- a/internal/assert/assert.go +++ b/internal/assert/assert.go @@ -2,6 +2,7 @@ package assert import ( "reflect" + "strings" "testing" "github.com/google/go-cmp/cmp" @@ -53,7 +54,7 @@ func structTypes(v reflect.Value, m map[reflect.Type]struct{}) { } } -func Equalf(t *testing.T, exp, act interface{}, f string, v ...interface{}) { +func Equalf(t testing.TB, exp, act interface{}, f string, v ...interface{}) { t.Helper() diff := cmpDiff(exp, act) if diff != "" { @@ -61,7 +62,40 @@ func Equalf(t *testing.T, exp, act interface{}, f string, v ...interface{}) { } } -func Success(t *testing.T, err error) { +func NotEqualf(t testing.TB, exp, act interface{}, f string, v ...interface{}) { t.Helper() - Equalf(t, error(nil), err, "unexpected failure") + diff := cmpDiff(exp, act) + if diff == "" { + t.Fatalf(f+": %v", append(v, diff)...) + } +} + +func Success(t testing.TB, err error) { + t.Helper() + if err != nil { + t.Fatalf("unexpected error: %+v", err) + } +} + +func Error(t testing.TB, err error) { + t.Helper() + if err == nil { + t.Fatal("expected error") + } +} + +func ErrorContains(t testing.TB, err error, sub string) { + t.Helper() + Error(t, err) + errs := err.Error() + if !strings.Contains(errs, sub) { + t.Fatalf("error string %q does not contain %q", errs, sub) + } +} + +func Panicf(t testing.TB, f string, v ...interface{}) { + r := recover() + if r == nil { + t.Fatalf(f, v...) + } } diff --git a/internal/atomicint/atomicint.go b/internal/atomicint/atomicint.go deleted file mode 100644 index 668b3b4b..00000000 --- a/internal/atomicint/atomicint.go +++ /dev/null @@ -1,32 +0,0 @@ -package atomicint - -import ( - "fmt" - "sync/atomic" -) - -// See https://github.com/nhooyr/websocket/issues/153 -type Int64 struct { - v int64 -} - -func (v *Int64) Load() int64 { - return atomic.LoadInt64(&v.v) -} - -func (v *Int64) Store(i int64) { - atomic.StoreInt64(&v.v, i) -} - -func (v *Int64) String() string { - return fmt.Sprint(v.Load()) -} - -// Increment increments the value and returns the new value. -func (v *Int64) Increment(delta int64) int64 { - return atomic.AddInt64(&v.v, delta) -} - -func (v *Int64) CAS(old, new int64) (swapped bool) { - return atomic.CompareAndSwapInt64(&v.v, old, new) -} diff --git a/internal/bufpool/buf.go b/internal/bufpool/buf.go index 324a17e1..0f7d9765 100644 --- a/internal/bufpool/buf.go +++ b/internal/bufpool/buf.go @@ -5,12 +5,12 @@ import ( "sync" ) -var bpool sync.Pool +var pool sync.Pool // Get returns a buffer from the pool or creates a new one if // the pool is empty. func Get() *bytes.Buffer { - b, ok := bpool.Get().(*bytes.Buffer) + b, ok := pool.Get().(*bytes.Buffer) if !ok { b = &bytes.Buffer{} } @@ -20,5 +20,5 @@ func Get() *bytes.Buffer { // Put returns a buffer into the pool. func Put(b *bytes.Buffer) { b.Reset() - bpool.Put(b) + pool.Put(b) } diff --git a/internal/bufpool/bufio.go b/internal/bufpool/bufio.go deleted file mode 100644 index 875bbf4b..00000000 --- a/internal/bufpool/bufio.go +++ /dev/null @@ -1,40 +0,0 @@ -package bufpool - -import ( - "bufio" - "io" - "sync" -) - -var readerPool = sync.Pool{ - New: func() interface{} { - return bufio.NewReader(nil) - }, -} - -func GetReader(r io.Reader) *bufio.Reader { - br := readerPool.Get().(*bufio.Reader) - br.Reset(r) - return br -} - -func PutReader(br *bufio.Reader) { - readerPool.Put(br) -} - -var writerPool = sync.Pool{ - New: func() interface{} { - return bufio.NewWriter(nil) - }, -} - -func GetWriter(w io.Writer) *bufio.Writer { - bw := writerPool.Get().(*bufio.Writer) - bw.Reset(w) - return bw -} - -func PutWriter(bw *bufio.Writer) { - writerPool.Put(bw) -} - diff --git a/internal/errd/errd.go b/internal/errd/errd.go new file mode 100644 index 00000000..51b7b4f6 --- /dev/null +++ b/internal/errd/errd.go @@ -0,0 +1,11 @@ +package errd + +import ( + "fmt" +) + +func Wrap(err *error, f string, v ...interface{}) { + if *err != nil { + *err = fmt.Errorf(f+ ": %w", append(v, *err)...) + } +} diff --git a/internal/wsecho/wsecho.go b/internal/wsecho/wsecho.go deleted file mode 100644 index c408f07f..00000000 --- a/internal/wsecho/wsecho.go +++ /dev/null @@ -1,55 +0,0 @@ -// +build !js - -package wsecho - -import ( - "context" - "io" - "time" - - "nhooyr.io/websocket" -) - -// Loop echos every msg received from c until an error -// occurs or the context expires. -// The read limit is set to 1 << 30. -func Loop(ctx context.Context, c *websocket.Conn) error { - defer c.Close(websocket.StatusInternalError, "") - - c.SetReadLimit(1 << 30) - - ctx, cancel := context.WithTimeout(ctx, time.Minute) - defer cancel() - - b := make([]byte, 32<<10) - echo := func() error { - typ, r, err := c.Reader(ctx) - if err != nil { - return err - } - - w, err := c.Writer(ctx, typ) - if err != nil { - return err - } - - _, err = io.CopyBuffer(w, r, b) - if err != nil { - return err - } - - err = w.Close() - if err != nil { - return err - } - - return nil - } - - for { - err := echo() - if err != nil { - return err - } - } -} diff --git a/internal/wsframe/frame.go b/internal/wsframe/frame.go deleted file mode 100644 index 50ff8c11..00000000 --- a/internal/wsframe/frame.go +++ /dev/null @@ -1,194 +0,0 @@ -package wsframe - -import ( - "encoding/binary" - "fmt" - "io" - "math" -) - -// Opcode represents a WebSocket Opcode. -type Opcode int - -// Opcode constants. -const ( - OpContinuation Opcode = iota - OpText - OpBinary - // 3 - 7 are reserved for further non-control frames. - _ - _ - _ - _ - _ - OpClose - OpPing - OpPong - // 11-16 are reserved for further control frames. -) - -func (o Opcode) Control() bool { - switch o { - case OpClose, OpPing, OpPong: - return true - } - return false -} - -func (o Opcode) Data() bool { - switch o { - case OpText, OpBinary: - return true - } - return false -} - -// First byte contains fin, rsv1, rsv2, rsv3. -// Second byte contains mask flag and payload len. -// Next 8 bytes are the maximum extended payload length. -// Last 4 bytes are the mask key. -// https://tools.ietf.org/html/rfc6455#section-5.2 -const maxHeaderSize = 1 + 1 + 8 + 4 - -// Header represents a WebSocket frame Header. -// See https://tools.ietf.org/html/rfc6455#section-5.2 -type Header struct { - Fin bool - RSV1 bool - RSV2 bool - RSV3 bool - Opcode Opcode - - PayloadLength int64 - - Masked bool - MaskKey uint32 -} - -// bytes returns the bytes of the Header. -// See https://tools.ietf.org/html/rfc6455#section-5.2 -func (h Header) Bytes(b []byte) []byte { - if b == nil { - b = make([]byte, maxHeaderSize) - } - - b = b[:2] - b[0] = 0 - - if h.Fin { - b[0] |= 1 << 7 - } - if h.RSV1 { - b[0] |= 1 << 6 - } - if h.RSV2 { - b[0] |= 1 << 5 - } - if h.RSV3 { - b[0] |= 1 << 4 - } - - b[0] |= byte(h.Opcode) - - switch { - case h.PayloadLength < 0: - panic(fmt.Sprintf("websocket: invalid Header: negative length: %v", h.PayloadLength)) - case h.PayloadLength <= 125: - b[1] = byte(h.PayloadLength) - case h.PayloadLength <= math.MaxUint16: - b[1] = 126 - b = b[:len(b)+2] - binary.BigEndian.PutUint16(b[len(b)-2:], uint16(h.PayloadLength)) - default: - b[1] = 127 - b = b[:len(b)+8] - binary.BigEndian.PutUint64(b[len(b)-8:], uint64(h.PayloadLength)) - } - - if h.Masked { - b[1] |= 1 << 7 - b = b[:len(b)+4] - binary.LittleEndian.PutUint32(b[len(b)-4:], h.MaskKey) - } - - return b -} - -func MakeReadHeaderBuf() []byte { - return make([]byte, maxHeaderSize-2) -} - -// ReadHeader reads a Header from the reader. -// See https://tools.ietf.org/html/rfc6455#section-5.2 -func ReadHeader(r io.Reader, b []byte) (Header, error) { - // We read the first two bytes first so that we know - // exactly how long the Header is. - b = b[:2] - _, err := io.ReadFull(r, b) - if err != nil { - return Header{}, err - } - - var h Header - h.Fin = b[0]&(1<<7) != 0 - h.RSV1 = b[0]&(1<<6) != 0 - h.RSV2 = b[0]&(1<<5) != 0 - h.RSV3 = b[0]&(1<<4) != 0 - - h.Opcode = Opcode(b[0] & 0xf) - - var extra int - - h.Masked = b[1]&(1<<7) != 0 - if h.Masked { - extra += 4 - } - - payloadLength := b[1] &^ (1 << 7) - switch { - case payloadLength < 126: - h.PayloadLength = int64(payloadLength) - case payloadLength == 126: - extra += 2 - case payloadLength == 127: - extra += 8 - } - - if extra == 0 { - return h, nil - } - - b = b[:extra] - _, err = io.ReadFull(r, b) - if err != nil { - return Header{}, err - } - - switch { - case payloadLength == 126: - h.PayloadLength = int64(binary.BigEndian.Uint16(b)) - b = b[2:] - case payloadLength == 127: - h.PayloadLength = int64(binary.BigEndian.Uint64(b)) - if h.PayloadLength < 0 { - return Header{}, fmt.Errorf("Header with negative payload length: %v", h.PayloadLength) - } - b = b[8:] - } - - if h.Masked { - h.MaskKey = binary.LittleEndian.Uint32(b) - } - - return h, nil -} - -const MaxControlFramePayload = 125 - -func ParseClosePayload(p []byte) (uint16, string, error) { - if len(p) < 2 { - return 0, "", fmt.Errorf("close payload %q too small, cannot even contain the 2 byte status code", p) - } - - return binary.BigEndian.Uint16(p), string(p[2:]), nil -} diff --git a/internal/wsframe/frame_stringer.go b/internal/wsframe/frame_stringer.go deleted file mode 100644 index b2e7f423..00000000 --- a/internal/wsframe/frame_stringer.go +++ /dev/null @@ -1,91 +0,0 @@ -// Code generated by "stringer -type=Opcode,MessageType,StatusCode -output=frame_stringer.go"; DO NOT EDIT. - -package wsframe - -import "strconv" - -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[OpContinuation-0] - _ = x[OpText-1] - _ = x[OpBinary-2] - _ = x[OpClose-8] - _ = x[OpPing-9] - _ = x[OpPong-10] -} - -const ( - _opcode_name_0 = "opContinuationopTextopBinary" - _opcode_name_1 = "opCloseopPingopPong" -) - -var ( - _opcode_index_0 = [...]uint8{0, 14, 20, 28} - _opcode_index_1 = [...]uint8{0, 7, 13, 19} -) - -func (i Opcode) String() string { - switch { - case 0 <= i && i <= 2: - return _opcode_name_0[_opcode_index_0[i]:_opcode_index_0[i+1]] - case 8 <= i && i <= 10: - i -= 8 - return _opcode_name_1[_opcode_index_1[i]:_opcode_index_1[i+1]] - default: - return "Opcode(" + strconv.FormatInt(int64(i), 10) + ")" - } -} -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[MessageText-1] - _ = x[MessageBinary-2] -} - -const _MessageType_name = "MessageTextMessageBinary" - -var _MessageType_index = [...]uint8{0, 11, 24} - -func (i MessageType) String() string { - i -= 1 - if i < 0 || i >= MessageType(len(_MessageType_index)-1) { - return "MessageType(" + strconv.FormatInt(int64(i+1), 10) + ")" - } - return _MessageType_name[_MessageType_index[i]:_MessageType_index[i+1]] -} -func _() { - // An "invalid array index" compiler error signifies that the constant values have changed. - // Re-run the stringer command to generate them again. - var x [1]struct{} - _ = x[StatusNormalClosure-1000] - _ = x[StatusGoingAway-1001] - _ = x[StatusProtocolError-1002] - _ = x[StatusUnsupportedData-1003] - _ = x[statusReserved-1004] - _ = x[StatusNoStatusRcvd-1005] - _ = x[StatusAbnormalClosure-1006] - _ = x[StatusInvalidFramePayloadData-1007] - _ = x[StatusPolicyViolation-1008] - _ = x[StatusMessageTooBig-1009] - _ = x[StatusMandatoryExtension-1010] - _ = x[StatusInternalError-1011] - _ = x[StatusServiceRestart-1012] - _ = x[StatusTryAgainLater-1013] - _ = x[StatusBadGateway-1014] - _ = x[StatusTLSHandshake-1015] -} - -const _StatusCode_name = "StatusNormalClosureStatusGoingAwayStatusProtocolErrorStatusUnsupportedDatastatusReservedStatusNoStatusRcvdStatusAbnormalClosureStatusInvalidFramePayloadDataStatusPolicyViolationStatusMessageTooBigStatusMandatoryExtensionStatusInternalErrorStatusServiceRestartStatusTryAgainLaterStatusBadGatewayStatusTLSHandshake" - -var _StatusCode_index = [...]uint16{0, 19, 34, 53, 74, 88, 106, 127, 156, 177, 196, 220, 239, 259, 278, 294, 312} - -func (i StatusCode) String() string { - i -= 1000 - if i < 0 || i >= StatusCode(len(_StatusCode_index)-1) { - return "StatusCode(" + strconv.FormatInt(int64(i+1000), 10) + ")" - } - return _StatusCode_name[_StatusCode_index[i]:_StatusCode_index[i+1]] -} diff --git a/internal/wsframe/frame_test.go b/internal/wsframe/frame_test.go deleted file mode 100644 index d6b66e7e..00000000 --- a/internal/wsframe/frame_test.go +++ /dev/null @@ -1,157 +0,0 @@ -// +build !js - -package wsframe - -import ( - "bytes" - "io" - "math/rand" - "strconv" - "testing" - "time" - _ "unsafe" - - "github.com/google/go-cmp/cmp" - _ "github.com/gorilla/websocket" -) - -func init() { - rand.Seed(time.Now().UnixNano()) -} - -func randBool() bool { - return rand.Intn(1) == 0 -} - -func TestHeader(t *testing.T) { - t.Parallel() - - t.Run("eof", func(t *testing.T) { - t.Parallel() - - testCases := []struct { - name string - bytes []byte - }{ - { - "start", - []byte{0xff}, - }, - { - "middle", - []byte{0xff, 0xff, 0xff}, - }, - } - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - b := bytes.NewBuffer(tc.bytes) - _, err := ReadHeader(nil, b) - if io.ErrUnexpectedEOF != err { - t.Fatalf("expected %v but got: %v", io.ErrUnexpectedEOF, err) - } - }) - } - }) - - t.Run("writeNegativeLength", func(t *testing.T) { - t.Parallel() - - defer func() { - r := recover() - if r == nil { - t.Fatal("failed to induce panic in writeHeader with negative payload length") - } - }() - - Header{ - PayloadLength: -1, - }.Bytes(nil) - }) - - t.Run("readNegativeLength", func(t *testing.T) { - t.Parallel() - - b := Header{ - PayloadLength: 1<<16 + 1, - }.Bytes(nil) - - // Make length negative - b[2] |= 1 << 7 - - r := bytes.NewReader(b) - _, err := ReadHeader(nil, r) - if err == nil { - t.Fatalf("unexpected error value: %+v", err) - } - }) - - t.Run("lengths", func(t *testing.T) { - t.Parallel() - - lengths := []int{ - 124, - 125, - 126, - 4096, - 16384, - 65535, - 65536, - 65537, - 131072, - } - - for _, n := range lengths { - n := n - t.Run(strconv.Itoa(n), func(t *testing.T) { - t.Parallel() - - testHeader(t, Header{ - PayloadLength: int64(n), - }) - }) - } - }) - - t.Run("fuzz", func(t *testing.T) { - t.Parallel() - - for i := 0; i < 10000; i++ { - h := Header{ - Fin: randBool(), - RSV1: randBool(), - RSV2: randBool(), - RSV3: randBool(), - Opcode: Opcode(rand.Intn(1 << 4)), - - Masked: randBool(), - PayloadLength: rand.Int63(), - } - - if h.Masked { - h.MaskKey = rand.Uint32() - } - - testHeader(t, h) - } - }) -} - -func testHeader(t *testing.T, h Header) { - b := h.Bytes(nil) - r := bytes.NewReader(b) - h2, err := ReadHeader(r, nil) - if err != nil { - t.Logf("Header: %#v", h) - t.Logf("bytes: %b", b) - t.Fatalf("failed to read Header: %v", err) - } - - if !cmp.Equal(h, h2, cmp.AllowUnexported(Header{})) { - t.Logf("Header: %#v", h) - t.Logf("bytes: %b", b) - t.Fatalf("parsed and read Header differ: %v", cmp.Diff(h, h2, cmp.AllowUnexported(Header{}))) - } -} diff --git a/internal/wsgrace/wsgrace.go b/internal/wsgrace/wsgrace.go deleted file mode 100644 index 513af1fe..00000000 --- a/internal/wsgrace/wsgrace.go +++ /dev/null @@ -1,50 +0,0 @@ -package wsgrace - -import ( - "context" - "fmt" - "net/http" - "sync/atomic" - "time" -) - -// Grace wraps s.Handler to gracefully shutdown WebSocket connections. -// The returned function must be used to close the server instead of s.Close. -func Grace(s *http.Server) (closeFn func() error) { - h := s.Handler - var conns int64 - s.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - atomic.AddInt64(&conns, 1) - defer atomic.AddInt64(&conns, -1) - - ctx, cancel := context.WithTimeout(r.Context(), time.Minute) - defer cancel() - - r = r.WithContext(ctx) - - h.ServeHTTP(w, r) - }) - - return func() error { - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - - err := s.Shutdown(ctx) - if err != nil { - return fmt.Errorf("server shutdown failed: %v", err) - } - - t := time.NewTicker(time.Millisecond * 10) - defer t.Stop() - for { - select { - case <-t.C: - if atomic.LoadInt64(&conns) == 0 { - return nil - } - case <-ctx.Done(): - return fmt.Errorf("failed to wait for WebSocket connections: %v", ctx.Err()) - } - } - } -} diff --git a/js_test.go b/js_test.go deleted file mode 100644 index 80af7896..00000000 --- a/js_test.go +++ /dev/null @@ -1,50 +0,0 @@ -package websocket_test - -import ( - "context" - "fmt" - "net/http" - "nhooyr.io/websocket/internal/wsecho" - "os" - "os/exec" - "strings" - "testing" - "time" - - "nhooyr.io/websocket" -) - -func TestJS(t *testing.T) { - t.Parallel() - - s, closeFn := testServer(t, func(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ - Subprotocols: []string{"echo"}, - InsecureSkipVerify: true, - }) - if err != nil { - return err - } - defer c.Close(websocket.StatusInternalError, "") - - err = wsecho.Loop(r.Context(), c) - if websocket.CloseStatus(err) != websocket.StatusNormalClosure { - return err - } - return nil - }, false) - defer closeFn() - - wsURL := strings.Replace(s.URL, "http", "ws", 1) - - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - - cmd := exec.CommandContext(ctx, "go", "test", "-exec=wasmbrowsertest", "./...") - cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm", fmt.Sprintf("WS_ECHO_SERVER_URL=%v", wsURL)) - - b, err := cmd.CombinedOutput() - if err != nil { - t.Fatalf("wasm test binary failed: %v:\n%s", err, b) - } -} diff --git a/read.go b/read.go new file mode 100644 index 00000000..97096f74 --- /dev/null +++ b/read.go @@ -0,0 +1,479 @@ +package websocket + +import ( + "bufio" + "context" + "errors" + "fmt" + "io" + "io/ioutil" + "log" + "nhooyr.io/websocket/internal/errd" + "strings" + "sync/atomic" + "time" +) + +// Reader waits until there is a WebSocket data message to read +// from the connection. +// It returns the type of the message and a reader to read it. +// The passed context will also bound the reader. +// Ensure you read to EOF otherwise the connection will hang. +// +// All returned errors will cause the connection +// to be closed so you do not need to write your own error message. +// This applies to the Read methods in the wsjson/wspb subpackages as well. +// +// You must read from the connection for control frames to be handled. +// Thus if you expect messages to take a long time to be responded to, +// you should handle such messages async to reading from the connection +// to ensure control frames are promptly handled. +// +// If you do not expect any data messages from the peer, call CloseRead. +// +// Only one Reader may be open at a time. +// +// If you need a separate timeout on the Reader call and then the message +// Read, use time.AfterFunc to cancel the context passed in early. +// See https://github.com/nhooyr/websocket/issues/87#issue-451703332 +// Most users should not need this. +func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) { + typ, r, err := c.cr.reader(ctx) + if err != nil { + return 0, nil, fmt.Errorf("failed to get reader: %w", err) + } + return typ, r, nil +} + +// Read is a convenience method to read a single message from the connection. +// +// See the Reader method to reuse buffers or for streaming. +// The docs on Reader apply to this method as well. +func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { + typ, r, err := c.Reader(ctx) + if err != nil { + return 0, nil, err + } + + b, err := ioutil.ReadAll(r) + return typ, b, err +} + +// CloseRead will start a goroutine to read from the connection until it is closed or a data message +// is received. If a data message is received, the connection will be closed with StatusPolicyViolation. +// Since CloseRead reads from the connection, it will respond to ping, pong and close frames. +// After calling this method, you cannot read any data messages from the connection. +// The returned context will be cancelled when the connection is closed. +// +// Use this when you do not want to read data messages from the connection anymore but will +// want to write messages to it. +func (c *Conn) CloseRead(ctx context.Context) context.Context { + ctx, cancel := context.WithCancel(ctx) + go func() { + defer cancel() + c.Reader(ctx) + c.Close(StatusPolicyViolation, "unexpected data message") + }() + return ctx +} + +// SetReadLimit sets the max number of bytes to read for a single message. +// It applies to the Reader and Read methods. +// +// By default, the connection has a message read limit of 32768 bytes. +// +// When the limit is hit, the connection will be closed with StatusMessageTooBig. +func (c *Conn) SetReadLimit(n int64) { + c.cr.mr.lr.limit.Store(n) +} + +type connReader struct { + c *Conn + br *bufio.Reader + timeout chan context.Context + + mu mu + controlPayloadBuf [maxControlPayload]byte + mr *msgReader +} + +func (cr *connReader) init(c *Conn, br *bufio.Reader) { + cr.c = c + cr.br = br + cr.timeout = make(chan context.Context) + + cr.mr = &msgReader{ + cr: cr, + fin: true, + } + + cr.mr.lr = newLimitReader(c, readerFunc(cr.mr.read), 32768) + if c.deflateNegotiated() && cr.contextTakeover() { + cr.ensureFlateReader() + } +} + +func (cr *connReader) ensureFlateReader() { + cr.mr.fr = getFlateReader(readerFunc(cr.mr.read)) + cr.mr.lr.reset(cr.mr.fr) +} + +func (cr *connReader) close() { + cr.mu.Lock(context.Background()) + if cr.c.client { + putBufioReader(cr.br) + } + if cr.c.deflateNegotiated() && cr.contextTakeover() { + putFlateReader(cr.mr.fr) + } +} + +func (cr *connReader) contextTakeover() bool { + if cr.c.client { + return cr.c.copts.serverNoContextTakeover + } + return cr.c.copts.clientNoContextTakeover +} + +func (cr *connReader) rsv1Illegal(h header) bool { + // If compression is enabled, rsv1 is always illegal. + if !cr.c.deflateNegotiated() { + return true + } + // rsv1 is only allowed on data frames beginning messages. + if h.opcode != opText && h.opcode != opBinary { + return true + } + return false +} + +func (cr *connReader) loop(ctx context.Context) (header, error) { + for { + h, err := cr.frameHeader(ctx) + if err != nil { + return header{}, err + } + + if h.rsv1 && cr.rsv1Illegal(h) || h.rsv2 || h.rsv3 { + err := fmt.Errorf("received header with unexpected rsv bits set: %v:%v:%v", h.rsv1, h.rsv2, h.rsv3) + cr.c.cw.error(StatusProtocolError, err) + return header{}, err + } + + if !cr.c.client && !h.masked { + return header{}, errors.New("received unmasked frame from client") + } + + switch h.opcode { + case opClose, opPing, opPong: + err = cr.control(ctx, h) + if err != nil { + // Pass through CloseErrors when receiving a close frame. + if h.opcode == opClose && CloseStatus(err) != -1 { + return header{}, err + } + return header{}, fmt.Errorf("failed to handle control frame %v: %w", h.opcode, err) + } + case opContinuation, opText, opBinary: + return h, nil + default: + err := fmt.Errorf("received unknown opcode %v", h.opcode) + cr.c.cw.error(StatusProtocolError, err) + return header{}, err + } + } +} + +func (cr *connReader) frameHeader(ctx context.Context) (header, error) { + select { + case <-cr.c.closed: + return header{}, cr.c.closeErr + case cr.timeout <- ctx: + } + + h, err := readFrameHeader(cr.br) + if err != nil { + select { + case <-cr.c.closed: + return header{}, cr.c.closeErr + case <-ctx.Done(): + return header{}, ctx.Err() + default: + cr.c.close(err) + return header{}, err + } + } + + select { + case <-cr.c.closed: + return header{}, cr.c.closeErr + case cr.timeout <- context.Background(): + } + + return h, nil +} + +func (cr *connReader) framePayload(ctx context.Context, p []byte) (int, error) { + select { + case <-cr.c.closed: + return 0, cr.c.closeErr + case cr.timeout <- ctx: + } + + n, err := io.ReadFull(cr.br, p) + if err != nil { + select { + case <-cr.c.closed: + return n, cr.c.closeErr + case <-ctx.Done(): + return n, ctx.Err() + default: + err = fmt.Errorf("failed to read frame payload: %w", err) + cr.c.close(err) + return n, err + } + } + + select { + case <-cr.c.closed: + return n, cr.c.closeErr + case cr.timeout <- context.Background(): + } + + return n, err +} + +func (cr *connReader) control(ctx context.Context, h header) error { + if h.payloadLength < 0 { + err := fmt.Errorf("received header with negative payload length: %v", h.payloadLength) + cr.c.cw.error(StatusProtocolError, err) + return err + } + + if h.payloadLength > maxControlPayload { + err := fmt.Errorf("received too big control frame at %v bytes", h.payloadLength) + cr.c.cw.error(StatusProtocolError, err) + return err + } + + if !h.fin { + err := errors.New("received fragmented control frame") + cr.c.cw.error(StatusProtocolError, err) + return err + } + + ctx, cancel := context.WithTimeout(ctx, time.Second*5) + defer cancel() + + b := cr.controlPayloadBuf[:h.payloadLength] + _, err := cr.framePayload(ctx, b) + if err != nil { + return err + } + + if h.masked { + mask(h.maskKey, b) + } + + switch h.opcode { + case opPing: + return cr.c.cw.control(ctx, opPong, b) + case opPong: + cr.c.activePingsMu.Lock() + pong, ok := cr.c.activePings[string(b)] + cr.c.activePingsMu.Unlock() + if ok { + close(pong) + } + return nil + } + + ce, err := parseClosePayload(b) + if err != nil { + err = fmt.Errorf("received invalid close payload: %w", err) + cr.c.cw.error(StatusProtocolError, err) + return err + } + + err = fmt.Errorf("received close frame: %w", ce) + cr.c.setCloseErr(err) + cr.c.cw.control(context.Background(), opClose, ce.bytes()) + return err +} + +func (cr *connReader) reader(ctx context.Context) (MessageType, io.Reader, error) { + err := cr.mu.Lock(ctx) + if err != nil { + return 0, nil, err + } + defer cr.mu.Unlock() + + if !cr.mr.fin { + return 0, nil, errors.New("previous message not read to completion") + } + + h, err := cr.loop(ctx) + if err != nil { + return 0, nil, err + } + + if h.opcode == opContinuation { + err := errors.New("received continuation frame without text or binary frame") + cr.c.cw.error(StatusProtocolError, err) + return 0, nil, err + } + + cr.mr.reset(ctx, h) + + return MessageType(h.opcode), cr.mr, nil +} + +type msgReader struct { + cr *connReader + fr io.Reader + lr *limitReader + + ctx context.Context + + deflate bool + deflateTail strings.Reader + + payloadLength int64 + maskKey uint32 + fin bool +} + +func (mr *msgReader) reset(ctx context.Context, h header) { + mr.ctx = ctx + mr.deflate = h.rsv1 + if mr.deflate { + mr.deflateTail.Reset(deflateMessageTail) + if !mr.cr.contextTakeover() { + mr.cr.ensureFlateReader() + } + } + mr.setFrame(h) + mr.fin = false +} + +func (mr *msgReader) setFrame(h header) { + mr.payloadLength = h.payloadLength + mr.maskKey = h.maskKey + mr.fin = h.fin +} + +func (mr *msgReader) Read(p []byte) (_ int, err error) { + defer func() { + errd.Wrap(&err, "failed to read") + if errors.Is(err, io.EOF) { + err = io.EOF + } + }() + + err = mr.cr.mu.Lock(mr.ctx) + if err != nil { + return 0, err + } + defer mr.cr.mu.Unlock() + + if mr.payloadLength == 0 && mr.fin { + if mr.cr.c.deflateNegotiated() && !mr.cr.contextTakeover() { + if mr.fr != nil { + putFlateReader(mr.fr) + mr.fr = nil + } + } + return 0, io.EOF + } + + return mr.lr.Read(p) +} + +func (mr *msgReader) read(p []byte) (int, error) { + log.Println("compress", mr.deflate) + + if mr.payloadLength == 0 { + h, err := mr.cr.loop(mr.ctx) + if err != nil { + return 0, err + } + if h.opcode != opContinuation { + err := errors.New("received new data message without finishing the previous message") + mr.cr.c.cw.error(StatusProtocolError, err) + return 0, err + } + mr.setFrame(h) + } + + if int64(len(p)) > mr.payloadLength { + p = p[:mr.payloadLength] + } + + n, err := mr.cr.framePayload(mr.ctx, p) + if err != nil { + return n, err + } + + mr.payloadLength -= int64(n) + + if !mr.cr.c.client { + mr.maskKey = mask(mr.maskKey, p) + } + + return n, nil +} + +type limitReader struct { + c *Conn + r io.Reader + limit atomicInt64 + n int64 +} + +func newLimitReader(c *Conn, r io.Reader, limit int64) *limitReader { + lr := &limitReader{ + c: c, + } + lr.limit.Store(limit) + lr.reset(r) + return lr +} + +func (lr *limitReader) reset(r io.Reader) { + lr.n = lr.limit.Load() + lr.r = r +} + +func (lr *limitReader) Read(p []byte) (int, error) { + if lr.n <= 0 { + err := fmt.Errorf("read limited at %v bytes", lr.limit.Load()) + lr.c.cw.error(StatusMessageTooBig, err) + return 0, err + } + + if int64(len(p)) > lr.n { + p = p[:lr.n] + } + n, err := lr.r.Read(p) + lr.n -= int64(n) + return n, err +} + +type atomicInt64 struct { + i atomic.Value +} + +func (v *atomicInt64) Load() int64 { + i, _ := v.i.Load().(int64) + return i +} + +func (v *atomicInt64) Store(i int64) { + v.i.Store(i) +} + +type readerFunc func(p []byte) (int, error) + +func (f readerFunc) Read(p []byte) (int, error) { + return f(p) +} diff --git a/reader.go b/reader.go deleted file mode 100644 index fe716569..00000000 --- a/reader.go +++ /dev/null @@ -1,31 +0,0 @@ -package websocket - -import ( - "bufio" - "context" - "io" - "nhooyr.io/websocket/internal/atomicint" - "nhooyr.io/websocket/internal/wsframe" - "strings" -) - -type reader struct { - // Acquired before performing any sort of read operation. - readLock chan struct{} - - c *Conn - - deflateReader io.Reader - br *bufio.Reader - - readClosed *atomicint.Int64 - readHeaderBuf []byte - controlPayloadBuf []byte - - msgCtx context.Context - msgCompressed bool - frameHeader wsframe.Header - frameMaskKey uint32 - frameEOF bool - deflateTail strings.Reader -} diff --git a/write.go b/write.go new file mode 100644 index 00000000..5bb489b4 --- /dev/null +++ b/write.go @@ -0,0 +1,348 @@ +package websocket + +import ( + "bufio" + "compress/flate" + "context" + "crypto/rand" + "encoding/binary" + "errors" + "fmt" + "io" + "nhooyr.io/websocket/internal/errd" + "time" +) + +// Writer returns a writer bounded by the context that will write +// a WebSocket message of type dataType to the connection. +// +// You must close the writer once you have written the entire message. +// +// Only one writer can be open at a time, multiple calls will block until the previous writer +// is closed. +// +// Never close the returned writer twice. +func (c *Conn) Writer(ctx context.Context, typ MessageType) (io.WriteCloser, error) { + w, err := c.cw.writer(ctx, typ) + if err != nil { + return nil, fmt.Errorf("failed to get writer: %w", err) + } + return w, nil +} + +// Write writes a message to the connection. +// +// See the Writer method if you want to stream a message. +// +// If compression is disabled, then it is guaranteed to write the message +// in a single frame. +func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { + _, err := c.cw.write(ctx, typ, p) + if err != nil { + return fmt.Errorf("failed to write msg: %w", err) + } + return nil +} + +type connWriter struct { + c *Conn + bw *bufio.Writer + + writeBuf []byte + + mw *messageWriter + frameMu mu + h header + + timeout chan context.Context +} + +func (cw *connWriter) init(c *Conn, bw *bufio.Writer) { + cw.c = c + cw.bw = bw + + if cw.c.client { + cw.writeBuf = extractBufioWriterBuf(cw.bw, c.rwc) + } + + cw.timeout = make(chan context.Context) + + cw.mw = &messageWriter{ + cw: cw, + } + cw.mw.tw = &trimLastFourBytesWriter{ + w: writerFunc(cw.mw.write), + } + if cw.c.deflateNegotiated() && cw.mw.contextTakeover() { + cw.mw.ensureFlateWriter() + } +} + +func (mw *messageWriter) ensureFlateWriter() { + mw.fw = getFlateWriter(mw.tw) +} + +func (cw *connWriter) close() { + if cw.c.client { + cw.frameMu.Lock(context.Background()) + putBufioWriter(cw.bw) + } + if cw.c.deflateNegotiated() && cw.mw.contextTakeover() { + cw.mw.mu.Lock(context.Background()) + putFlateWriter(cw.mw.fw) + } +} + +func (mw *messageWriter) contextTakeover() bool { + if mw.cw.c.client { + return mw.cw.c.copts.clientNoContextTakeover + } + return mw.cw.c.copts.serverNoContextTakeover +} + +func (cw *connWriter) writer(ctx context.Context, typ MessageType) (io.WriteCloser, error) { + err := cw.mw.reset(ctx, typ) + if err != nil { + return nil, err + } + return cw.mw, nil +} + +func (cw *connWriter) write(ctx context.Context, typ MessageType, p []byte) (int, error) { + ww, err := cw.writer(ctx, typ) + if err != nil { + return 0, err + } + + if !cw.c.deflateNegotiated() { + // Fast single frame path. + defer cw.mw.mu.Unlock() + return cw.frame(ctx, true, cw.mw.opcode, p) + } + + n, err := ww.Write(p) + if err != nil { + return n, err + } + + err = ww.Close() + return n, err +} + +type messageWriter struct { + cw *connWriter + + mu mu + compress bool + tw *trimLastFourBytesWriter + fw *flate.Writer + ctx context.Context + opcode opcode + closed bool +} + +func (mw *messageWriter) reset(ctx context.Context, typ MessageType) error { + err := mw.mu.Lock(ctx) + if err != nil { + return err + } + + mw.closed = false + mw.ctx = ctx + mw.opcode = opcode(typ) + return nil +} + +// Write writes the given bytes to the WebSocket connection. +func (mw *messageWriter) Write(p []byte) (_ int, err error) { + defer errd.Wrap(&err, "failed to write") + + if mw.closed { + return 0, errors.New("cannot use closed writer") + } + + if mw.cw.c.deflateNegotiated() { + if !mw.compress { + if !mw.contextTakeover() { + mw.ensureFlateWriter() + } + mw.tw.reset() + mw.compress = true + } + + return mw.fw.Write(p) + } + + return mw.write(p) +} + +func (mw *messageWriter) write(p []byte) (int, error) { + n, err := mw.cw.frame(mw.ctx, false, mw.opcode, p) + if err != nil { + return n, fmt.Errorf("failed to write data frame: %w", err) + } + mw.opcode = opContinuation + return n, nil +} + +// Close flushes the frame to the connection. +// This must be called for every messageWriter. +func (mw *messageWriter) Close() (err error) { + defer errd.Wrap(&err, "failed to close writer") + + if mw.closed { + return errors.New("cannot use closed writer") + } + mw.closed = true + + if mw.cw.c.deflateNegotiated() { + err = mw.fw.Flush() + if err != nil { + return fmt.Errorf("failed to flush flate writer: %w", err) + } + } + + _, err = mw.cw.frame(mw.ctx, true, mw.opcode, nil) + if err != nil { + return fmt.Errorf("failed to write fin frame: %w", err) + } + + if mw.compress && !mw.contextTakeover() { + putFlateWriter(mw.fw) + mw.compress = false + } + + mw.mu.Unlock() + return nil +} + +func (cw *connWriter) control(ctx context.Context, opcode opcode, p []byte) error { + ctx, cancel := context.WithTimeout(ctx, time.Second*5) + defer cancel() + + _, err := cw.frame(ctx, true, opcode, p) + if err != nil { + return fmt.Errorf("failed to write control frame %v: %w", opcode, err) + } + return nil +} + +// frame handles all writes to the connection. +func (cw *connWriter) frame(ctx context.Context, fin bool, opcode opcode, p []byte) (int, error) { + err := cw.frameMu.Lock(ctx) + if err != nil { + return 0, err + } + defer cw.frameMu.Unlock() + + select { + case <-cw.c.closed: + return 0, cw.c.closeErr + case cw.timeout <- ctx: + } + + cw.h.fin = fin + cw.h.opcode = opcode + cw.h.masked = cw.c.client + cw.h.payloadLength = int64(len(p)) + + cw.h.rsv1 = false + if cw.mw.compress && (opcode == opText || opcode == opBinary) { + cw.h.rsv1 = true + } + + if cw.h.masked { + err = binary.Read(rand.Reader, binary.LittleEndian, &cw.h.maskKey) + if err != nil { + return 0, fmt.Errorf("failed to generate masking key: %w", err) + } + } + + err = writeFrameHeader(cw.h, cw.bw) + if err != nil { + return 0, err + } + + n, err := cw.framePayload(p) + if err != nil { + return n, err + } + + if cw.h.fin { + err = cw.bw.Flush() + if err != nil { + return n, fmt.Errorf("failed to flush: %w", err) + } + } + + select { + case <-cw.c.closed: + return n, cw.c.closeErr + case cw.timeout <- context.Background(): + } + + return n, nil +} + +func (cw *connWriter) framePayload(p []byte) (_ int, err error) { + defer errd.Wrap(&err, "failed to write frame payload") + + if !cw.h.masked { + return cw.bw.Write(p) + } + + var n int + maskKey := cw.h.maskKey + for len(p) > 0 { + // If the buffer is full, we need to flush. + if cw.bw.Available() == 0 { + err = cw.bw.Flush() + if err != nil { + return n, err + } + } + + // Start of next write in the buffer. + i := cw.bw.Buffered() + + j := len(p) + if j > cw.bw.Available() { + j = cw.bw.Available() + } + + _, err := cw.bw.Write(p[:j]) + if err != nil { + return n, err + } + + maskKey = mask(maskKey, cw.writeBuf[i:cw.bw.Buffered()]) + + p = p[j:] + n += j + } + + return n, nil +} + +type writerFunc func(p []byte) (int, error) + +func (f writerFunc) Write(p []byte) (int, error) { + return f(p) +} + +// extractBufioWriterBuf grabs the []byte backing a *bufio.Writer +// and returns it. +func extractBufioWriterBuf(bw *bufio.Writer, w io.Writer) []byte { + var writeBuf []byte + bw.Reset(writerFunc(func(p2 []byte) (int, error) { + writeBuf = p2[:cap(p2)] + return len(p2), nil + })) + + bw.WriteByte(0) + bw.Flush() + + bw.Reset(w) + + return writeBuf +} diff --git a/writer.go b/writer.go deleted file mode 100644 index b31d57ad..00000000 --- a/writer.go +++ /dev/null @@ -1,5 +0,0 @@ -package websocket - -type writer struct { - -} diff --git a/ws_js.go b/ws_js.go index 4c067430..10ce0da8 100644 --- a/ws_js.go +++ b/ws_js.go @@ -9,7 +9,7 @@ import ( "fmt" "io" "net/http" - "nhooyr.io/websocket/internal/atomicint" + "nhooyr.io/websocket/internal/wssync" "reflect" "runtime" "sync" @@ -24,10 +24,10 @@ type Conn struct { ws wsjs.WebSocket // read limit for a message in bytes. - msgReadLimit *atomicint.Int64 + msgReadLimit *wssync.Int64 closingMu sync.Mutex - isReadClosed *atomicint.Int64 + isReadClosed *wssync.Int64 closeOnce sync.Once closed chan struct{} closeErrOnce sync.Once @@ -59,10 +59,10 @@ func (c *Conn) init() { c.closed = make(chan struct{}) c.readSignal = make(chan struct{}, 1) - c.msgReadLimit = &atomicint.Int64{} + c.msgReadLimit = &wssync.Int64{} c.msgReadLimit.Store(32768) - c.isReadClosed = &atomicint.Int64{} + c.isReadClosed = &wssync.Int64{} c.releaseOnClose = c.ws.OnClose(func(e wsjs.CloseEvent) { err := CloseError{ @@ -105,7 +105,7 @@ func (c *Conn) closeWithInternal() { // The maximum time spent waiting is bounded by the context. func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { if c.isReadClosed.Load() == 1 { - return 0, nil, fmt.Errorf("websocket connection read closed") + return 0, nil, errors.New("websocket connection read closed") } typ, p, err := c.read(ctx) diff --git a/wsjson/wsjson.go b/wsjson/wsjson.go index 9fa8b54c..e8188051 100644 --- a/wsjson/wsjson.go +++ b/wsjson/wsjson.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "log" "nhooyr.io/websocket" "nhooyr.io/websocket/internal/bufpool" ) @@ -41,6 +42,7 @@ func read(ctx context.Context, c *websocket.Conn, v interface{}) error { err = json.Unmarshal(b.Bytes(), v) if err != nil { c.Close(websocket.StatusInvalidFramePayloadData, "failed to unmarshal JSON") + log.Printf("%X", b.Bytes()) return fmt.Errorf("failed to unmarshal json: %w", err) } From dd107dd12713665b37436d2af3302f9e83409240 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 28 Nov 2019 14:59:31 -0500 Subject: [PATCH 221/519] Update CI --- .github/CODEOWNERS | 1 + .github/workflows/ci.yml | 38 +++++++++++++++++---- Makefile | 4 --- ci/{ => image}/Dockerfile | 13 -------- conn_test.go | 69 ++++++++++++++++++++------------------- 5 files changed, 68 insertions(+), 57 deletions(-) create mode 100644 .github/CODEOWNERS rename ci/{ => image}/Dockerfile (52%) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..d2eae33e --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @nhooyr diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2cc69828..865c67f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,22 +4,48 @@ on: [push, pull_request] jobs: fmt: runs-on: ubuntu-latest - container: nhooyr/websocket-ci@sha256:8a8fd73fdea33585d50a33619c4936adfd016246a2ed6bbfbf06def24b518a6a steps: - uses: actions/checkout@v1 - - run: make fmt + - uses: actions/cache@v1 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: make fmt + uses: ./ci/image + with: + args: make fmt + lint: runs-on: ubuntu-latest - container: nhooyr/websocket-ci@sha256:8a8fd73fdea33585d50a33619c4936adfd016246a2ed6bbfbf06def24b518a6a steps: - uses: actions/checkout@v1 - - run: make lint + - uses: actions/cache@v1 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: make lint + uses: ./ci/image + with: + args: make lint + test: runs-on: ubuntu-latest - container: nhooyr/websocket-ci@sha256:8a8fd73fdea33585d50a33619c4936adfd016246a2ed6bbfbf06def24b518a6a steps: - uses: actions/checkout@v1 - - run: make test + - uses: actions/cache@v1 + with: + path: ~/go/pkg/mod + key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ runner.os }}-go- + - name: make test + uses: ./ci/image + with: + args: make test env: COVERALLS_TOKEN: ${{ secrets.github_token }} - name: Upload coverage.html diff --git a/Makefile b/Makefile index 8c8e1a08..ad1ba257 100644 --- a/Makefile +++ b/Makefile @@ -11,7 +11,3 @@ SHELL = bash include ci/fmt.mk include ci/lint.mk include ci/test.mk - -ci-image: - docker build -f ./ci/Dockerfile -t nhooyr/websocket-ci . - docker push nhooyr/websocket-ci diff --git a/ci/Dockerfile b/ci/image/Dockerfile similarity index 52% rename from ci/Dockerfile rename to ci/image/Dockerfile index 0f0fc7d9..ccfac109 100644 --- a/ci/Dockerfile +++ b/ci/image/Dockerfile @@ -5,8 +5,6 @@ RUN apt-get install -y chromium RUN apt-get install -y npm RUN apt-get install -y jq -ENV GOPATH=/root/gopath -ENV PATH=$GOPATH/bin:$PATH ENV GOFLAGS="-mod=readonly" ENV PAGER=cat ENV CI=true @@ -18,14 +16,3 @@ RUN go get golang.org/x/tools/cmd/goimports RUN go get golang.org/x/lint/golint RUN go get github.com/agnivade/wasmbrowsertest RUN go get github.com/mattn/goveralls - -# Cache go modules and build cache. -COPY . /tmp/websocket -RUN cd /tmp/websocket && \ - CI= make && \ - rm -rf /tmp/websocket - -# GitHub actions tries to override HOME to /github/home and then -# mounts a temp directory into there. We do not want this behaviour. -# I assume it is so that $HOME is preserved between steps in a job. -ENTRYPOINT ["env", "HOME=/root"] diff --git a/conn_test.go b/conn_test.go index 992c8861..1014dbf3 100644 --- a/conn_test.go +++ b/conn_test.go @@ -17,6 +17,41 @@ import ( "nhooyr.io/websocket" ) +func TestConn(t *testing.T) { + t.Parallel() + + t.Run("json", func(t *testing.T) { + s, closeFn := testServer(t, func(w http.ResponseWriter, r *http.Request) { + c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + Subprotocols: []string{"echo"}, + InsecureSkipVerify: true, + }) + assert.Success(t, err) + defer c.Close(websocket.StatusInternalError, "") + + err = echoLoop(r.Context(), c) + assertCloseStatus(t, websocket.StatusNormalClosure, err) + }, false) + defer closeFn() + + wsURL := strings.Replace(s.URL, "http", "ws", 1) + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + opts := &websocket.DialOptions{ + Subprotocols: []string{"echo"}, + } + opts.HTTPClient = s.Client() + + c, _, err := websocket.Dial(ctx, wsURL, opts) + assert.Success(t, err) + + assertJSONEcho(t, ctx, c, 2) + }) +} + + func testServer(tb testing.TB, fn func(w http.ResponseWriter, r *http.Request), tls bool) (s *httptest.Server, closeFn func()) { h := http.HandlerFunc(fn) if tls { @@ -108,37 +143,3 @@ func echoLoop(ctx context.Context, c *websocket.Conn) error { } } } - -func TestConn(t *testing.T) { - t.Parallel() - - t.Run("json", func(t *testing.T) { - s, closeFn := testServer(t, func(w http.ResponseWriter, r *http.Request) { - c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ - Subprotocols: []string{"echo"}, - InsecureSkipVerify: true, - }) - assert.Success(t, err) - defer c.Close(websocket.StatusInternalError, "") - - err = echoLoop(r.Context(), c) - assertCloseStatus(t, websocket.StatusNormalClosure, err) - }, false) - defer closeFn() - - wsURL := strings.Replace(s.URL, "http", "ws", 1) - - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - - opts := &websocket.DialOptions{ - Subprotocols: []string{"echo"}, - } - opts.HTTPClient = s.Client() - - c, _, err := websocket.Dial(ctx, wsURL, opts) - assert.Success(t, err) - - assertJSONEcho(t, ctx, c, 2) - }) -} From 6c6b8e9af2030e9ce4352ae006092255ca62fef5 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 28 Nov 2019 15:58:41 -0500 Subject: [PATCH 222/519] Cleanup wspb and wsjson --- .github/CONTRIBUTING.md | 45 -------------- .github/ISSUE_TEMPLATE.md | 1 - .github/PULL_REQUEST_TEMPLATE.md | 4 -- README.md | 4 -- assert_test.go | 12 ++-- ci/test.mk | 4 +- close.go | 6 +- close_test.go | 2 +- frame.go | 1 - frame_test.go | 6 +- go.mod | 4 +- internal/assert/assert.go | 69 ++------------------- internal/assert/cmp.go | 52 ++++++++++++++++ internal/{bufpool/buf.go => bpool/bpool.go} | 10 +-- internal/bufpool/buf_test.go | 46 -------------- internal/errd/errd.go | 11 ---- internal/errd/wrap.go | 14 +++++ read.go | 3 - write.go | 1 - ws_js.go | 6 +- wsjson/wsjson.go | 60 ++++++++---------- wspb/wspb.go | 53 ++++++++-------- 22 files changed, 146 insertions(+), 268 deletions(-) delete mode 100644 .github/CONTRIBUTING.md delete mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 internal/assert/cmp.go rename internal/{bufpool/buf.go => bpool/bpool.go} (72%) delete mode 100644 internal/bufpool/buf_test.go delete mode 100644 internal/errd/errd.go create mode 100644 internal/errd/wrap.go diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md deleted file mode 100644 index 357c314a..00000000 --- a/.github/CONTRIBUTING.md +++ /dev/null @@ -1,45 +0,0 @@ -# Contributing - -## Issues - -Please be as descriptive as possible. - -Reproducible examples are key to finding and fixing bugs. - -## Pull requests - -Good issues for first time contributors are marked as such. Feel free to -reach out for clarification on what needs to be done. - -Split up large changes into several small descriptive commits. - -Capitalize the first word in the commit message title. - -The commit message title should use the verb tense + phrase that completes the blank in - -> This change modifies websocket to \_\_\_\_\_\_\_\_\_ - -Be sure to [correctly link](https://help.github.com/en/articles/closing-issues-using-keywords) -to an existing issue if one exists. In general, create an issue before a PR to get some -discussion going and to make sure you do not spend time on a PR that may be rejected. - -CI must pass on your changes for them to be merged. - -### CI - -CI will ensure your code is formatted, lints and passes tests. -It will collect coverage and report it to [coveralls](https://coveralls.io/github/nhooyr/websocket) -and also upload a html `coverage` artifact that you can download to browse coverage. - -You can run CI locally. - -See [ci/image/Dockerfile](../ci/image/Dockerfile) for the installation of the CI dependencies on Ubuntu. - -1. `make fmt` performs code generation and formatting. -1. `make lint` performs linting. -1. `make test` runs tests. -1. `make` runs the above targets. - -For coverage details locally, see `ci/out/coverage.html` after running `make test`. - -You can run tests normally with `go test`. `make test` wraps around `go test` to collect coverage. diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index fce01709..7b580937 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,4 +1,3 @@ diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md deleted file mode 100644 index 901c994a..00000000 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ /dev/null @@ -1,4 +0,0 @@ - diff --git a/README.md b/README.md index 17c7c838..8ac418a7 100644 --- a/README.md +++ b/README.md @@ -165,10 +165,6 @@ faster, the compression extensions are fully supported and as much as possible i See the gorilla/websocket comparison for more performance details. -## Contributing - -See [.github/CONTRIBUTING.md](.github/CONTRIBUTING.md). - ## Users If your company or project is using this library, feel free to open an issue or PR to amend this list. diff --git a/assert_test.go b/assert_test.go index 0cc9dfe3..6e4e75e6 100644 --- a/assert_test.go +++ b/assert_test.go @@ -33,7 +33,7 @@ func assertJSONEcho(t *testing.T, ctx context.Context, c *websocket.Conn, n int) err = wsjson.Read(ctx, c, &act) assert.Success(t, err) - assert.Equalf(t, exp, act, "unexpected JSON") + assert.Equal(t, exp, act, "unexpected JSON") } func assertJSONRead(t *testing.T, ctx context.Context, c *websocket.Conn, exp interface{}) { @@ -43,7 +43,7 @@ func assertJSONRead(t *testing.T, ctx context.Context, c *websocket.Conn, exp in err := wsjson.Read(ctx, c, &act) assert.Success(t, err) - assert.Equalf(t, exp, act, "unexpected JSON") + assert.Equal(t, exp, act, "unexpected JSON") } func randString(n int) string { @@ -69,18 +69,18 @@ func assertEcho(t *testing.T, ctx context.Context, c *websocket.Conn, typ websoc typ2, p2, err := c.Read(ctx) assert.Success(t, err) - assert.Equalf(t, typ, typ2, "unexpected data type") - assert.Equalf(t, p, p2, "unexpected payload") + assert.Equal(t, typ, typ2, "unexpected data type") + assert.Equal(t, p, p2, "unexpected payload") } func assertSubprotocol(t *testing.T, c *websocket.Conn, exp string) { t.Helper() - assert.Equalf(t, exp, c.Subprotocol(), "unexpected subprotocol") + assert.Equal(t, exp, c.Subprotocol(), "unexpected subprotocol") } func assertCloseStatus(t *testing.T, exp websocket.StatusCode, err error) { t.Helper() - assert.Equalf(t, exp, websocket.CloseStatus(err), "unexpected status code") + assert.Equal(t, exp, websocket.CloseStatus(err), "unexpected status code") } diff --git a/ci/test.mk b/ci/test.mk index 3183552e..9e4e0803 100644 --- a/ci/test.mk +++ b/ci/test.mk @@ -20,6 +20,4 @@ coveralls: gotest gotest: go test -covermode=count -coverprofile=ci/out/coverage.prof -coverpkg=./... $${GOTESTFLAGS-} ./... sed -i '/_stringer\.go/d' ci/out/coverage.prof - sed -i '/wsecho\.go/d' ci/out/coverage.prof - sed -i '/assert\.go/d' ci/out/coverage.prof - sed -i '/wsgrace\.go/d' ci/out/coverage.prof + sed -i '/assert/d' ci/out/coverage.prof diff --git a/close.go b/close.go index b1bc50e9..57d69a37 100644 --- a/close.go +++ b/close.go @@ -6,7 +6,7 @@ import ( "errors" "fmt" "log" - "nhooyr.io/websocket/internal/bufpool" + "nhooyr.io/websocket/internal/bpool" "time" ) @@ -146,10 +146,10 @@ func (cr *connReader) waitClose() error { } defer cr.mu.Unlock() - b := bufpool.Get() + b := bpool.Get() buf := b.Bytes() buf = buf[:cap(buf)] - defer bufpool.Put(b) + defer bpool.Put(b) for { // TODO diff --git a/close_test.go b/close_test.go index ee10cd3f..ca51a298 100644 --- a/close_test.go +++ b/close_test.go @@ -189,7 +189,7 @@ func TestCloseStatus(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - assert.Equalf(t, tc.exp, CloseStatus(tc.in), "unexpected close status") + assert.Equal(t, tc.exp, CloseStatus(tc.in), "unexpected close status") }) } } diff --git a/frame.go b/frame.go index 0f10d553..f36334c2 100644 --- a/frame.go +++ b/frame.go @@ -5,7 +5,6 @@ import ( "encoding/binary" "math" "math/bits" - "nhooyr.io/websocket/internal/errd" ) // opcode represents a WebSocket opcode. diff --git a/frame_test.go b/frame_test.go index 0ed14aef..a4a1f5a8 100644 --- a/frame_test.go +++ b/frame_test.go @@ -90,7 +90,7 @@ func testHeader(t *testing.T, h header) { h2, err := readFrameHeader(r) assert.Success(t, err) - assert.Equalf(t, h, h2, "written and read headers differ") + assert.Equal(t, h, h2, "written and read headers differ") } func Test_mask(t *testing.T) { @@ -101,8 +101,8 @@ func Test_mask(t *testing.T) { p := []byte{0xa, 0xb, 0xc, 0xf2, 0xc} gotKey32 := mask(key32, p) - assert.Equalf(t, []byte{0, 0, 0, 0x0d, 0x6}, p, "unexpected mask") - assert.Equalf(t, bits.RotateLeft32(key32, -8), gotKey32, "unexpected mask key") + assert.Equal(t, []byte{0, 0, 0, 0x0d, 0x6}, p, "unexpected mask") + assert.Equal(t, bits.RotateLeft32(key32, -8), gotKey32, "unexpected mask key") } func basicMask(maskKey [4]byte, pos int, b []byte) int { diff --git a/go.mod b/go.mod index e6ef0014..3108c020 100644 --- a/go.mod +++ b/go.mod @@ -7,13 +7,13 @@ require ( github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee // indirect github.com/gobwas/pool v0.2.0 // indirect github.com/gobwas/ws v1.0.2 - github.com/golang/protobuf v1.3.2 + github.com/golang/protobuf v1.3.2 // indirect github.com/google/go-cmp v0.3.1 github.com/gorilla/websocket v1.4.1 github.com/kr/pretty v0.1.0 // indirect github.com/stretchr/testify v1.4.0 // indirect go.uber.org/atomic v1.4.0 // indirect - go.uber.org/multierr v1.1.0 + go.uber.org/multierr v1.1.0 // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect ) diff --git a/internal/assert/assert.go b/internal/assert/assert.go index 1d9aeced..4ebdb511 100644 --- a/internal/assert/assert.go +++ b/internal/assert/assert.go @@ -1,79 +1,29 @@ package assert import ( - "reflect" "strings" "testing" - - "github.com/google/go-cmp/cmp" ) -// https://github.com/google/go-cmp/issues/40#issuecomment-328615283 -func cmpDiff(exp, act interface{}) string { - return cmp.Diff(exp, act, deepAllowUnexported(exp, act)) -} - -func deepAllowUnexported(vs ...interface{}) cmp.Option { - m := make(map[reflect.Type]struct{}) - for _, v := range vs { - structTypes(reflect.ValueOf(v), m) - } - var typs []interface{} - for t := range m { - typs = append(typs, reflect.New(t).Elem().Interface()) - } - return cmp.AllowUnexported(typs...) -} - -func structTypes(v reflect.Value, m map[reflect.Type]struct{}) { - if !v.IsValid() { - return - } - switch v.Kind() { - case reflect.Ptr: - if !v.IsNil() { - structTypes(v.Elem(), m) - } - case reflect.Interface: - if !v.IsNil() { - structTypes(v.Elem(), m) - } - case reflect.Slice, reflect.Array: - for i := 0; i < v.Len(); i++ { - structTypes(v.Index(i), m) - } - case reflect.Map: - for _, k := range v.MapKeys() { - structTypes(v.MapIndex(k), m) - } - case reflect.Struct: - m[v.Type()] = struct{}{} - for i := 0; i < v.NumField(); i++ { - structTypes(v.Field(i), m) - } - } -} - -func Equalf(t testing.TB, exp, act interface{}, f string, v ...interface{}) { +func Equal(t testing.TB, exp, act interface{}, name string) { t.Helper() diff := cmpDiff(exp, act) if diff != "" { - t.Fatalf(f+": %v", append(v, diff)...) + t.Fatalf("unexpected %v: %v", name, diff) } } -func NotEqualf(t testing.TB, exp, act interface{}, f string, v ...interface{}) { +func NotEqual(t testing.TB, exp, act interface{}, name string) { t.Helper() - diff := cmpDiff(exp, act) - if diff == "" { - t.Fatalf(f+": %v", append(v, diff)...) + if cmpDiff(exp, act) == "" { + t.Fatalf("expected different %v: %+v", name, act) } } func Success(t testing.TB, err error) { t.Helper() if err != nil { - t.Fatalf("unexpected error: %+v", err) + t.Fatalf("unexpected error : %+v", err) } } @@ -92,10 +42,3 @@ func ErrorContains(t testing.TB, err error, sub string) { t.Fatalf("error string %q does not contain %q", errs, sub) } } - -func Panicf(t testing.TB, f string, v ...interface{}) { - r := recover() - if r == nil { - t.Fatalf(f, v...) - } -} diff --git a/internal/assert/cmp.go b/internal/assert/cmp.go new file mode 100644 index 00000000..0edcf2cd --- /dev/null +++ b/internal/assert/cmp.go @@ -0,0 +1,52 @@ +package assert + +import ( + "github.com/google/go-cmp/cmp" + "reflect" +) + +// https://github.com/google/go-cmp/issues/40#issuecomment-328615283 +func cmpDiff(exp, act interface{}) string { + return cmp.Diff(exp, act, deepAllowUnexported(exp, act)) +} + +func deepAllowUnexported(vs ...interface{}) cmp.Option { + m := make(map[reflect.Type]struct{}) + for _, v := range vs { + structTypes(reflect.ValueOf(v), m) + } + var typs []interface{} + for t := range m { + typs = append(typs, reflect.New(t).Elem().Interface()) + } + return cmp.AllowUnexported(typs...) +} + +func structTypes(v reflect.Value, m map[reflect.Type]struct{}) { + if !v.IsValid() { + return + } + switch v.Kind() { + case reflect.Ptr: + if !v.IsNil() { + structTypes(v.Elem(), m) + } + case reflect.Interface: + if !v.IsNil() { + structTypes(v.Elem(), m) + } + case reflect.Slice, reflect.Array: + for i := 0; i < v.Len(); i++ { + structTypes(v.Index(i), m) + } + case reflect.Map: + for _, k := range v.MapKeys() { + structTypes(v.MapIndex(k), m) + } + case reflect.Struct: + m[v.Type()] = struct{}{} + for i := 0; i < v.NumField(); i++ { + structTypes(v.Field(i), m) + } + } +} diff --git a/internal/bufpool/buf.go b/internal/bpool/bpool.go similarity index 72% rename from internal/bufpool/buf.go rename to internal/bpool/bpool.go index 0f7d9765..e2c5f76a 100644 --- a/internal/bufpool/buf.go +++ b/internal/bpool/bpool.go @@ -1,4 +1,4 @@ -package bufpool +package bpool import ( "bytes" @@ -10,11 +10,11 @@ var pool sync.Pool // Get returns a buffer from the pool or creates a new one if // the pool is empty. func Get() *bytes.Buffer { - b, ok := pool.Get().(*bytes.Buffer) - if !ok { - b = &bytes.Buffer{} + b := pool.Get() + if b == nil { + return &bytes.Buffer{} } - return b + return b.(*bytes.Buffer) } // Put returns a buffer into the pool. diff --git a/internal/bufpool/buf_test.go b/internal/bufpool/buf_test.go deleted file mode 100644 index 42a2fea7..00000000 --- a/internal/bufpool/buf_test.go +++ /dev/null @@ -1,46 +0,0 @@ -package bufpool - -import ( - "strconv" - "sync" - "testing" -) - -func BenchmarkSyncPool(b *testing.B) { - sizes := []int{ - 2, - 16, - 32, - 64, - 128, - 256, - 512, - 4096, - 16384, - } - for _, size := range sizes { - b.Run(strconv.Itoa(size), func(b *testing.B) { - b.Run("allocate", func(b *testing.B) { - b.ReportAllocs() - for i := 0; i < b.N; i++ { - buf := make([]byte, size) - _ = buf - } - }) - b.Run("pool", func(b *testing.B) { - b.ReportAllocs() - - p := sync.Pool{} - - for i := 0; i < b.N; i++ { - buf := p.Get() - if buf == nil { - buf = make([]byte, size) - } - - p.Put(buf) - } - }) - }) - } -} diff --git a/internal/errd/errd.go b/internal/errd/errd.go deleted file mode 100644 index 51b7b4f6..00000000 --- a/internal/errd/errd.go +++ /dev/null @@ -1,11 +0,0 @@ -package errd - -import ( - "fmt" -) - -func Wrap(err *error, f string, v ...interface{}) { - if *err != nil { - *err = fmt.Errorf(f+ ": %w", append(v, *err)...) - } -} diff --git a/internal/errd/wrap.go b/internal/errd/wrap.go new file mode 100644 index 00000000..849335c9 --- /dev/null +++ b/internal/errd/wrap.go @@ -0,0 +1,14 @@ +package errd + +import ( + "fmt" +) + +// Wrap wraps err with fmt.Errorf if err is non nil. +// Intended for use with defer and a named error return. +// Inspired by https://github.com/golang/go/issues/32676. +func Wrap(err *error, f string, v ...interface{}) { + if *err != nil { + *err = fmt.Errorf(f+ ": %w", append(v, *err)...) + } +} diff --git a/read.go b/read.go index 97096f74..1f5a88ad 100644 --- a/read.go +++ b/read.go @@ -7,7 +7,6 @@ import ( "fmt" "io" "io/ioutil" - "log" "nhooyr.io/websocket/internal/errd" "strings" "sync/atomic" @@ -390,8 +389,6 @@ func (mr *msgReader) Read(p []byte) (_ int, err error) { } func (mr *msgReader) read(p []byte) (int, error) { - log.Println("compress", mr.deflate) - if mr.payloadLength == 0 { h, err := mr.cr.loop(mr.ctx) if err != nil { diff --git a/write.go b/write.go index 5bb489b4..e1ea007e 100644 --- a/write.go +++ b/write.go @@ -9,7 +9,6 @@ import ( "errors" "fmt" "io" - "nhooyr.io/websocket/internal/errd" "time" ) diff --git a/ws_js.go b/ws_js.go index 10ce0da8..882535b1 100644 --- a/ws_js.go +++ b/ws_js.go @@ -15,7 +15,7 @@ import ( "sync" "syscall/js" - "nhooyr.io/websocket/internal/bufpool" + "nhooyr.io/websocket/internal/bpool" "nhooyr.io/websocket/internal/wsjs" ) @@ -302,7 +302,7 @@ func (c *Conn) Writer(ctx context.Context, typ MessageType) (io.WriteCloser, err c: c, ctx: ctx, typ: typ, - b: bufpool.Get(), + b: bpool.Get(), }, nil } @@ -332,7 +332,7 @@ func (w writer) Close() error { return errors.New("cannot close closed writer") } w.closed = true - defer bufpool.Put(w.b) + defer bpool.Put(w.b) err := w.c.Write(w.ctx, w.typ, w.b.Bytes()) if err != nil { diff --git a/wsjson/wsjson.go b/wsjson/wsjson.go index e8188051..36dd2dfd 100644 --- a/wsjson/wsjson.go +++ b/wsjson/wsjson.go @@ -1,38 +1,36 @@ -// Package wsjson provides websocket helpers for JSON messages. +// Package wsjson provides helpers for reading and writing JSON messages. package wsjson // import "nhooyr.io/websocket/wsjson" import ( "context" "encoding/json" "fmt" - "log" "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/bufpool" + "nhooyr.io/websocket/internal/bpool" + "nhooyr.io/websocket/internal/errd" ) -// Read reads a json message from c into v. -// It will reuse buffers to avoid allocations. +// Read reads a JSON message from c into v. +// It will reuse buffers in between calls to avoid allocations. func Read(ctx context.Context, c *websocket.Conn, v interface{}) error { - err := read(ctx, c, v) - if err != nil { - return fmt.Errorf("failed to read json: %w", err) - } - return nil + return read(ctx, c, v) } -func read(ctx context.Context, c *websocket.Conn, v interface{}) error { +func read(ctx context.Context, c *websocket.Conn, v interface{}) (err error) { + defer errd.Wrap(&err, "failed to read JSON message") + typ, r, err := c.Reader(ctx) if err != nil { return err } if typ != websocket.MessageText { - c.Close(websocket.StatusUnsupportedData, "can only accept text messages") - return fmt.Errorf("unexpected frame type for json (expected %v): %v", websocket.MessageText, typ) + c.Close(websocket.StatusUnsupportedData, "expected text message") + return fmt.Errorf("expected text message for JSON but got: %v", typ) } - b := bufpool.Get() - defer bufpool.Put(b) + b := bpool.Get() + defer bpool.Put(b) _, err = b.ReadFrom(r) if err != nil { @@ -42,40 +40,32 @@ func read(ctx context.Context, c *websocket.Conn, v interface{}) error { err = json.Unmarshal(b.Bytes(), v) if err != nil { c.Close(websocket.StatusInvalidFramePayloadData, "failed to unmarshal JSON") - log.Printf("%X", b.Bytes()) - return fmt.Errorf("failed to unmarshal json: %w", err) + return fmt.Errorf("failed to unmarshal JSON: %w", err) } return nil } -// Write writes the json message v to c. -// It will reuse buffers to avoid allocations. +// Write writes the JSON message v to c. +// It will reuse buffers in between calls to avoid allocations. func Write(ctx context.Context, c *websocket.Conn, v interface{}) error { - err := write(ctx, c, v) - if err != nil { - return fmt.Errorf("failed to write json: %w", err) - } - return nil + return write(ctx, c, v) } -func write(ctx context.Context, c *websocket.Conn, v interface{}) error { +func write(ctx context.Context, c *websocket.Conn, v interface{}) (err error) { + defer errd.Wrap(&err, "failed to write JSON message") + w, err := c.Writer(ctx, websocket.MessageText) if err != nil { return err } - // We use Encode because it automatically enables buffer reuse without us - // needing to do anything. Though see https://github.com/golang/go/issues/27735 - e := json.NewEncoder(w) - err = e.Encode(v) + // json.Marshal cannot reuse buffers between calls as it has to return + // a copy of the byte slice but Encoder does as it directly writes to w. + err = json.NewEncoder(w).Encode(v) if err != nil { - return fmt.Errorf("failed to encode json: %w", err) + return fmt.Errorf("failed to marshal JSON: %w", err) } - err = w.Close() - if err != nil { - return err - } - return nil + return w.Close() } diff --git a/wspb/wspb.go b/wspb/wspb.go index 52ddcd57..f4b7c1c5 100644 --- a/wspb/wspb.go +++ b/wspb/wspb.go @@ -1,40 +1,39 @@ -// Package wspb provides websocket helpers for protobuf messages. +// Package wspb provides helpers for reading and writing protobuf messages. package wspb // import "nhooyr.io/websocket/wspb" import ( "bytes" "context" "fmt" + "nhooyr.io/websocket/internal/errd" "github.com/golang/protobuf/proto" "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/bufpool" + "nhooyr.io/websocket/internal/bpool" ) -// Read reads a protobuf message from c into v. -// It will reuse buffers to avoid allocations. +// Read reads a Protobuf message from c into v. +// It will reuse buffers in between calls to avoid allocations. func Read(ctx context.Context, c *websocket.Conn, v proto.Message) error { - err := read(ctx, c, v) - if err != nil { - return fmt.Errorf("failed to read protobuf: %w", err) - } - return nil + return read(ctx, c, v) } -func read(ctx context.Context, c *websocket.Conn, v proto.Message) error { +func read(ctx context.Context, c *websocket.Conn, v proto.Message) (err error) { + defer errd.Wrap(&err, "failed to read Protobuf message") + typ, r, err := c.Reader(ctx) if err != nil { return err } if typ != websocket.MessageBinary { - c.Close(websocket.StatusUnsupportedData, "can only accept binary messages") - return fmt.Errorf("unexpected frame type for protobuf (expected %v): %v", websocket.MessageBinary, typ) + c.Close(websocket.StatusUnsupportedData, "expected binary message") + return fmt.Errorf("expected binary message for Protobuf but got: %v", typ) } - b := bufpool.Get() - defer bufpool.Put(b) + b := bpool.Get() + defer bpool.Put(b) _, err = b.ReadFrom(r) if err != nil { @@ -43,33 +42,31 @@ func read(ctx context.Context, c *websocket.Conn, v proto.Message) error { err = proto.Unmarshal(b.Bytes(), v) if err != nil { - c.Close(websocket.StatusInvalidFramePayloadData, "failed to unmarshal protobuf") - return fmt.Errorf("failed to unmarshal protobuf: %w", err) + c.Close(websocket.StatusInvalidFramePayloadData, "failed to unmarshal Protobuf") + return fmt.Errorf("failed to unmarshal Protobuf: %w", err) } return nil } -// Write writes the protobuf message v to c. -// It will reuse buffers to avoid allocations. +// Write writes the Protobuf message v to c. +// It will reuse buffers in between calls to avoid allocations. func Write(ctx context.Context, c *websocket.Conn, v proto.Message) error { - err := write(ctx, c, v) - if err != nil { - return fmt.Errorf("failed to write protobuf: %w", err) - } - return nil + return write(ctx, c, v) } -func write(ctx context.Context, c *websocket.Conn, v proto.Message) error { - b := bufpool.Get() +func write(ctx context.Context, c *websocket.Conn, v proto.Message) (err error) { + defer errd.Wrap(&err, "failed to write Protobuf message") + + b := bpool.Get() pb := proto.NewBuffer(b.Bytes()) defer func() { - bufpool.Put(bytes.NewBuffer(pb.Bytes())) + bpool.Put(bytes.NewBuffer(pb.Bytes())) }() - err := pb.Marshal(v) + err = pb.Marshal(v) if err != nil { - return fmt.Errorf("failed to marshal protobuf: %w", err) + return fmt.Errorf("failed to marshal Protobuf: %w", err) } return c.Write(ctx, websocket.MessageBinary, pb.Bytes()) From 6b782a3359d2055dba2a975c828047e6d36cdad4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 28 Nov 2019 16:00:02 -0500 Subject: [PATCH 223/519] Run make fmt --- autobahn_test.go | 3 +- ci/fmt.mk | 2 +- close.go | 3 +- close_test.go | 6 ++- conn.go | 2 +- conn_test.go | 3 +- frame.go | 2 + frame_test.go | 5 ++- internal/assert/cmp.go | 3 +- internal/errd/wrap.go | 2 +- netconn.go | 1 - read.go | 3 +- websocket_stringer.go | 91 ++++++++++++++++++++++++++++++++++++++++++ write.go | 2 + ws_js.go | 2 +- ws_js_test.go | 10 +++++ wsjson/wsjson.go | 1 + wspb/wspb.go | 2 +- 18 files changed, 127 insertions(+), 16 deletions(-) create mode 100644 websocket_stringer.go diff --git a/autobahn_test.go b/autobahn_test.go index 27f8a1b4..21a30b4f 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -8,13 +8,14 @@ import ( "net" "net/http" "net/http/httptest" - "nhooyr.io/websocket" "os" "os/exec" "strconv" "strings" "testing" "time" + + "nhooyr.io/websocket" ) func TestAutobahn(t *testing.T) { diff --git a/ci/fmt.mk b/ci/fmt.mk index 3637c1ac..f82d74dd 100644 --- a/ci/fmt.mk +++ b/ci/fmt.mk @@ -22,4 +22,4 @@ prettier: prettier --write --print-width=120 --no-semi --trailing-comma=all --loglevel=warn $$(git ls-files "*.yml" "*.md") gen: - stringer -type=Opcode,MessageType,StatusCode -output=websocket_stringer.go + stringer -type=opcode,MessageType,StatusCode -output=websocket_stringer.go diff --git a/close.go b/close.go index 57d69a37..432019c6 100644 --- a/close.go +++ b/close.go @@ -6,8 +6,9 @@ import ( "errors" "fmt" "log" - "nhooyr.io/websocket/internal/bpool" "time" + + "nhooyr.io/websocket/internal/bpool" ) // StatusCode represents a WebSocket status code. diff --git a/close_test.go b/close_test.go index ca51a298..c2d11bb8 100644 --- a/close_test.go +++ b/close_test.go @@ -1,12 +1,14 @@ package websocket import ( - "github.com/google/go-cmp/cmp" "io" "math" - "nhooyr.io/websocket/internal/assert" "strings" "testing" + + "github.com/google/go-cmp/cmp" + + "nhooyr.io/websocket/internal/assert" ) func TestCloseError(t *testing.T) { diff --git a/conn.go b/conn.go index e3f24171..5c041b8d 100644 --- a/conn.go +++ b/conn.go @@ -194,7 +194,7 @@ func (c *Conn) ping(ctx context.Context, p string) error { type mu struct { once sync.Once - ch chan struct{} + ch chan struct{} } func (m *mu) init() { diff --git a/conn_test.go b/conn_test.go index 1014dbf3..6b8a778b 100644 --- a/conn_test.go +++ b/conn_test.go @@ -8,13 +8,13 @@ import ( "io" "net/http" "net/http/httptest" - "nhooyr.io/websocket/internal/assert" "strings" "sync/atomic" "testing" "time" "nhooyr.io/websocket" + "nhooyr.io/websocket/internal/assert" ) func TestConn(t *testing.T) { @@ -51,7 +51,6 @@ func TestConn(t *testing.T) { }) } - func testServer(tb testing.TB, fn func(w http.ResponseWriter, r *http.Request), tls bool) (s *httptest.Server, closeFn func()) { h := http.HandlerFunc(fn) if tls { diff --git a/frame.go b/frame.go index f36334c2..e55c8f2c 100644 --- a/frame.go +++ b/frame.go @@ -5,6 +5,8 @@ import ( "encoding/binary" "math" "math/bits" + + "nhooyr.io/websocket/internal/errd" ) // opcode represents a WebSocket opcode. diff --git a/frame_test.go b/frame_test.go index a4a1f5a8..fa231c57 100644 --- a/frame_test.go +++ b/frame_test.go @@ -7,7 +7,7 @@ import ( "bytes" "encoding/binary" "math/bits" - "nhooyr.io/websocket/internal/assert" + "math/rand" "strconv" "testing" "time" @@ -15,7 +15,8 @@ import ( "github.com/gobwas/ws" _ "github.com/gorilla/websocket" - "math/rand" + + "nhooyr.io/websocket/internal/assert" ) func init() { diff --git a/internal/assert/cmp.go b/internal/assert/cmp.go index 0edcf2cd..39be1f4a 100644 --- a/internal/assert/cmp.go +++ b/internal/assert/cmp.go @@ -1,8 +1,9 @@ package assert import ( - "github.com/google/go-cmp/cmp" "reflect" + + "github.com/google/go-cmp/cmp" ) // https://github.com/google/go-cmp/issues/40#issuecomment-328615283 diff --git a/internal/errd/wrap.go b/internal/errd/wrap.go index 849335c9..6e779131 100644 --- a/internal/errd/wrap.go +++ b/internal/errd/wrap.go @@ -9,6 +9,6 @@ import ( // Inspired by https://github.com/golang/go/issues/32676. func Wrap(err *error, f string, v ...interface{}) { if *err != nil { - *err = fmt.Errorf(f+ ": %w", append(v, *err)...) + *err = fmt.Errorf(f+": %w", append(v, *err)...) } } diff --git a/netconn.go b/netconn.go index 74a2c7c1..64aadf0b 100644 --- a/netconn.go +++ b/netconn.go @@ -164,4 +164,3 @@ func (c *netConn) SetReadDeadline(t time.Time) error { } return nil } - diff --git a/read.go b/read.go index 1f5a88ad..13c8d703 100644 --- a/read.go +++ b/read.go @@ -7,10 +7,11 @@ import ( "fmt" "io" "io/ioutil" - "nhooyr.io/websocket/internal/errd" "strings" "sync/atomic" "time" + + "nhooyr.io/websocket/internal/errd" ) // Reader waits until there is a WebSocket data message to read diff --git a/websocket_stringer.go b/websocket_stringer.go new file mode 100644 index 00000000..571e505f --- /dev/null +++ b/websocket_stringer.go @@ -0,0 +1,91 @@ +// Code generated by "stringer -type=opcode,MessageType,StatusCode -output=websocket_stringer.go"; DO NOT EDIT. + +package websocket + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[opContinuation-0] + _ = x[opText-1] + _ = x[opBinary-2] + _ = x[opClose-8] + _ = x[opPing-9] + _ = x[opPong-10] +} + +const ( + _opcode_name_0 = "opContinuationopTextopBinary" + _opcode_name_1 = "opCloseopPingopPong" +) + +var ( + _opcode_index_0 = [...]uint8{0, 14, 20, 28} + _opcode_index_1 = [...]uint8{0, 7, 13, 19} +) + +func (i opcode) String() string { + switch { + case 0 <= i && i <= 2: + return _opcode_name_0[_opcode_index_0[i]:_opcode_index_0[i+1]] + case 8 <= i && i <= 10: + i -= 8 + return _opcode_name_1[_opcode_index_1[i]:_opcode_index_1[i+1]] + default: + return "opcode(" + strconv.FormatInt(int64(i), 10) + ")" + } +} +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[MessageText-1] + _ = x[MessageBinary-2] +} + +const _MessageType_name = "MessageTextMessageBinary" + +var _MessageType_index = [...]uint8{0, 11, 24} + +func (i MessageType) String() string { + i -= 1 + if i < 0 || i >= MessageType(len(_MessageType_index)-1) { + return "MessageType(" + strconv.FormatInt(int64(i+1), 10) + ")" + } + return _MessageType_name[_MessageType_index[i]:_MessageType_index[i+1]] +} +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[StatusNormalClosure-1000] + _ = x[StatusGoingAway-1001] + _ = x[StatusProtocolError-1002] + _ = x[StatusUnsupportedData-1003] + _ = x[statusReserved-1004] + _ = x[StatusNoStatusRcvd-1005] + _ = x[StatusAbnormalClosure-1006] + _ = x[StatusInvalidFramePayloadData-1007] + _ = x[StatusPolicyViolation-1008] + _ = x[StatusMessageTooBig-1009] + _ = x[StatusMandatoryExtension-1010] + _ = x[StatusInternalError-1011] + _ = x[StatusServiceRestart-1012] + _ = x[StatusTryAgainLater-1013] + _ = x[StatusBadGateway-1014] + _ = x[StatusTLSHandshake-1015] +} + +const _StatusCode_name = "StatusNormalClosureStatusGoingAwayStatusProtocolErrorStatusUnsupportedDatastatusReservedStatusNoStatusRcvdStatusAbnormalClosureStatusInvalidFramePayloadDataStatusPolicyViolationStatusMessageTooBigStatusMandatoryExtensionStatusInternalErrorStatusServiceRestartStatusTryAgainLaterStatusBadGatewayStatusTLSHandshake" + +var _StatusCode_index = [...]uint16{0, 19, 34, 53, 74, 88, 106, 127, 156, 177, 196, 220, 239, 259, 278, 294, 312} + +func (i StatusCode) String() string { + i -= 1000 + if i < 0 || i >= StatusCode(len(_StatusCode_index)-1) { + return "StatusCode(" + strconv.FormatInt(int64(i+1000), 10) + ")" + } + return _StatusCode_name[_StatusCode_index[i]:_StatusCode_index[i+1]] +} diff --git a/write.go b/write.go index e1ea007e..9cafc5c5 100644 --- a/write.go +++ b/write.go @@ -10,6 +10,8 @@ import ( "fmt" "io" "time" + + "nhooyr.io/websocket/internal/errd" ) // Writer returns a writer bounded by the context that will write diff --git a/ws_js.go b/ws_js.go index 882535b1..2e654feb 100644 --- a/ws_js.go +++ b/ws_js.go @@ -9,7 +9,6 @@ import ( "fmt" "io" "net/http" - "nhooyr.io/websocket/internal/wssync" "reflect" "runtime" "sync" @@ -17,6 +16,7 @@ import ( "nhooyr.io/websocket/internal/bpool" "nhooyr.io/websocket/internal/wsjs" + "nhooyr.io/websocket/internal/wssync" ) // Conn provides a wrapper around the browser WebSocket API. diff --git a/ws_js_test.go b/ws_js_test.go index abd950c7..9330b411 100644 --- a/ws_js_test.go +++ b/ws_js_test.go @@ -1,5 +1,15 @@ package websocket +import ( + "context" + "net/http" + "os" + "testing" + "time" + + "nhooyr.io/websocket" +) + func TestEcho(t *testing.T) { t.Parallel() diff --git a/wsjson/wsjson.go b/wsjson/wsjson.go index 36dd2dfd..99996a69 100644 --- a/wsjson/wsjson.go +++ b/wsjson/wsjson.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "nhooyr.io/websocket" "nhooyr.io/websocket/internal/bpool" "nhooyr.io/websocket/internal/errd" diff --git a/wspb/wspb.go b/wspb/wspb.go index f4b7c1c5..666c6fa5 100644 --- a/wspb/wspb.go +++ b/wspb/wspb.go @@ -5,12 +5,12 @@ import ( "bytes" "context" "fmt" - "nhooyr.io/websocket/internal/errd" "github.com/golang/protobuf/proto" "nhooyr.io/websocket" "nhooyr.io/websocket/internal/bpool" + "nhooyr.io/websocket/internal/errd" ) // Read reads a Protobuf message from c into v. From 989ba2f7ae8912e0bf586be4a9e1bd2c6b7b3fcc Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 28 Nov 2019 16:37:18 -0500 Subject: [PATCH 224/519] Change websocket to WebSocket in docs/errors --- README.md | 2 +- accept.go | 126 ++++++++++----------------- accept_test.go | 3 +- ci/fmt.mk | 2 +- ci/test.mk | 2 +- close.go | 4 +- dial.go | 10 +-- example_echo_test.go | 2 +- internal/wsjs/wsjs_js.go | 4 +- websocket_stringer.go => stringer.go | 2 +- ws_js.go | 10 +-- 11 files changed, 68 insertions(+), 99 deletions(-) rename websocket_stringer.go => stringer.go (98%) diff --git a/README.md b/README.md index 8ac418a7..c927e8c1 100644 --- a/README.md +++ b/README.md @@ -170,4 +170,4 @@ See the gorilla/websocket comparison for more performance details. If your company or project is using this library, feel free to open an issue or PR to amend this list. - [Coder](https://github.com/cdr) -- [Tatsu Works](https://github.com/tatsuworks) - Ingresses 20 TB in websocket data every month on their Discord bot. +- [Tatsu Works](https://github.com/tatsuworks) - Ingresses 20 TB in WebSocket data every month on their Discord bot. diff --git a/accept.go b/accept.go index 2028d4b2..dbfb2c30 100644 --- a/accept.go +++ b/accept.go @@ -10,68 +10,56 @@ import ( "net/http" "net/textproto" "net/url" + "nhooyr.io/websocket/internal/errd" "strings" ) // AcceptOptions represents the options available to pass to Accept. type AcceptOptions struct { - // Subprotocols lists the websocket subprotocols that Accept will negotiate with a client. + // Subprotocols lists the WebSocket subprotocols that Accept will negotiate with the client. // The empty subprotocol will always be negotiated as per RFC 6455. If you would like to - // reject it, close the connection if c.Subprotocol() == "". + // reject it, close the connection when c.Subprotocol() == "". Subprotocols []string - // InsecureSkipVerify disables Accept's origin verification - // behaviour. By default Accept only allows the handshake to - // succeed if the javascript that is initiating the handshake - // is on the same domain as the server. This is to prevent CSRF - // attacks when secure data is stored in a cookie as there is no same - // origin policy for WebSockets. In other words, javascript from - // any domain can perform a WebSocket dial on an arbitrary server. - // This dial will include cookies which means the arbitrary javascript - // can perform actions as the authenticated user. + // InsecureSkipVerify disables Accept's origin verification behaviour. By default, + // the connection will only be accepted if the request origin is equal to the request + // host. + // + // This is only required if you want javascript served from a different domain + // to access your WebSocket server. // // See https://stackoverflow.com/a/37837709/4283659 // - // The only time you need this is if your javascript is running on a different domain - // than your WebSocket server. - // Think carefully about whether you really need this option before you use it. - // If you do, remember that if you store secure data in cookies, you wil need to verify the - // Origin header yourself otherwise you are exposing yourself to a CSRF attack. + // Please ensure you understand the ramifications of enabling this. + // If used incorrectly your WebSocket server will be open to CSRF attacks. InsecureSkipVerify bool // CompressionMode sets the compression mode. - // See docs on the CompressionMode type and defined constants. + // See docs on the CompressionMode type. CompressionMode CompressionMode } -// Accept accepts a WebSocket HTTP handshake from a client and upgrades the +// Accept accepts a WebSocket handshake from a client and upgrades the // the connection to a WebSocket. // -// Accept will reject the handshake if the Origin domain is not the same as the Host unless -// the InsecureSkipVerify option is set. In other words, by default it does not allow -// cross origin requests. +// Accept will not allow cross origin requests by default. +// See the InsecureSkipVerify option to allow cross origin requests. // -// If an error occurs, Accept will write a response with a safe error message to w. +// Accept will write a response to w on all errors. func Accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, error) { - c, err := accept(w, r, opts) - if err != nil { - return nil, fmt.Errorf("failed to accept websocket connection: %w", err) - } - return c, nil + return accept(w, r, opts) } -func (opts *AcceptOptions) ensure() *AcceptOptions { +func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Conn, err error) { + defer errd.Wrap(&err, "failed to accept WebSocket connection") + if opts == nil { - return &AcceptOptions{} + opts = &AcceptOptions{} } - return opts -} - -func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, error) { - opts = opts.ensure() - err := verifyClientRequest(w, r) + err = verifyClientRequest(r) if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) return nil, err } @@ -85,7 +73,7 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, hj, ok := w.(http.Hijacker) if !ok { - err = errors.New("passed ResponseWriter does not implement http.Hijacker") + err = errors.New("http.ResponseWriter does not implement http.Hijacker") http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented) return nil, err } @@ -93,7 +81,8 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, w.Header().Set("Upgrade", "websocket") w.Header().Set("Connection", "Upgrade") - handleSecWebSocketKey(w, r) + key := r.Header.Get("Sec-WebSocket-Key") + w.Header().Set("Sec-WebSocket-Accept", secWebSocketAccept(key)) subproto := selectSubprotocol(r, opts.Subprotocols) if subproto != "" { @@ -102,7 +91,6 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, copts, err := acceptCompression(r, w, opts.CompressionMode) if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) return nil, err } @@ -129,41 +117,29 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, }), nil } -func verifyClientRequest(w http.ResponseWriter, r *http.Request) error { +func verifyClientRequest(r *http.Request) error { if !r.ProtoAtLeast(1, 1) { - err := fmt.Errorf("websocket protocol violation: handshake request must be at least HTTP/1.1: %q", r.Proto) - http.Error(w, err.Error(), http.StatusBadRequest) - return err + return fmt.Errorf("WebSocket protocol violation: handshake request must be at least HTTP/1.1: %q", r.Proto) } if !headerContainsToken(r.Header, "Connection", "Upgrade") { - err := fmt.Errorf("websocket protocol violation: Connection header %q does not contain Upgrade", r.Header.Get("Connection")) - http.Error(w, err.Error(), http.StatusBadRequest) - return err + return fmt.Errorf("WebSocket protocol violation: Connection header %q does not contain Upgrade", r.Header.Get("Connection")) } - if !headerContainsToken(r.Header, "Upgrade", "WebSocket") { - err := fmt.Errorf("websocket protocol violation: Upgrade header %q does not contain websocket", r.Header.Get("Upgrade")) - http.Error(w, err.Error(), http.StatusBadRequest) - return err + if !headerContainsToken(r.Header, "Upgrade", "websocket") { + return fmt.Errorf("WebSocket protocol violation: Upgrade header %q does not contain websocket", r.Header.Get("Upgrade")) } if r.Method != "GET" { - err := fmt.Errorf("websocket protocol violation: handshake request method is not GET but %q", r.Method) - http.Error(w, err.Error(), http.StatusBadRequest) - return err + return fmt.Errorf("WebSocket protocol violation: handshake request method is not GET but %q", r.Method) } if r.Header.Get("Sec-WebSocket-Version") != "13" { - err := fmt.Errorf("unsupported websocket protocol version (only 13 is supported): %q", r.Header.Get("Sec-WebSocket-Version")) - http.Error(w, err.Error(), http.StatusBadRequest) - return err + return fmt.Errorf("unsupported WebSocket protocol version (only 13 is supported): %q", r.Header.Get("Sec-WebSocket-Version")) } if r.Header.Get("Sec-WebSocket-Key") == "" { - err := errors.New("websocket protocol violation: missing Sec-WebSocket-Key") - http.Error(w, err.Error(), http.StatusBadRequest) - return err + return errors.New("WebSocket protocol violation: missing Sec-WebSocket-Key") } return nil @@ -171,30 +147,20 @@ func verifyClientRequest(w http.ResponseWriter, r *http.Request) error { func authenticateOrigin(r *http.Request) error { origin := r.Header.Get("Origin") - if origin == "" { - return nil - } - u, err := url.Parse(origin) - if err != nil { - return fmt.Errorf("failed to parse Origin header %q: %w", origin, err) - } - if !strings.EqualFold(u.Host, r.Host) { - return fmt.Errorf("request Origin %q is not authorized for Host %q", origin, r.Host) + if origin != "" { + u, err := url.Parse(origin) + if err != nil { + return fmt.Errorf("failed to parse Origin header %q: %w", origin, err) + } + if !strings.EqualFold(u.Host, r.Host) { + return fmt.Errorf("request Origin %q is not authorized for Host %q", origin, r.Host) + } } return nil } -func handleSecWebSocketKey(w http.ResponseWriter, r *http.Request) { - key := r.Header.Get("Sec-WebSocket-Key") - w.Header().Set("Sec-WebSocket-Accept", secWebSocketAccept(key)) -} - func selectSubprotocol(r *http.Request, subprotocols []string) string { cps := headerTokens(r.Header, "Sec-WebSocket-Protocol") - if len(cps) == 0 { - return "" - } - for _, sp := range subprotocols { for _, cp := range cps { if strings.EqualFold(sp, cp) { @@ -236,7 +202,9 @@ func acceptDeflate(w http.ResponseWriter, ext websocketExtension, mode Compressi continue } - return nil, fmt.Errorf("unsupported permessage-deflate parameter: %q", p) + err := fmt.Errorf("unsupported permessage-deflate parameter: %q", p) + http.Error(w, err.Error(), http.StatusBadRequest) + return nil, err } copts.setHeader(w.Header()) @@ -264,7 +232,9 @@ func acceptWebkitDeflate(w http.ResponseWriter, ext websocketExtension, mode Com // // Either way, we're only implementing this for webkit which never sends the max_window_bits // parameter so we don't need to worry about it. - return nil, fmt.Errorf("unsupported x-webkit-deflate-frame parameter: %q", p) + err := fmt.Errorf("unsupported x-webkit-deflate-frame parameter: %q", p) + http.Error(w, err.Error(), http.StatusBadRequest) + return nil, err } s := "x-webkit-deflate-frame" diff --git a/accept_test.go b/accept_test.go index 9598cd58..a8ab7d69 100644 --- a/accept_test.go +++ b/accept_test.go @@ -114,7 +114,6 @@ func Test_verifyClientHandshake(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - w := httptest.NewRecorder() r := httptest.NewRequest(tc.method, "/", nil) r.ProtoMajor = 1 @@ -127,7 +126,7 @@ func Test_verifyClientHandshake(t *testing.T) { r.Header.Set(k, v) } - err := verifyClientRequest(w, r) + err := verifyClientRequest(r) if (err == nil) != tc.success { t.Fatalf("unexpected error value: %+v", err) } diff --git a/ci/fmt.mk b/ci/fmt.mk index f82d74dd..f3969721 100644 --- a/ci/fmt.mk +++ b/ci/fmt.mk @@ -22,4 +22,4 @@ prettier: prettier --write --print-width=120 --no-semi --trailing-comma=all --loglevel=warn $$(git ls-files "*.yml" "*.md") gen: - stringer -type=opcode,MessageType,StatusCode -output=websocket_stringer.go + stringer -type=opcode,MessageType,StatusCode -output=stringer.go diff --git a/ci/test.mk b/ci/test.mk index 9e4e0803..f9a6e09a 100644 --- a/ci/test.mk +++ b/ci/test.mk @@ -19,5 +19,5 @@ coveralls: gotest gotest: go test -covermode=count -coverprofile=ci/out/coverage.prof -coverpkg=./... $${GOTESTFLAGS-} ./... - sed -i '/_stringer\.go/d' ci/out/coverage.prof + sed -i '/stringer\.go/d' ci/out/coverage.prof sed -i '/assert/d' ci/out/coverage.prof diff --git a/close.go b/close.go index 432019c6..6bb48bd5 100644 --- a/close.go +++ b/close.go @@ -97,7 +97,7 @@ func CloseStatus(err error) StatusCode { func (c *Conn) Close(code StatusCode, reason string) error { err := c.closeHandshake(code, reason) if err != nil { - return fmt.Errorf("failed to close websocket: %w", err) + return fmt.Errorf("failed to close WebSocket: %w", err) } return nil } @@ -236,7 +236,7 @@ func (c *Conn) setCloseErr(err error) { func (c *Conn) setCloseErrNoLock(err error) { if c.closeErr == nil { - c.closeErr = fmt.Errorf("websocket closed: %w", err) + c.closeErr = fmt.Errorf("WebSocket closed: %w", err) } } diff --git a/dial.go b/dial.go index 8fa0f7ab..3a2165ab 100644 --- a/dial.go +++ b/dial.go @@ -47,7 +47,7 @@ type DialOptions struct { func Dial(ctx context.Context, u string, opts *DialOptions) (*Conn, *http.Response, error) { c, r, err := dial(ctx, u, opts) if err != nil { - return nil, r, fmt.Errorf("failed to websocket dial: %w", err) + return nil, r, fmt.Errorf("failed to WebSocket dial: %w", err) } return c, r, nil } @@ -158,22 +158,22 @@ func verifyServerResponse(r *http.Request, resp *http.Response) (*compressionOpt } if !headerContainsToken(resp.Header, "Connection", "Upgrade") { - return nil, fmt.Errorf("websocket protocol violation: Connection header %q does not contain Upgrade", resp.Header.Get("Connection")) + return nil, fmt.Errorf("WebSocket protocol violation: Connection header %q does not contain Upgrade", resp.Header.Get("Connection")) } if !headerContainsToken(resp.Header, "Upgrade", "WebSocket") { - return nil, fmt.Errorf("websocket protocol violation: Upgrade header %q does not contain websocket", resp.Header.Get("Upgrade")) + return nil, fmt.Errorf("WebSocket protocol violation: Upgrade header %q does not contain websocket", resp.Header.Get("Upgrade")) } if resp.Header.Get("Sec-WebSocket-Accept") != secWebSocketAccept(r.Header.Get("Sec-WebSocket-Key")) { - return nil, fmt.Errorf("websocket protocol violation: invalid Sec-WebSocket-Accept %q, key %q", + return nil, fmt.Errorf("WebSocket protocol violation: invalid Sec-WebSocket-Accept %q, key %q", resp.Header.Get("Sec-WebSocket-Accept"), r.Header.Get("Sec-WebSocket-Key"), ) } if proto := resp.Header.Get("Sec-WebSocket-Protocol"); proto != "" && !headerContainsToken(r.Header, "Sec-WebSocket-Protocol", proto) { - return nil, fmt.Errorf("websocket protocol violation: unexpected Sec-WebSocket-Protocol from server: %q", proto) + return nil, fmt.Errorf("WebSocket protocol violation: unexpected Sec-WebSocket-Protocol from server: %q", proto) } copts, err := verifyServerExtensions(resp.Header) diff --git a/example_echo_test.go b/example_echo_test.go index 16d003d9..cd195d2e 100644 --- a/example_echo_test.go +++ b/example_echo_test.go @@ -93,7 +93,7 @@ func echoServer(w http.ResponseWriter, r *http.Request) error { } } -// echo reads from the websocket connection and then writes +// echo reads from the WebSocket connection and then writes // the received message back to it. // The entire function has 10s to complete. func echo(ctx context.Context, c *websocket.Conn, l *rate.Limiter) error { diff --git a/internal/wsjs/wsjs_js.go b/internal/wsjs/wsjs_js.go index d48691d4..26ffb456 100644 --- a/internal/wsjs/wsjs_js.go +++ b/internal/wsjs/wsjs_js.go @@ -102,7 +102,7 @@ type MessageEvent struct { // See https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent } -// OnMessage registers a function to be called when the websocket receives a message. +// OnMessage registers a function to be called when the WebSocket receives a message. func (c WebSocket) OnMessage(fn func(m MessageEvent)) (remove func()) { return c.addEventListener("message", func(e js.Value) { var data interface{} @@ -128,7 +128,7 @@ func (c WebSocket) Subprotocol() string { return c.v.Get("protocol").String() } -// OnOpen registers a function to be called when the websocket is opened. +// OnOpen registers a function to be called when the WebSocket is opened. func (c WebSocket) OnOpen(fn func(e js.Value)) (remove func()) { return c.addEventListener("open", fn) } diff --git a/websocket_stringer.go b/stringer.go similarity index 98% rename from websocket_stringer.go rename to stringer.go index 571e505f..5a66ba29 100644 --- a/websocket_stringer.go +++ b/stringer.go @@ -1,4 +1,4 @@ -// Code generated by "stringer -type=opcode,MessageType,StatusCode -output=websocket_stringer.go"; DO NOT EDIT. +// Code generated by "stringer -type=opcode,MessageType,StatusCode -output=stringer.go"; DO NOT EDIT. package websocket diff --git a/ws_js.go b/ws_js.go index 2e654feb..7f10ee17 100644 --- a/ws_js.go +++ b/ws_js.go @@ -105,7 +105,7 @@ func (c *Conn) closeWithInternal() { // The maximum time spent waiting is bounded by the context. func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { if c.isReadClosed.Load() == 1 { - return 0, nil, errors.New("websocket connection read closed") + return 0, nil, errors.New("WebSocket connection read closed") } typ, p, err := c.read(ctx) @@ -188,14 +188,14 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) error { } } -// Close closes the websocket with the given code and reason. +// Close closes the WebSocket with the given code and reason. // It will wait until the peer responds with a close frame // or the connection is closed. // It thus performs the full WebSocket close handshake. func (c *Conn) Close(code StatusCode, reason string) error { err := c.exportedClose(code, reason) if err != nil { - return fmt.Errorf("failed to close websocket: %w", err) + return fmt.Errorf("failed to close WebSocket: %w", err) } return nil } @@ -245,7 +245,7 @@ type DialOptions struct { func Dial(ctx context.Context, url string, opts *DialOptions) (*Conn, *http.Response, error) { c, resp, err := dial(ctx, url, opts) if err != nil { - return nil, resp, fmt.Errorf("failed to websocket dial %q: %w", url, err) + return nil, resp, fmt.Errorf("failed to WebSocket dial %q: %w", url, err) } return c, resp, nil } @@ -359,7 +359,7 @@ func (c *Conn) SetReadLimit(n int64) { func (c *Conn) setCloseErr(err error) { c.closeErrOnce.Do(func() { - c.closeErr = fmt.Errorf("websocket closed: %w", err) + c.closeErr = fmt.Errorf("WebSocket closed: %w", err) }) } From 9f159635813f2022ea18007636be4f89e6043afa Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 28 Nov 2019 17:10:42 -0500 Subject: [PATCH 225/519] Simplify dial.go --- accept.go | 7 +- accept_test.go | 27 ++++--- autobahn_test.go | 21 ++---- dial.go | 182 +++++++++++++++++++++++++---------------------- dial_test.go | 6 +- 5 files changed, 124 insertions(+), 119 deletions(-) diff --git a/accept.go b/accept.go index dbfb2c30..964e0401 100644 --- a/accept.go +++ b/accept.go @@ -10,11 +10,12 @@ import ( "net/http" "net/textproto" "net/url" - "nhooyr.io/websocket/internal/errd" "strings" + + "nhooyr.io/websocket/internal/errd" ) -// AcceptOptions represents the options available to pass to Accept. +// AcceptOptions represents Accept's options. type AcceptOptions struct { // Subprotocols lists the WebSocket subprotocols that Accept will negotiate with the client. // The empty subprotocol will always be negotiated as per RFC 6455. If you would like to @@ -35,7 +36,7 @@ type AcceptOptions struct { InsecureSkipVerify bool // CompressionMode sets the compression mode. - // See docs on the CompressionMode type. + // See the docs on CompressionMode. CompressionMode CompressionMode } diff --git a/accept_test.go b/accept_test.go index a8ab7d69..d68d4d6d 100644 --- a/accept_test.go +++ b/accept_test.go @@ -4,6 +4,8 @@ import ( "net/http/httptest" "strings" "testing" + + "nhooyr.io/websocket/internal/assert" ) func TestAccept(t *testing.T) { @@ -16,10 +18,7 @@ func TestAccept(t *testing.T) { r := httptest.NewRequest("GET", "/", nil) _, err := Accept(w, r, nil) - if err == nil { - t.Fatalf("unexpected error value: %v", err) - } - + assert.ErrorContains(t, err, "protocol violation") }) t.Run("requireHttpHijacker", func(t *testing.T) { @@ -33,9 +32,7 @@ func TestAccept(t *testing.T) { r.Header.Set("Sec-WebSocket-Key", "meow123") _, err := Accept(w, r, nil) - if err == nil || !strings.Contains(err.Error(), "http.Hijacker") { - t.Fatalf("unexpected error value: %v", err) - } + assert.ErrorContains(t, err, "http.ResponseWriter does not implement http.Hijacker") }) } @@ -127,8 +124,10 @@ func Test_verifyClientHandshake(t *testing.T) { } err := verifyClientRequest(r) - if (err == nil) != tc.success { - t.Fatalf("unexpected error value: %+v", err) + if tc.success { + assert.Success(t, err) + } else { + assert.Error(t, err) } }) } @@ -178,9 +177,7 @@ func Test_selectSubprotocol(t *testing.T) { r.Header.Set("Sec-WebSocket-Protocol", strings.Join(tc.clientProtocols, ",")) negotiated := selectSubprotocol(r, tc.serverProtocols) - if tc.negotiated != negotiated { - t.Fatalf("expected %q but got %q", tc.negotiated, negotiated) - } + assert.Equal(t, tc.negotiated, negotiated, "negotiated") }) } } @@ -234,8 +231,10 @@ func Test_authenticateOrigin(t *testing.T) { r.Header.Set("Origin", tc.origin) err := authenticateOrigin(r) - if (err == nil) != tc.success { - t.Fatalf("unexpected error value: %+v", err) + if tc.success { + assert.Success(t, err) + } else { + assert.Error(t, err) } }) } diff --git a/autobahn_test.go b/autobahn_test.go index 21a30b4f..30c96a7c 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -18,21 +18,19 @@ import ( "nhooyr.io/websocket" ) +// https://github.com/crossbario/autobahn-python/tree/master/wstest func TestAutobahn(t *testing.T) { - // This test contains the old autobahn test suite tests that use the - // python binary. The approach is clunky and slow so new tests - // have been written in pure Go in websocket_test.go. - // These have been kept for correctness purposes and are occasionally ran. + t.Parallel() + if os.Getenv("AUTOBAHN") == "" { t.Skip("Set $AUTOBAHN to run tests against the autobahn test suite") } - t.Run("server", testServerAutobahnPython) - t.Run("client", testClientAutobahnPython) + t.Run("server", testServerAutobahn) + t.Run("client", testClientAutobahn) } -// https://github.com/crossbario/autobahn-python/tree/master/wstest -func testServerAutobahnPython(t *testing.T) { +func testServerAutobahn(t *testing.T) { t.Parallel() s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -101,14 +99,9 @@ func unusedListenAddr() (string, error) { return l.Addr().String(), nil } -// https://github.com/crossbario/autobahn-python/blob/master/wstest/testee_client_aio.py -func testClientAutobahnPython(t *testing.T) { +func testClientAutobahn(t *testing.T) { t.Parallel() - if os.Getenv("AUTOBAHN_PYTHON") == "" { - t.Skip("Set $AUTOBAHN_PYTHON to test against the python autobahn test suite") - } - serverAddr, err := unusedListenAddr() if err != nil { t.Fatalf("failed to get unused listen addr for wstest: %v", err) diff --git a/dial.go b/dial.go index 3a2165ab..a1a10556 100644 --- a/dial.go +++ b/dial.go @@ -14,51 +14,50 @@ import ( "net/url" "strings" "sync" + + "nhooyr.io/websocket/internal/errd" ) -// DialOptions represents the options available to pass to Dial. +// DialOptions represents Dial's options. type DialOptions struct { - // HTTPClient is the http client used for the handshake. - // Its Transport must return writable bodies - // for WebSocket handshakes. - // http.Transport does this correctly beginning with Go 1.12. + // HTTPClient is used for the connection. + // Its Transport must return writable bodies for WebSocket handshakes. + // http.Transport does beginning with Go 1.12. HTTPClient *http.Client // HTTPHeader specifies the HTTP headers included in the handshake request. HTTPHeader http.Header - // Subprotocols lists the subprotocols to negotiate with the server. + // Subprotocols lists the WebSocket subprotocols to negotiate with the server. Subprotocols []string - // See docs on CompressionMode. + // CompressionMode sets the compression mode. + // See the docs on CompressionMode. CompressionMode CompressionMode } -// Dial performs a WebSocket handshake on the given url with the given options. +// Dial performs a WebSocket handshake on url. +// // The response is the WebSocket handshake response from the server. -// If an error occurs, the returned response may be non nil. However, you can only -// read the first 1024 bytes of its body. +// You never need to close resp.Body yourself. // -// You never need to close the resp.Body yourself. +// If an error occurs, the returned response may be non nil. +// However, you can only read the first 1024 bytes of the body. // -// This function requires at least Go 1.12 to succeed as it uses a new feature -// in net/http to perform WebSocket handshakes and get a writable body -// from the transport. See https://github.com/golang/go/issues/26937#issuecomment-415855861 +// This function requires at least Go 1.12 as it uses a new feature +// in net/http to perform WebSocket handshakes. +// See docs on the HTTPClient option and https://github.com/golang/go/issues/26937#issuecomment-415855861 func Dial(ctx context.Context, u string, opts *DialOptions) (*Conn, *http.Response, error) { - c, r, err := dial(ctx, u, opts) - if err != nil { - return nil, r, fmt.Errorf("failed to WebSocket dial: %w", err) - } - return c, r, nil + return dial(ctx, u, opts) } -func (opts *DialOptions) ensure() *DialOptions { +func dial(ctx context.Context, urls string, opts *DialOptions) (_ *Conn, _ *http.Response, err error) { + defer errd.Wrap(&err, "failed to WebSocket dial") + if opts == nil { opts = &DialOptions{} - } else { - opts = &*opts } - + opts = &*opts if opts.HTTPClient == nil { opts.HTTPClient = http.DefaultClient } @@ -66,71 +65,35 @@ func (opts *DialOptions) ensure() *DialOptions { opts.HTTPHeader = http.Header{} } - return opts -} - -func dial(ctx context.Context, u string, opts *DialOptions) (_ *Conn, _ *http.Response, err error) { - opts = opts.ensure() - - if opts.HTTPClient.Timeout > 0 { - return nil, nil, errors.New("use context for cancellation instead of http.Client.Timeout; see https://github.com/nhooyr/websocket/issues/67") - } - - parsedURL, err := url.Parse(u) - if err != nil { - return nil, nil, fmt.Errorf("failed to parse url: %w", err) - } - - switch parsedURL.Scheme { - case "ws": - parsedURL.Scheme = "http" - case "wss": - parsedURL.Scheme = "https" - default: - return nil, nil, fmt.Errorf("unexpected url scheme: %q", parsedURL.Scheme) - } - - req, _ := http.NewRequest("GET", parsedURL.String(), nil) - req = req.WithContext(ctx) - req.Header = opts.HTTPHeader - req.Header.Set("Connection", "Upgrade") - req.Header.Set("Upgrade", "websocket") - req.Header.Set("Sec-WebSocket-Version", "13") secWebSocketKey, err := secWebSocketKey() if err != nil { return nil, nil, fmt.Errorf("failed to generate Sec-WebSocket-Key: %w", err) } - req.Header.Set("Sec-WebSocket-Key", secWebSocketKey) - if len(opts.Subprotocols) > 0 { - req.Header.Set("Sec-WebSocket-Protocol", strings.Join(opts.Subprotocols, ",")) - } - if opts.CompressionMode != CompressionDisabled { - copts := opts.CompressionMode.opts() - copts.setHeader(req.Header) - } - resp, err := opts.HTTPClient.Do(req) + resp, err := handshakeRequest(ctx, urls, opts, secWebSocketKey) if err != nil { - return nil, nil, fmt.Errorf("failed to send handshake request: %w", err) + return nil, resp, err } + respBody := resp.Body + resp.Body = nil defer func() { if err != nil { // We read a bit of the body for easier debugging. - r := io.LimitReader(resp.Body, 1024) + r := io.LimitReader(respBody, 1024) b, _ := ioutil.ReadAll(r) - resp.Body.Close() + respBody.Close() resp.Body = ioutil.NopCloser(bytes.NewReader(b)) } }() - copts, err := verifyServerResponse(req, resp) + copts, err := verifyServerResponse(opts, secWebSocketKey, resp) if err != nil { return nil, resp, err } - rwc, ok := resp.Body.(io.ReadWriteCloser) + rwc, ok := respBody.(io.ReadWriteCloser) if !ok { - return nil, resp, fmt.Errorf("response body is not a io.ReadWriteCloser: %T", rwc) + return nil, resp, fmt.Errorf("response body is not a io.ReadWriteCloser: %T", respBody) } return newConn(connConfig{ @@ -143,6 +106,46 @@ func dial(ctx context.Context, u string, opts *DialOptions) (_ *Conn, _ *http.Re }), resp, nil } +func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, secWebSocketKey string) (*http.Response, error) { + if opts.HTTPClient.Timeout > 0 { + return nil, errors.New("use context for cancellation instead of http.Client.Timeout; see https://github.com/nhooyr/websocket/issues/67") + } + + u, err := url.Parse(urls) + if err != nil { + return nil, fmt.Errorf("failed to parse url: %w", err) + } + + switch u.Scheme { + case "ws": + u.Scheme = "http" + case "wss": + u.Scheme = "https" + default: + return nil, fmt.Errorf("unexpected url scheme: %q", u.Scheme) + } + + req, _ := http.NewRequestWithContext(ctx, "GET", u.String(), nil) + req.Header = opts.HTTPHeader.Clone() + req.Header.Set("Connection", "Upgrade") + req.Header.Set("Upgrade", "websocket") + req.Header.Set("Sec-WebSocket-Version", "13") + req.Header.Set("Sec-WebSocket-Key", secWebSocketKey) + if len(opts.Subprotocols) > 0 { + req.Header.Set("Sec-WebSocket-Protocol", strings.Join(opts.Subprotocols, ",")) + } + if opts.CompressionMode != CompressionDisabled { + copts := opts.CompressionMode.opts() + copts.setHeader(req.Header) + } + + resp, err := opts.HTTPClient.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to send handshake request: %w", err) + } + return resp, nil +} + func secWebSocketKey() (string, error) { b := make([]byte, 16) _, err := io.ReadFull(rand.Reader, b) @@ -152,7 +155,7 @@ func secWebSocketKey() (string, error) { return base64.StdEncoding.EncodeToString(b), nil } -func verifyServerResponse(r *http.Request, resp *http.Response) (*compressionOptions, error) { +func verifyServerResponse(opts *DialOptions, secWebSocketKey string, resp *http.Response) (*compressionOptions, error) { if resp.StatusCode != http.StatusSwitchingProtocols { return nil, fmt.Errorf("expected handshake response status code %v but got %v", http.StatusSwitchingProtocols, resp.StatusCode) } @@ -165,23 +168,34 @@ func verifyServerResponse(r *http.Request, resp *http.Response) (*compressionOpt return nil, fmt.Errorf("WebSocket protocol violation: Upgrade header %q does not contain websocket", resp.Header.Get("Upgrade")) } - if resp.Header.Get("Sec-WebSocket-Accept") != secWebSocketAccept(r.Header.Get("Sec-WebSocket-Key")) { + if resp.Header.Get("Sec-WebSocket-Accept") != secWebSocketAccept(secWebSocketKey) { return nil, fmt.Errorf("WebSocket protocol violation: invalid Sec-WebSocket-Accept %q, key %q", resp.Header.Get("Sec-WebSocket-Accept"), - r.Header.Get("Sec-WebSocket-Key"), + secWebSocketKey, ) } - if proto := resp.Header.Get("Sec-WebSocket-Protocol"); proto != "" && !headerContainsToken(r.Header, "Sec-WebSocket-Protocol", proto) { - return nil, fmt.Errorf("WebSocket protocol violation: unexpected Sec-WebSocket-Protocol from server: %q", proto) - } - - copts, err := verifyServerExtensions(resp.Header) + err := verifySubprotocol(opts.Subprotocols, resp) if err != nil { return nil, err } - return copts, nil + return verifyServerExtensions(resp.Header) +} + +func verifySubprotocol(subprotos []string, resp *http.Response) error { + proto := resp.Header.Get("Sec-WebSocket-Protocol") + if proto == "" { + return nil + } + + for _, sp2 := range subprotos { + if strings.EqualFold(sp2, proto) { + return nil + } + } + + return fmt.Errorf("WebSocket protocol violation: unexpected Sec-WebSocket-Protocol from server: %q", proto) } func verifyServerExtensions(h http.Header) (*compressionOptions, error) { @@ -191,12 +205,8 @@ func verifyServerExtensions(h http.Header) (*compressionOptions, error) { } ext := exts[0] - if ext.name != "permessage-deflate" { - return nil, fmt.Errorf("unexpected extension from server: %q", ext) - } - - if len(exts) > 1 { - return nil, fmt.Errorf("unexpected extra extensions from server: %+v", exts[1:]) + if ext.name != "permessage-deflate" || len(exts) > 1 { + return nil, fmt.Errorf("WebSocket protcol violation: unsupported extensions from server: %+v", exts[1:]) } copts := &compressionOptions{} @@ -204,13 +214,11 @@ func verifyServerExtensions(h http.Header) (*compressionOptions, error) { switch p { case "client_no_context_takeover": copts.clientNoContextTakeover = true - continue case "server_no_context_takeover": copts.serverNoContextTakeover = true - continue + default: + return nil, fmt.Errorf("unsupported permessage-deflate parameter: %q", p) } - - return nil, fmt.Errorf("unsupported permessage-deflate parameter: %q", p) } return copts, nil diff --git a/dial_test.go b/dial_test.go index 5eeb904a..6286f0ff 100644 --- a/dial_test.go +++ b/dial_test.go @@ -6,6 +6,7 @@ import ( "context" "net/http" "net/http/httptest" + "strings" "testing" "time" ) @@ -140,7 +141,10 @@ func Test_verifyServerHandshake(t *testing.T) { resp.Header.Set("Sec-WebSocket-Accept", secWebSocketAccept(key)) } - _, err = verifyServerResponse(r, resp) + opts := &DialOptions{ + Subprotocols: strings.Split(r.Header.Get("Sec-WebSocket-Protocol"), ","), + } + _, err = verifyServerResponse(opts, key, resp) if (err == nil) != tc.success { t.Fatalf("unexpected error: %+v", err) } From 120911b598eab98c2ad624baaa0f81b473e7baad Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 28 Nov 2019 18:13:37 -0500 Subject: [PATCH 226/519] Remove use of math/rand.Init --- assert_test.go | 38 +++++++++++++++----------------------- close.go | 43 ++++++++++++++++++++++--------------------- close_test.go | 25 +++++++++++-------------- doc.go | 43 +++++++++---------------------------------- frame.go | 2 +- frame_test.go | 23 +++++++++-------------- go.mod | 2 +- ws_js.go | 14 +++++--------- ws_js_test.go | 2 +- 9 files changed, 74 insertions(+), 118 deletions(-) diff --git a/assert_test.go b/assert_test.go index 6e4e75e6..b6e50a47 100644 --- a/assert_test.go +++ b/assert_test.go @@ -2,38 +2,31 @@ package websocket_test import ( "context" - "math/rand" + "crypto/rand" + "io" "strings" "testing" - "time" "nhooyr.io/websocket" "nhooyr.io/websocket/internal/assert" "nhooyr.io/websocket/wsjson" ) -func init() { - rand.Seed(time.Now().UnixNano()) -} - -func randBytes(n int) []byte { +func randBytes(t *testing.T, n int) []byte { b := make([]byte, n) - rand.Read(b) + _, err := io.ReadFull(rand.Reader, b) + assert.Success(t, err) return b } func assertJSONEcho(t *testing.T, ctx context.Context, c *websocket.Conn, n int) { t.Helper() - exp := randString(n) + exp := randString(t, n) err := wsjson.Write(ctx, c, exp) assert.Success(t, err) - var act interface{} - err = wsjson.Read(ctx, c, &act) - assert.Success(t, err) - - assert.Equal(t, exp, act, "unexpected JSON") + assertJSONRead(t, ctx, c, exp) } func assertJSONRead(t *testing.T, ctx context.Context, c *websocket.Conn, exp interface{}) { @@ -43,11 +36,11 @@ func assertJSONRead(t *testing.T, ctx context.Context, c *websocket.Conn, exp in err := wsjson.Read(ctx, c, &act) assert.Success(t, err) - assert.Equal(t, exp, act, "unexpected JSON") + assert.Equal(t, exp, act, "JSON") } -func randString(n int) string { - s := strings.ToValidUTF8(string(randBytes(n)), "_") +func randString(t *testing.T, n int) string { + s := strings.ToValidUTF8(string(randBytes(t, n)), "_") if len(s) > n { return s[:n] } @@ -62,25 +55,24 @@ func randString(n int) string { func assertEcho(t *testing.T, ctx context.Context, c *websocket.Conn, typ websocket.MessageType, n int) { t.Helper() - p := randBytes(n) + p := randBytes(t, n) err := c.Write(ctx, typ, p) assert.Success(t, err) typ2, p2, err := c.Read(ctx) assert.Success(t, err) - assert.Equal(t, typ, typ2, "unexpected data type") - assert.Equal(t, p, p2, "unexpected payload") + assert.Equal(t, typ, typ2, "data type") + assert.Equal(t, p, p2, "payload") } func assertSubprotocol(t *testing.T, c *websocket.Conn, exp string) { t.Helper() - assert.Equal(t, exp, c.Subprotocol(), "unexpected subprotocol") + assert.Equal(t, exp, c.Subprotocol(), "subprotocol") } func assertCloseStatus(t *testing.T, exp websocket.StatusCode, err error) { t.Helper() - - assert.Equal(t, exp, websocket.CloseStatus(err), "unexpected status code") + assert.Equal(t, exp, websocket.CloseStatus(err), "StatusCode") } diff --git a/close.go b/close.go index 6bb48bd5..baa1a7e0 100644 --- a/close.go +++ b/close.go @@ -15,11 +15,13 @@ import ( // https://tools.ietf.org/html/rfc6455#section-7.4 type StatusCode int -// These codes were retrieved from: // https://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number // -// The defined constants only represent the status codes registered with IANA. -// The 4000-4999 range of status codes is reserved for arbitrary use by applications. +// These are only the status codes defined by the protocol. +// +// You can define custom codes in the 3000-4999 range. +// The 3000-3999 range is reserved for use by libraries, frameworks and applications. +// The 4000-4999 range is reserved for private use. const ( StatusNormalClosure StatusCode = 1000 StatusGoingAway StatusCode = 1001 @@ -31,11 +33,12 @@ const ( // StatusNoStatusRcvd cannot be sent in a close message. // It is reserved for when a close message is received without - // an explicit status. + // a status code. StatusNoStatusRcvd StatusCode = 1005 - // StatusAbnormalClosure is only exported for use with Wasm. - // In non Wasm Go, the returned error will indicate whether the connection was closed or not or what happened. + // StatusAbnormalClosure is exported for use only with Wasm. + // In non Wasm Go, the returned error will indicate whether the + // connection was closed abnormally. StatusAbnormalClosure StatusCode = 1006 StatusInvalidFramePayloadData StatusCode = 1007 @@ -48,15 +51,15 @@ const ( StatusBadGateway StatusCode = 1014 // StatusTLSHandshake is only exported for use with Wasm. - // In non Wasm Go, the returned error will indicate whether there was a TLS handshake failure. + // In non Wasm Go, the returned error will indicate whether there was + // a TLS handshake failure. StatusTLSHandshake StatusCode = 1015 ) -// CloseError represents a WebSocket close frame. -// It is returned by Conn's methods when a WebSocket close frame is received from -// the peer. -// You will need to use the https://golang.org/pkg/errors/#As function, new in Go 1.13, -// to check for this error. See the CloseError example. +// CloseError is returned when the connection is closed with a status and reason. +// +// Use Go 1.13's errors.As to check for this error. +// Also see the CloseStatus helper. type CloseError struct { Code StatusCode Reason string @@ -66,9 +69,10 @@ func (ce CloseError) Error() string { return fmt.Sprintf("status = %v and reason = %q", ce.Code, ce.Reason) } -// CloseStatus is a convenience wrapper around errors.As to grab -// the status code from a *CloseError. If the passed error is nil -// or not a *CloseError, the returned StatusCode will be -1. +// CloseStatus is a convenience wrapper around Go 1.13's errors.As to grab +// the status code from a CloseError. +// +// -1 will be returned if the passed error is nil or not a CloseError. func CloseStatus(err error) StatusCode { var ce CloseError if errors.As(err, &ce) { @@ -77,19 +81,16 @@ func CloseStatus(err error) StatusCode { return -1 } -// Close closes the WebSocket connection with the given status code and reason. +// Close performs the WebSocket close handshake with the given status code and reason. // // It will write a WebSocket close frame with a timeout of 5s and then wait 5s for // the peer to send a close frame. -// Thus, it implements the full WebSocket close handshake. -// All data messages received from the peer during the close handshake -// will be discarded. +// All data messages received from the peer during the close handshake will be discarded. // // The connection can only be closed once. Additional calls to Close // are no-ops. // -// The maximum length of reason must be 125 bytes otherwise an internal -// error will be sent to the peer. For this reason, you should avoid +// The maximum length of reason must be 125 bytes. Avoid // sending a dynamic reason. // // Close will unblock all goroutines interacting with the connection once diff --git a/close_test.go b/close_test.go index c2d11bb8..9551699a 100644 --- a/close_test.go +++ b/close_test.go @@ -6,8 +6,6 @@ import ( "strings" "testing" - "github.com/google/go-cmp/cmp" - "nhooyr.io/websocket/internal/assert" ) @@ -51,8 +49,10 @@ func TestCloseError(t *testing.T) { t.Parallel() _, err := tc.ce.bytesErr() - if (err == nil) != tc.success { - t.Fatalf("unexpected error value: %+v", err) + if (tc.success) { + assert.Success(t, err) + } else { + assert.Error(t, err) } }) } @@ -101,12 +101,11 @@ func Test_parseClosePayload(t *testing.T) { t.Parallel() ce, err := parseClosePayload(tc.p) - if (err == nil) != tc.success { - t.Fatalf("unexpected expected error value: %+v", err) - } - - if tc.success && tc.ce != ce { - t.Fatalf("unexpected close error: %v", cmp.Diff(tc.ce, ce)) + if (tc.success) { + assert.Success(t, err) + assert.Equal(t, tc.ce, ce, "CloseError") + } else { + assert.Error(t, err) } }) } @@ -152,9 +151,7 @@ func Test_validWireCloseCode(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - if valid := validWireCloseCode(tc.code); tc.valid != valid { - t.Fatalf("expected %v for %v but got %v", tc.valid, tc.code, valid) - } + assert.Equal(t, tc.code, validWireCloseCode(tc.code), "validWireCloseCode") }) } } @@ -191,7 +188,7 @@ func TestCloseStatus(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - assert.Equal(t, tc.exp, CloseStatus(tc.in), "unexpected close status") + assert.Equal(t, tc.exp, CloseStatus(tc.in), "CloseStatus") }) } } diff --git a/doc.go b/doc.go index 5285a780..54b7e1ea 100644 --- a/doc.go +++ b/doc.go @@ -4,51 +4,26 @@ // // https://tools.ietf.org/html/rfc6455 // -// Conn, Dial, and Accept are the main entrypoints into this package. Use Dial to dial -// a WebSocket server, Accept to accept a WebSocket client dial and then Conn to interact -// with the resulting WebSocket connections. +// Use Dial to dial a WebSocket server and Accept to accept a WebSocket client. +// Conn represents the resulting WebSocket connection. // // The examples are the best way to understand how to correctly use the library. // -// The wsjson and wspb subpackages contain helpers for JSON and ProtoBuf messages. +// The wsjson and wspb subpackages contain helpers for JSON and Protobuf messages. // -// See https://nhooyr.io/websocket for more overview docs and a -// comparison with existing implementations. -// -// Use the errors.As function new in Go 1.13 to check for websocket.CloseError. -// Or use the CloseStatus function to grab the StatusCode out of a websocket.CloseError -// See the CloseStatus example. +// See https://nhooyr.io/websocket for further information. // // Wasm // -// The client side fully supports compiling to Wasm. +// The client side supports compiling to Wasm. // It wraps the WebSocket browser API. // // See https://developer.mozilla.org/en-US/docs/Web/API/WebSocket // -// Thus the unsupported features (not compiled in) for Wasm are: -// -// - Accept and AcceptOptions -// - Conn.Ping -// - HTTPClient and HTTPHeader fields in DialOptions -// - CompressionOptions -// -// The *http.Response returned by Dial will always either be nil or &http.Response{} as -// we do not have access to the handshake response in the browser. -// -// The Writer method on the Conn buffers everything in memory and then sends it as a message -// when the writer is closed. -// -// The Reader method also reads the entire response and then returns a reader that -// reads from the byte slice. -// -// SetReadLimit cannot actually limit the number of bytes read from the connection so instead -// when a message beyond the limit is fully read, it throws an error. -// -// Writes are also always async so the passed context is no-op. +// Some important caveats to be aware of: // -// Everything else is fully supported. This includes the wsjson and wspb helper packages. +// - Conn.Ping is no-op +// - HTTPClient, HTTPHeader and CompressionOptions in DialOptions are no-op +// - *http.Response from Dial is &http.Response{} on success // -// Once https://github.com/gopherjs/gopherjs/issues/929 is closed, GopherJS should be supported -// as well. package websocket // import "nhooyr.io/websocket" diff --git a/frame.go b/frame.go index e55c8f2c..0257835e 100644 --- a/frame.go +++ b/frame.go @@ -12,7 +12,7 @@ import ( // opcode represents a WebSocket opcode. type opcode int -// List at https://tools.ietf.org/html/rfc6455#section-11.8. +// https://tools.ietf.org/html/rfc6455#section-11.8. const ( opContinuation opcode = iota opText diff --git a/frame_test.go b/frame_test.go index fa231c57..68455cfa 100644 --- a/frame_test.go +++ b/frame_test.go @@ -19,10 +19,6 @@ import ( "nhooyr.io/websocket/internal/assert" ) -func init() { - rand.Seed(time.Now().UnixNano()) -} - func TestHeader(t *testing.T) { t.Parallel() @@ -56,8 +52,9 @@ func TestHeader(t *testing.T) { t.Run("fuzz", func(t *testing.T) { t.Parallel() + r := rand.New(rand.NewSource(time.Now().UnixNano())) randBool := func() bool { - return rand.Intn(1) == 0 + return r.Intn(1) == 0 } for i := 0; i < 10000; i++ { @@ -66,11 +63,11 @@ func TestHeader(t *testing.T) { rsv1: randBool(), rsv2: randBool(), rsv3: randBool(), - opcode: opcode(rand.Intn(16)), + opcode: opcode(r.Intn(16)), masked: randBool(), - maskKey: rand.Uint32(), - payloadLength: rand.Int63(), + maskKey: r.Uint32(), + payloadLength: r.Int63(), } testHeader(t, h) @@ -91,7 +88,7 @@ func testHeader(t *testing.T, h header) { h2, err := readFrameHeader(r) assert.Success(t, err) - assert.Equal(t, h, h2, "written and read headers differ") + assert.Equal(t, h, h2, "header") } func Test_mask(t *testing.T) { @@ -102,8 +99,8 @@ func Test_mask(t *testing.T) { p := []byte{0xa, 0xb, 0xc, 0xf2, 0xc} gotKey32 := mask(key32, p) - assert.Equal(t, []byte{0, 0, 0, 0x0d, 0x6}, p, "unexpected mask") - assert.Equal(t, bits.RotateLeft32(key32, -8), gotKey32, "unexpected mask key") + assert.Equal(t, []byte{0, 0, 0, 0x0d, 0x6}, p, "mask") + assert.Equal(t, bits.RotateLeft32(key32, -8), gotKey32, "mask key") } func basicMask(maskKey [4]byte, pos int, b []byte) int { @@ -173,9 +170,7 @@ func Benchmark_mask(b *testing.B) { }, } - var key [4]byte - _, err := rand.Read(key[:]) - assert.Success(b, err) + key := [4]byte{1, 2, 3, 4} for _, size := range sizes { p := make([]byte, size) diff --git a/go.mod b/go.mod index 3108c020..1a2b08f4 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee // indirect github.com/gobwas/pool v0.2.0 // indirect github.com/gobwas/ws v1.0.2 - github.com/golang/protobuf v1.3.2 // indirect + github.com/golang/protobuf v1.3.2 github.com/google/go-cmp v0.3.1 github.com/gorilla/websocket v1.4.1 github.com/kr/pretty v0.1.0 // indirect diff --git a/ws_js.go b/ws_js.go index 7f10ee17..3043106b 100644 --- a/ws_js.go +++ b/ws_js.go @@ -1,5 +1,3 @@ -// +build js - package websocket // import "nhooyr.io/websocket" import ( @@ -8,7 +6,6 @@ import ( "errors" "fmt" "io" - "net/http" "reflect" "runtime" "sync" @@ -242,15 +239,15 @@ type DialOptions struct { // The passed context bounds the maximum time spent waiting for the connection to open. // The returned *http.Response is always nil or the zero value. It's only in the signature // to match the core API. -func Dial(ctx context.Context, url string, opts *DialOptions) (*Conn, *http.Response, error) { - c, resp, err := dial(ctx, url, opts) +func Dial(ctx context.Context, url string, opts *DialOptions) (*Conn, error) { + c, err := dial(ctx, url, opts) if err != nil { return nil, resp, fmt.Errorf("failed to WebSocket dial %q: %w", url, err) } - return c, resp, nil + return c, nil } -func dial(ctx context.Context, url string, opts *DialOptions) (*Conn, *http.Response, error) { +func dial(ctx context.Context, url string, opts *DialOptions) (*Conn, error) { if opts == nil { opts = &DialOptions{} } @@ -280,8 +277,7 @@ func dial(ctx context.Context, url string, opts *DialOptions) (*Conn, *http.Resp return c, nil, c.closeErr } - // Have to return a non nil response as the normal API does that. - return c, &http.Response{}, nil + return c, nil } // Reader attempts to read a message from the connection. diff --git a/ws_js_test.go b/ws_js_test.go index 9330b411..ea888b59 100644 --- a/ws_js_test.go +++ b/ws_js_test.go @@ -23,7 +23,7 @@ func TestEcho(t *testing.T) { defer c.Close(websocket.StatusInternalError, "") assertSubprotocol(t, c, "echo") - assert.Equalf(t, &http.Response{}, resp, "unexpected http response") + assert.Equalf(t, &http.Response{}, resp, "http.Response") assertJSONEcho(t, ctx, c, 1024) assertEcho(t, ctx, c, websocket.MessageBinary, 1024) From 7ad15141157fc06bdeb2505085811563f182688d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 28 Nov 2019 19:13:42 -0500 Subject: [PATCH 227/519] Update README.md comparison --- README.md | 136 +++++++++++++++++++----------------------------------- close.go | 17 ++++--- conn.go | 26 +++++------ read.go | 4 +- 4 files changed, 70 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index c927e8c1..477a59ff 100644 --- a/README.md +++ b/README.md @@ -16,17 +16,17 @@ go get nhooyr.io/websocket ## Features - Minimal and idiomatic API -- Tiny codebase at 2200 lines - First class [context.Context](https://blog.golang.org/context) support - Thorough tests, fully passes the [autobahn-testsuite](https://github.com/crossbario/autobahn-testsuite) - [Zero dependencies](https://godoc.org/nhooyr.io/websocket?imports) - JSON and ProtoBuf helpers in the [wsjson](https://godoc.org/nhooyr.io/websocket/wsjson) and [wspb](https://godoc.org/nhooyr.io/websocket/wspb) subpackages -- Highly optimized by default - - Zero alloc reads and writes -- Concurrent writes out of the box -- [Complete Wasm](https://godoc.org/nhooyr.io/websocket#hdr-Wasm) support -- [Close handshake](https://godoc.org/nhooyr.io/websocket#Conn.Close) -- Full support of [RFC 7692](https://tools.ietf.org/html/rfc7692) permessage-deflate compression extension +- Zero alloc reads and writes +- Concurrent writes +- WebSocket [Close handshake](https://godoc.org/nhooyr.io/websocket#Conn.Close) +- [net.Conn](https://godoc.org/nhooyr.io/websocket#NetConn) wrapper +- WebSocket [Pings](https://godoc.org/nhooyr.io/websocket#Conn.Ping) +- [RFC 7692](https://tools.ietf.org/html/rfc7692) permessage-deflate compression +- [Wasm](https://godoc.org/nhooyr.io/websocket#hdr-Wasm) ## Roadmap @@ -34,11 +34,7 @@ go get nhooyr.io/websocket ## Examples -For a production quality example that shows off the full API, see the [echo example on the godoc](https://godoc.org/nhooyr.io/websocket#example-package--Echo). On github, the example is at [example_echo_test.go](./example_echo_test.go). - -Use the [errors.As](https://golang.org/pkg/errors/#As) function [new in Go 1.13](https://golang.org/doc/go1.13#error_wrapping) to check for [websocket.CloseError](https://godoc.org/nhooyr.io/websocket#CloseError). -There is also [websocket.CloseStatus](https://godoc.org/nhooyr.io/websocket#CloseStatus) to quickly grab the close status code out of a [websocket.CloseError](https://godoc.org/nhooyr.io/websocket#CloseError). -See the [CloseStatus godoc example](https://godoc.org/nhooyr.io/websocket#example-CloseStatus). +For a production quality example that demonstrates the full API, see the [echo example](https://godoc.org/nhooyr.io/websocket#example-package--Echo). ### Server @@ -87,83 +83,45 @@ c.Close(websocket.StatusNormalClosure, "") ## Comparison -Before the comparison, I want to point out that gorilla/websocket was extremely useful in implementing the -WebSocket protocol correctly so _big thanks_ to its authors. In particular, I made sure to go through the -issue tracker of gorilla/websocket to ensure I implemented details correctly and understood how people were -using WebSockets in production. - -### gorilla/websocket - -https://github.com/gorilla/websocket - -The implementation of gorilla/websocket is 6 years old. As such, it is -widely used and very mature compared to nhooyr.io/websocket. - -On the other hand, it has grown organically and now there are too many ways to do -the same thing. Compare the godoc of -[nhooyr/websocket](https://godoc.org/nhooyr.io/websocket) with -[gorilla/websocket](https://godoc.org/github.com/gorilla/websocket) side by side. - -The API for nhooyr.io/websocket has been designed such that there is only one way to do things. -This makes it easy to use correctly. Not only is the API simpler, the implementation is -only 2200 lines whereas gorilla/websocket is at 3500 lines. That's more code to maintain, -more code to test, more code to document and more surface area for bugs. - -Moreover, nhooyr.io/websocket supports newer Go idioms such as context.Context. -It also uses net/http's Client and ResponseWriter directly for WebSocket handshakes. -gorilla/websocket writes its handshakes to the underlying net.Conn. -Thus it has to reinvent hooks for TLS and proxies and prevents easy support of HTTP/2. - -Some more advantages of nhooyr.io/websocket are that it supports concurrent writes and -makes it very easy to close the connection with a status code and reason. In fact, -nhooyr.io/websocket even implements the complete WebSocket close handshake for you whereas -with gorilla/websocket you have to perform it manually. See [gorilla/websocket#448](https://github.com/gorilla/websocket/issues/448). - -The ping API is also nicer. gorilla/websocket requires registering a pong handler on the Conn -which results in awkward control flow. With nhooyr.io/websocket you use the Ping method on the Conn -that sends a ping and also waits for the pong. - -Additionally, nhooyr.io/websocket can compile to [Wasm](https://godoc.org/nhooyr.io/websocket#hdr-Wasm) for the browser. - -In terms of performance, the differences mostly depend on your application code. nhooyr.io/websocket -reuses message buffers out of the box if you use the wsjson and wspb subpackages. -As mentioned above, nhooyr.io/websocket also supports concurrent writers. - -The WebSocket masking algorithm used by this package is [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) -faster than gorilla/websocket while using only pure safe Go. - -The [permessage-deflate compression extension](https://tools.ietf.org/html/rfc7692) is fully supported by this library -whereas gorilla only supports no context takeover mode. See our godoc for the differences. This will make a big -difference on bandwidth used in most use cases. - -The only performance con to nhooyr.io/websocket is that it uses a goroutine to support -cancellation with context.Context. This costs 2 KB of memory which is cheap compared to -the benefits. - -### x/net/websocket - -https://godoc.org/golang.org/x/net/websocket - -Unmaintained and the API does not reflect WebSocket semantics. Should never be used. - -See https://github.com/golang/go/issues/18152 - -### gobwas/ws - -https://github.com/gobwas/ws - -This library has an extremely flexible API but that comes at the cost of usability -and clarity. - -Due to its flexibility, it can be used in a event driven style for performance. -Definitely check out his fantastic [blog post](https://medium.freecodecamp.org/million-websockets-and-go-cc58418460bb) about performant WebSocket servers. - -If you want a library that gives you absolute control over everything, this is the library. -But for 99.9% of use cases, nhooyr.io/websocket will fit better as it is both easier and -faster for normal idiomatic Go. The masking implementation is [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) -faster, the compression extensions are fully supported and as much as possible is reused by default. - -See the gorilla/websocket comparison for more performance details. +### [gorilla/websocket](https://github.com/gorilla/websocket) + +Advantages of nhooyr.io/websocket: + - Minimal and idiomatic API + - 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. + - [net.Conn](https://godoc.org/nhooyr.io/websocket#NetConn) wrapper + - Zero alloc reads and writes ([gorilla/websocket#535](https://github.com/gorilla/websocket/issues/535)) + - Full [context.Context](https://blog.golang.org/context) support + - Uses [net/http.Client](https://golang.org/pkg/net/http/#Client) for dialing + - Will enable easy HTTP/2 support in the future + - Gorilla writes directly to a net.Conn and so duplicates features from net/http.Client. + - Concurrent writes + - Close handshake ([gorilla/websocket#448](https://github.com/gorilla/websocket/issues/448)) + - Idiomatic [ping](https://godoc.org/nhooyr.io/websocket#Conn.Ping) API + - gorilla/websocket requires registering a pong callback and then sending a Ping + - Wasm ([gorilla/websocket#432](https://github.com/gorilla/websocket/issues/432)) + - Transparent buffer reuse with [wsjson](https://godoc.org/nhooyr.io/websocket/wsjson) and [wspb](https://godoc.org/nhooyr.io/websocket/wspb) subpackages + - [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster WebSocket masking implementation in pure Go + - Gorilla's implementation depends on unsafe and is slower + - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support + - Gorilla only supports no context takeover mode + - [CloseRead](https://godoc.org/nhooyr.io/websocket#Conn.CloseRead) helper + - Actively maintained ([gorilla/websocket#370](https://github.com/gorilla/websocket/issues/370)) + +Advantages of gorilla/websocket: + - Widely used and mature + +### [x/net/websocket](https://godoc.org/golang.org/x/net/websocket) + +Deprecated. See ([golang/go/issues/18152](https://github.com/golang/go/issues/18152)). + +The [net.Conn](https://godoc.org/nhooyr.io/websocket#NetConn) wrapper will ease in transitioning to nhooyr.io/websocket. + +### [gobwas/ws](https://github.com/gobwas/ws) + +This library has an extremely flexible API that allows it to be used in an unidiomatic event driven style +for performance. See the author's [blog post](https://medium.freecodecamp.org/million-websockets-and-go-cc58418460bb). + +When writing idiomatic Go, nhooyr.io/websocket is a better choice as it will be faster and easier to use. ## Users diff --git a/close.go b/close.go index baa1a7e0..a02dc7d9 100644 --- a/close.go +++ b/close.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "log" + "nhooyr.io/websocket/internal/errd" "time" "nhooyr.io/websocket/internal/bpool" @@ -96,15 +97,13 @@ func CloseStatus(err error) StatusCode { // Close will unblock all goroutines interacting with the connection once // complete. func (c *Conn) Close(code StatusCode, reason string) error { - err := c.closeHandshake(code, reason) - if err != nil { - return fmt.Errorf("failed to close WebSocket: %w", err) - } - return nil + return c.closeHandshake(code, reason) } -func (c *Conn) closeHandshake(code StatusCode, reason string) error { - err := c.cw.sendClose(code, reason) +func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { + defer errd.Wrap(&err, "failed to close WebSocket") + + err = c.cw.sendClose(code, reason) if err != nil { return err } @@ -115,7 +114,7 @@ func (c *Conn) closeHandshake(code StatusCode, reason string) error { func (cw *connWriter) error(code StatusCode, err error) { cw.c.setCloseErr(err) cw.sendClose(code, err.Error()) - cw.c.close(nil) + cw.c.closeWithErr(nil) } func (cw *connWriter) sendClose(code StatusCode, reason string) error { @@ -135,7 +134,7 @@ func (cw *connWriter) sendClose(code StatusCode, reason string) error { } func (cr *connReader) waitClose() error { - defer cr.c.close(nil) + defer cr.c.closeWithErr(nil) return nil diff --git a/conn.go b/conn.go index 5c041b8d..d9001791 100644 --- a/conn.go +++ b/conn.go @@ -33,11 +33,10 @@ const ( // frames will not be handled. See the docs on Reader and CloseRead. // // Be sure to call Close on the connection when you -// are finished with it to release the associated resources. +// are finished with it to release associated resources. // -// Every error from Read or Reader will cause the connection -// to be closed so you do not need to write your own error message. -// This applies to the Read methods in the wsjson/wspb subpackages as well. +// On any error from any method, the connection is closed +// with an appropriate reason. type Conn struct { subprotocol string rwc io.ReadWriteCloser @@ -69,11 +68,12 @@ type connConfig struct { } func newConn(cfg connConfig) *Conn { - c := &Conn{} - c.subprotocol = cfg.subprotocol - c.rwc = cfg.rwc - c.client = cfg.client - c.copts = cfg.copts + c := &Conn{ + subprotocol: cfg.subprotocol, + rwc: cfg.rwc, + client: cfg.client, + copts: cfg.copts, + } c.cr.init(c, cfg.br) c.cw.init(c, cfg.bw) @@ -82,7 +82,7 @@ func newConn(cfg connConfig) *Conn { c.activePings = make(map[string]chan<- struct{}) runtime.SetFinalizer(c, func(c *Conn) { - c.close(errors.New("connection garbage collected")) + c.closeWithErr(errors.New("connection garbage collected")) }) go c.timeoutLoop() @@ -96,7 +96,7 @@ func (c *Conn) Subprotocol() string { return c.subprotocol } -func (c *Conn) close(err error) { +func (c *Conn) closeWithErr(err error) { c.closeMu.Lock() defer c.closeMu.Unlock() @@ -135,7 +135,7 @@ func (c *Conn) timeoutLoop() { c.cw.error(StatusPolicyViolation, errors.New("timed out")) return case <-writeCtx.Done(): - c.close(fmt.Errorf("write timed out: %w", writeCtx.Err())) + c.closeWithErr(fmt.Errorf("write timed out: %w", writeCtx.Err())) return } } @@ -185,7 +185,7 @@ func (c *Conn) ping(ctx context.Context, p string) error { return c.closeErr case <-ctx.Done(): err := fmt.Errorf("failed to wait for pong: %w", ctx.Err()) - c.close(err) + c.closeWithErr(err) return err case <-pong: return nil diff --git a/read.go b/read.go index 13c8d703..7dba832a 100644 --- a/read.go +++ b/read.go @@ -199,7 +199,7 @@ func (cr *connReader) frameHeader(ctx context.Context) (header, error) { case <-ctx.Done(): return header{}, ctx.Err() default: - cr.c.close(err) + cr.c.closeWithErr(err) return header{}, err } } @@ -229,7 +229,7 @@ func (cr *connReader) framePayload(ctx context.Context, p []byte) (int, error) { return n, ctx.Err() default: err = fmt.Errorf("failed to read frame payload: %w", err) - cr.c.close(err) + cr.c.closeWithErr(err) return n, err } } From 746140b8b5604d895bea36e23cf511e749dfd66c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 28 Nov 2019 19:23:14 -0500 Subject: [PATCH 228/519] Further improve README --- README.md | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 477a59ff..efb4a592 100644 --- a/README.md +++ b/README.md @@ -22,11 +22,11 @@ go get nhooyr.io/websocket - JSON and ProtoBuf helpers in the [wsjson](https://godoc.org/nhooyr.io/websocket/wsjson) and [wspb](https://godoc.org/nhooyr.io/websocket/wspb) subpackages - Zero alloc reads and writes - Concurrent writes -- WebSocket [Close handshake](https://godoc.org/nhooyr.io/websocket#Conn.Close) +- [Close handshake](https://godoc.org/nhooyr.io/websocket#Conn.Close) - [net.Conn](https://godoc.org/nhooyr.io/websocket#NetConn) wrapper -- WebSocket [Pings](https://godoc.org/nhooyr.io/websocket#Conn.Ping) +- [Pings](https://godoc.org/nhooyr.io/websocket#Conn.Ping) - [RFC 7692](https://tools.ietf.org/html/rfc7692) permessage-deflate compression -- [Wasm](https://godoc.org/nhooyr.io/websocket#hdr-Wasm) +- Compile to [Wasm](https://godoc.org/nhooyr.io/websocket#hdr-Wasm) ## Roadmap @@ -83,7 +83,9 @@ c.Close(websocket.StatusNormalClosure, "") ## Comparison -### [gorilla/websocket](https://github.com/gorilla/websocket) +### gorilla/websocket + +[gorilla/websocket](https://github.com/gorilla/websocket) is a widely used and mature library. Advantages of nhooyr.io/websocket: - Minimal and idiomatic API @@ -103,25 +105,24 @@ Advantages of nhooyr.io/websocket: - [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster WebSocket masking implementation in pure Go - Gorilla's implementation depends on unsafe and is slower - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support - - Gorilla only supports no context takeover mode + - Gorilla only supports no context takeover mode - [CloseRead](https://godoc.org/nhooyr.io/websocket#Conn.CloseRead) helper - Actively maintained ([gorilla/websocket#370](https://github.com/gorilla/websocket/issues/370)) -Advantages of gorilla/websocket: - - Widely used and mature - -### [x/net/websocket](https://godoc.org/golang.org/x/net/websocket) +#### golang.org/x/net/websocket -Deprecated. See ([golang/go/issues/18152](https://github.com/golang/go/issues/18152)). +[golang.org/x/net/websocket](https://godoc.org/golang.org/x/net/websocket) is deprecated. +See ([golang/go/issues/18152](https://github.com/golang/go/issues/18152)). -The [net.Conn](https://godoc.org/nhooyr.io/websocket#NetConn) wrapper will ease in transitioning to nhooyr.io/websocket. +The [net.Conn](https://godoc.org/nhooyr.io/websocket#NetConn) wrapper will ease in transitioning +to nhooyr.io/websocket. -### [gobwas/ws](https://github.com/gobwas/ws) +#### gobwas/ws -This library has an extremely flexible API that allows it to be used in an unidiomatic event driven style -for performance. See the author's [blog post](https://medium.freecodecamp.org/million-websockets-and-go-cc58418460bb). +[gobwas/ws](https://github.com/gobwas/ws) has an extremely flexible API that allows it to be used +in an event driven style for performance. See the author's [blog post](https://medium.freecodecamp.org/million-websockets-and-go-cc58418460bb). -When writing idiomatic Go, nhooyr.io/websocket is a better choice as it will be faster and easier to use. +However when writing idiomatic Go, nhooyr.io/websocket will be faster and easier to use. ## Users From 43cb01eaf9fad1e2052a18b69b777db62820aae7 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 29 Nov 2019 00:00:52 -0500 Subject: [PATCH 229/519] Refactor read.go/write.go --- README.md | 43 +++--- assert_test.go | 13 +- close.go | 64 +++++---- conn.go | 92 ++++++++++--- conn_test.go | 3 +- internal/assert/assert.go | 2 +- read.go | 266 ++++++++++++++++---------------------- write.go | 215 +++++++++++++----------------- wsjson/wsjson.go | 1 - 9 files changed, 345 insertions(+), 354 deletions(-) diff --git a/README.md b/README.md index efb4a592..f0babdfc 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ go get nhooyr.io/websocket - Concurrent writes - [Close handshake](https://godoc.org/nhooyr.io/websocket#Conn.Close) - [net.Conn](https://godoc.org/nhooyr.io/websocket#NetConn) wrapper -- [Pings](https://godoc.org/nhooyr.io/websocket#Conn.Ping) +- [Ping pong](https://godoc.org/nhooyr.io/websocket#Conn.Ping) - [RFC 7692](https://tools.ietf.org/html/rfc7692) permessage-deflate compression - Compile to [Wasm](https://godoc.org/nhooyr.io/websocket#hdr-Wasm) @@ -88,26 +88,27 @@ c.Close(websocket.StatusNormalClosure, "") [gorilla/websocket](https://github.com/gorilla/websocket) is a widely used and mature library. Advantages of nhooyr.io/websocket: - - Minimal and idiomatic API - - 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. - - [net.Conn](https://godoc.org/nhooyr.io/websocket#NetConn) wrapper - - Zero alloc reads and writes ([gorilla/websocket#535](https://github.com/gorilla/websocket/issues/535)) - - Full [context.Context](https://blog.golang.org/context) support - - Uses [net/http.Client](https://golang.org/pkg/net/http/#Client) for dialing - - Will enable easy HTTP/2 support in the future - - Gorilla writes directly to a net.Conn and so duplicates features from net/http.Client. - - Concurrent writes - - Close handshake ([gorilla/websocket#448](https://github.com/gorilla/websocket/issues/448)) - - Idiomatic [ping](https://godoc.org/nhooyr.io/websocket#Conn.Ping) API - - gorilla/websocket requires registering a pong callback and then sending a Ping - - Wasm ([gorilla/websocket#432](https://github.com/gorilla/websocket/issues/432)) - - Transparent buffer reuse with [wsjson](https://godoc.org/nhooyr.io/websocket/wsjson) and [wspb](https://godoc.org/nhooyr.io/websocket/wspb) subpackages - - [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster WebSocket masking implementation in pure Go - - Gorilla's implementation depends on unsafe and is slower - - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support + +- Minimal and idiomatic API + - 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. +- [net.Conn](https://godoc.org/nhooyr.io/websocket#NetConn) wrapper +- Zero alloc reads and writes ([gorilla/websocket#535](https://github.com/gorilla/websocket/issues/535)) +- Full [context.Context](https://blog.golang.org/context) support +- Uses [net/http.Client](https://golang.org/pkg/net/http/#Client) for dialing + - Will enable easy HTTP/2 support in the future + - Gorilla writes directly to a net.Conn and so duplicates features from net/http.Client. +- Concurrent writes +- Close handshake ([gorilla/websocket#448](https://github.com/gorilla/websocket/issues/448)) +- Idiomatic [ping](https://godoc.org/nhooyr.io/websocket#Conn.Ping) API + - gorilla/websocket requires registering a pong callback and then sending a Ping +- Wasm ([gorilla/websocket#432](https://github.com/gorilla/websocket/issues/432)) +- Transparent message buffer reuse with [wsjson](https://godoc.org/nhooyr.io/websocket/wsjson) and [wspb](https://godoc.org/nhooyr.io/websocket/wspb) subpackages +- [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster WebSocket masking implementation in pure Go + - Gorilla's implementation depends on unsafe and is slower +- Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support - Gorilla only supports no context takeover mode - - [CloseRead](https://godoc.org/nhooyr.io/websocket#Conn.CloseRead) helper - - Actively maintained ([gorilla/websocket#370](https://github.com/gorilla/websocket/issues/370)) +- [CloseRead](https://godoc.org/nhooyr.io/websocket#Conn.CloseRead) helper +- Actively maintained ([gorilla/websocket#370](https://github.com/gorilla/websocket/issues/370)) #### golang.org/x/net/websocket @@ -120,7 +121,7 @@ to nhooyr.io/websocket. #### gobwas/ws [gobwas/ws](https://github.com/gobwas/ws) has an extremely flexible API that allows it to be used -in an event driven style for performance. See the author's [blog post](https://medium.freecodecamp.org/million-websockets-and-go-cc58418460bb). +in an event driven style for performance. See the author's [blog post](https://medium.freecodecamp.org/million-websockets-and-go-cc58418460bb). However when writing idiomatic Go, nhooyr.io/websocket will be faster and easier to use. diff --git a/assert_test.go b/assert_test.go index b6e50a47..e4319938 100644 --- a/assert_test.go +++ b/assert_test.go @@ -4,12 +4,11 @@ import ( "context" "crypto/rand" "io" - "strings" - "testing" - "nhooyr.io/websocket" "nhooyr.io/websocket/internal/assert" "nhooyr.io/websocket/wsjson" + "strings" + "testing" ) func randBytes(t *testing.T, n int) []byte { @@ -21,12 +20,15 @@ func randBytes(t *testing.T, n int) []byte { func assertJSONEcho(t *testing.T, ctx context.Context, c *websocket.Conn, n int) { t.Helper() + defer c.Close(websocket.StatusInternalError, "") exp := randString(t, n) err := wsjson.Write(ctx, c, exp) assert.Success(t, err) assertJSONRead(t, ctx, c, exp) + + c.Close(websocket.StatusNormalClosure, "") } func assertJSONRead(t *testing.T, ctx context.Context, c *websocket.Conn, exp interface{}) { @@ -74,5 +76,10 @@ func assertSubprotocol(t *testing.T, c *websocket.Conn, exp string) { func assertCloseStatus(t *testing.T, exp websocket.StatusCode, err error) { t.Helper() + defer func() { + if t.Failed() { + t.Logf("error: %+v", err) + } + }() assert.Equal(t, exp, websocket.CloseStatus(err), "StatusCode") } diff --git a/close.go b/close.go index a02dc7d9..4c474b78 100644 --- a/close.go +++ b/close.go @@ -7,9 +7,6 @@ import ( "fmt" "log" "nhooyr.io/websocket/internal/errd" - "time" - - "nhooyr.io/websocket/internal/bpool" ) // StatusCode represents a WebSocket status code. @@ -103,59 +100,58 @@ func (c *Conn) Close(code StatusCode, reason string) error { func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { defer errd.Wrap(&err, "failed to close WebSocket") - err = c.cw.sendClose(code, reason) + err = c.writeClose(code, reason) if err != nil { return err } - return c.cr.waitClose() + return c.waitClose() } -func (cw *connWriter) error(code StatusCode, err error) { - cw.c.setCloseErr(err) - cw.sendClose(code, err.Error()) - cw.c.closeWithErr(nil) +func (c *Conn) writeError(code StatusCode, err error) { + c.setCloseErr(err) + c.writeClose(code, err.Error()) + c.closeWithErr(nil) } -func (cw *connWriter) sendClose(code StatusCode, reason string) error { +func (c *Conn) writeClose(code StatusCode, reason string) error { ce := CloseError{ Code: code, Reason: reason, } - cw.c.setCloseErr(fmt.Errorf("sent close frame: %w", ce)) + c.setCloseErr(fmt.Errorf("sent close frame: %w", ce)) var p []byte if ce.Code != StatusNoStatusRcvd { p = ce.bytes() } - return cw.control(context.Background(), opClose, p) + return c.writeControl(context.Background(), opClose, p) } -func (cr *connReader) waitClose() error { - defer cr.c.closeWithErr(nil) +func (c *Conn) waitClose() error { + defer c.closeWithErr(nil) return nil - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - defer cancel() - - err := cr.mu.Lock(ctx) - if err != nil { - return err - } - defer cr.mu.Unlock() - - b := bpool.Get() - buf := b.Bytes() - buf = buf[:cap(buf)] - defer bpool.Put(b) - - for { - // TODO - return nil - } + // ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + // defer cancel() + // + // err := cr.mu.Lock(ctx) + // if err != nil { + // return err + // } + // defer cr.mu.Unlock() + // + // b := bpool.Get() + // buf := b.Bytes() + // buf = buf[:cap(buf)] + // defer bpool.Put(b) + // + // for { + // return nil + // } } func parseClosePayload(p []byte) (CloseError, error) { @@ -230,11 +226,11 @@ func (ce CloseError) bytesErr() ([]byte, error) { func (c *Conn) setCloseErr(err error) { c.closeMu.Lock() - c.setCloseErrNoLock(err) + c.setCloseErrLocked(err) c.closeMu.Unlock() } -func (c *Conn) setCloseErrNoLock(err error) { +func (c *Conn) setCloseErrLocked(err error) { if c.closeErr == nil { c.closeErr = fmt.Errorf("WebSocket closed: %w", err) } diff --git a/conn.go b/conn.go index d9001791..dc067d18 100644 --- a/conn.go +++ b/conn.go @@ -30,7 +30,7 @@ const ( // All methods may be called concurrently except for Reader and Read. // // You must always read from the connection. Otherwise control -// frames will not be handled. See the docs on Reader and CloseRead. +// frames will not be handled. See Reader and CloseRead. // // Be sure to call Close on the connection when you // are finished with it to release associated resources. @@ -42,9 +42,22 @@ type Conn struct { rwc io.ReadWriteCloser client bool copts *compressionOptions + br *bufio.Reader + bw *bufio.Writer - cr connReader - cw connWriter + readTimeout chan context.Context + writeTimeout chan context.Context + + // Read state. + readMu mu + readControlBuf [maxControlPayload]byte + msgReader *msgReader + + // Write state. + msgWriter *msgWriter + writeFrameMu mu + writeBuf []byte + writeHeader header closed chan struct{} @@ -63,8 +76,8 @@ type connConfig struct { client bool copts *compressionOptions - bw *bufio.Writer br *bufio.Reader + bw *bufio.Writer } func newConn(cfg connConfig) *Conn { @@ -73,13 +86,23 @@ func newConn(cfg connConfig) *Conn { rwc: cfg.rwc, client: cfg.client, copts: cfg.copts, + + br: cfg.br, + bw: cfg.bw, + + readTimeout: make(chan context.Context), + writeTimeout: make(chan context.Context), + + closed: make(chan struct{}), + activePings: make(map[string]chan<- struct{}), } - c.cr.init(c, cfg.br) - c.cw.init(c, cfg.bw) + c.msgReader = newMsgReader(c) - c.closed = make(chan struct{}) - c.activePings = make(map[string]chan<- struct{}) + c.msgWriter = newMsgWriter(c) + if c.client { + c.writeBuf = extractBufioWriterBuf(c.bw, c.rwc) + } runtime.SetFinalizer(c, func(c *Conn) { c.closeWithErr(errors.New("connection garbage collected")) @@ -90,6 +113,34 @@ func newConn(cfg connConfig) *Conn { return c } +func newMsgReader(c *Conn) *msgReader { + mr := &msgReader{ + c: c, + fin: true, + } + + mr.limitReader = newLimitReader(c, readerFunc(mr.read), 32768) + if c.deflateNegotiated() && mr.contextTakeover() { + mr.ensureFlateReader() + } + + return mr +} + +func newMsgWriter(c *Conn) *msgWriter { + mw := &msgWriter{ + c: c, + } + mw.trimWriter = &trimLastFourBytesWriter{ + w: writerFunc(mw.write), + } + if c.deflateNegotiated() && mw.contextTakeover() { + mw.ensureFlateWriter() + } + + return mw +} + // Subprotocol returns the negotiated subprotocol. // An empty string means the default protocol. func (c *Conn) Subprotocol() string { @@ -105,7 +156,7 @@ func (c *Conn) closeWithErr(err error) { } close(c.closed) runtime.SetFinalizer(c, nil) - c.setCloseErrNoLock(err) + c.setCloseErrLocked(err) // Have to close after c.closed is closed to ensure any goroutine that wakes up // from the connection being closed also sees that c.closed is closed and returns @@ -113,8 +164,18 @@ func (c *Conn) closeWithErr(err error) { c.rwc.Close() go func() { - c.cr.close() - c.cw.close() + if c.client { + c.writeFrameMu.Lock(context.Background()) + putBufioWriter(c.bw) + } + c.msgWriter.close() + + if c.client { + c.readMu.Lock(context.Background()) + putBufioReader(c.br) + c.readMu.Unlock() + } + c.msgReader.close() }() } @@ -127,13 +188,12 @@ func (c *Conn) timeoutLoop() { case <-c.closed: return - case writeCtx = <-c.cw.timeout: - case readCtx = <-c.cr.timeout: + case writeCtx = <-c.writeTimeout: + case readCtx = <-c.readTimeout: case <-readCtx.Done(): c.setCloseErr(fmt.Errorf("read timed out: %w", readCtx.Err())) - c.cw.error(StatusPolicyViolation, errors.New("timed out")) - return + go c.writeError(StatusPolicyViolation, errors.New("timed out")) case <-writeCtx.Done(): c.closeWithErr(fmt.Errorf("write timed out: %w", writeCtx.Err())) return @@ -175,7 +235,7 @@ func (c *Conn) ping(ctx context.Context, p string) error { c.activePingsMu.Unlock() }() - err := c.cw.control(ctx, opPing, []byte(p)) + err := c.writeControl(ctx, opPing, []byte(p)) if err != nil { return err } diff --git a/conn_test.go b/conn_test.go index 6b8a778b..cf2334f7 100644 --- a/conn_test.go +++ b/conn_test.go @@ -25,6 +25,7 @@ func TestConn(t *testing.T) { c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ Subprotocols: []string{"echo"}, InsecureSkipVerify: true, + // CompressionMode: websocket.CompressionDisabled, }) assert.Success(t, err) defer c.Close(websocket.StatusInternalError, "") @@ -41,12 +42,12 @@ func TestConn(t *testing.T) { opts := &websocket.DialOptions{ Subprotocols: []string{"echo"}, + // CompressionMode: websocket.CompressionDisabled, } opts.HTTPClient = s.Client() c, _, err := websocket.Dial(ctx, wsURL, opts) assert.Success(t, err) - assertJSONEcho(t, ctx, c, 2) }) } diff --git a/internal/assert/assert.go b/internal/assert/assert.go index 4ebdb511..b448711a 100644 --- a/internal/assert/assert.go +++ b/internal/assert/assert.go @@ -23,7 +23,7 @@ func NotEqual(t testing.TB, exp, act interface{}, name string) { func Success(t testing.TB, err error) { t.Helper() if err != nil { - t.Fatalf("unexpected error : %+v", err) + t.Fatalf("unexpected error: %+v", err) } } diff --git a/read.go b/read.go index 7dba832a..d8691d65 100644 --- a/read.go +++ b/read.go @@ -1,7 +1,6 @@ package websocket import ( - "bufio" "context" "errors" "fmt" @@ -14,41 +13,22 @@ import ( "nhooyr.io/websocket/internal/errd" ) -// Reader waits until there is a WebSocket data message to read -// from the connection. -// It returns the type of the message and a reader to read it. +// Reader reads from the connection until until there is a WebSocket +// data message to be read. It will handle ping, pong and close frames as appropriate. +// +// It returns the type of the message and an io.Reader to read it. // The passed context will also bound the reader. // Ensure you read to EOF otherwise the connection will hang. // -// All returned errors will cause the connection -// to be closed so you do not need to write your own error message. -// This applies to the Read methods in the wsjson/wspb subpackages as well. -// -// You must read from the connection for control frames to be handled. -// Thus if you expect messages to take a long time to be responded to, -// you should handle such messages async to reading from the connection -// to ensure control frames are promptly handled. -// -// If you do not expect any data messages from the peer, call CloseRead. +// Call CloseRead if you do not expect any data messages from the peer. // // Only one Reader may be open at a time. -// -// If you need a separate timeout on the Reader call and then the message -// Read, use time.AfterFunc to cancel the context passed in early. -// See https://github.com/nhooyr/websocket/issues/87#issue-451703332 -// Most users should not need this. func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) { - typ, r, err := c.cr.reader(ctx) - if err != nil { - return 0, nil, fmt.Errorf("failed to get reader: %w", err) - } - return typ, r, nil + return c.reader(ctx) } -// Read is a convenience method to read a single message from the connection. -// -// See the Reader method to reuse buffers or for streaming. -// The docs on Reader apply to this method as well. +// Read is a convenience method around Reader to read a single message +// from the connection. func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { typ, r, err := c.Reader(ctx) if err != nil { @@ -59,14 +39,17 @@ func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { return typ, b, err } -// CloseRead will start a goroutine to read from the connection until it is closed or a data message -// is received. If a data message is received, the connection will be closed with StatusPolicyViolation. -// Since CloseRead reads from the connection, it will respond to ping, pong and close frames. -// After calling this method, you cannot read any data messages from the connection. +// CloseRead starts a goroutine to read from the connection until it is closed +// or a data message is received. +// +// Once CloseRead is called you cannot read any messages from the connection. // The returned context will be cancelled when the connection is closed. // -// Use this when you do not want to read data messages from the connection anymore but will -// want to write messages to it. +// If a data message is received, the connection will be closed with StatusPolicyViolation. +// +// Call CloseRead when you do not expect to read any more messages. +// Since it actively reads from the connection, it will ensure that ping, pong and close +// frames are responded to. func (c *Conn) CloseRead(ctx context.Context) context.Context { ctx, cancel := context.WithCancel(ctx) go func() { @@ -84,60 +67,32 @@ func (c *Conn) CloseRead(ctx context.Context) context.Context { // // When the limit is hit, the connection will be closed with StatusMessageTooBig. func (c *Conn) SetReadLimit(n int64) { - c.cr.mr.lr.limit.Store(n) -} - -type connReader struct { - c *Conn - br *bufio.Reader - timeout chan context.Context - - mu mu - controlPayloadBuf [maxControlPayload]byte - mr *msgReader -} - -func (cr *connReader) init(c *Conn, br *bufio.Reader) { - cr.c = c - cr.br = br - cr.timeout = make(chan context.Context) - - cr.mr = &msgReader{ - cr: cr, - fin: true, - } - - cr.mr.lr = newLimitReader(c, readerFunc(cr.mr.read), 32768) - if c.deflateNegotiated() && cr.contextTakeover() { - cr.ensureFlateReader() - } + c.msgReader.limitReader.setLimit(n) } -func (cr *connReader) ensureFlateReader() { - cr.mr.fr = getFlateReader(readerFunc(cr.mr.read)) - cr.mr.lr.reset(cr.mr.fr) +func (mr *msgReader) ensureFlateReader() { + mr.flateReader = getFlateReader(readerFunc(mr.read)) + mr.limitReader.reset(mr.flateReader) } -func (cr *connReader) close() { - cr.mu.Lock(context.Background()) - if cr.c.client { - putBufioReader(cr.br) - } - if cr.c.deflateNegotiated() && cr.contextTakeover() { - putFlateReader(cr.mr.fr) +func (mr *msgReader) close() { + if mr.c.deflateNegotiated() && mr.contextTakeover() { + mr.c.readMu.Lock(context.Background()) + putFlateReader(mr.flateReader) + mr.c.readMu.Unlock() } } -func (cr *connReader) contextTakeover() bool { - if cr.c.client { - return cr.c.copts.serverNoContextTakeover +func (mr *msgReader) contextTakeover() bool { + if mr.c.client { + return mr.c.copts.serverNoContextTakeover } - return cr.c.copts.clientNoContextTakeover + return mr.c.copts.clientNoContextTakeover } -func (cr *connReader) rsv1Illegal(h header) bool { +func (c *Conn) readRSV1Illegal(h header) bool { // If compression is enabled, rsv1 is always illegal. - if !cr.c.deflateNegotiated() { + if !c.deflateNegotiated() { return true } // rsv1 is only allowed on data frames beginning messages. @@ -147,26 +102,26 @@ func (cr *connReader) rsv1Illegal(h header) bool { return false } -func (cr *connReader) loop(ctx context.Context) (header, error) { +func (c *Conn) readLoop(ctx context.Context) (header, error) { for { - h, err := cr.frameHeader(ctx) + h, err := c.readFrameHeader(ctx) if err != nil { return header{}, err } - if h.rsv1 && cr.rsv1Illegal(h) || h.rsv2 || h.rsv3 { + if h.rsv1 && c.readRSV1Illegal(h) || h.rsv2 || h.rsv3 { err := fmt.Errorf("received header with unexpected rsv bits set: %v:%v:%v", h.rsv1, h.rsv2, h.rsv3) - cr.c.cw.error(StatusProtocolError, err) + c.writeError(StatusProtocolError, err) return header{}, err } - if !cr.c.client && !h.masked { + if !c.client && !h.masked { return header{}, errors.New("received unmasked frame from client") } switch h.opcode { case opClose, opPing, opPong: - err = cr.control(ctx, h) + err = c.handleControl(ctx, h) if err != nil { // Pass through CloseErrors when receiving a close frame. if h.opcode == opClose && CloseStatus(err) != -1 { @@ -178,95 +133,89 @@ func (cr *connReader) loop(ctx context.Context) (header, error) { return h, nil default: err := fmt.Errorf("received unknown opcode %v", h.opcode) - cr.c.cw.error(StatusProtocolError, err) + c.writeError(StatusProtocolError, err) return header{}, err } } } -func (cr *connReader) frameHeader(ctx context.Context) (header, error) { +func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { select { - case <-cr.c.closed: - return header{}, cr.c.closeErr - case cr.timeout <- ctx: + case <-c.closed: + return header{}, c.closeErr + case c.readTimeout <- ctx: } - h, err := readFrameHeader(cr.br) + h, err := readFrameHeader(c.br) if err != nil { select { - case <-cr.c.closed: - return header{}, cr.c.closeErr + case <-c.closed: + return header{}, c.closeErr case <-ctx.Done(): return header{}, ctx.Err() default: - cr.c.closeWithErr(err) + c.closeWithErr(err) return header{}, err } } select { - case <-cr.c.closed: - return header{}, cr.c.closeErr - case cr.timeout <- context.Background(): + case <-c.closed: + return header{}, c.closeErr + case c.readTimeout <- context.Background(): } return h, nil } -func (cr *connReader) framePayload(ctx context.Context, p []byte) (int, error) { +func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { select { - case <-cr.c.closed: - return 0, cr.c.closeErr - case cr.timeout <- ctx: + case <-c.closed: + return 0, c.closeErr + case c.readTimeout <- ctx: } - n, err := io.ReadFull(cr.br, p) + n, err := io.ReadFull(c.br, p) if err != nil { select { - case <-cr.c.closed: - return n, cr.c.closeErr + case <-c.closed: + return n, c.closeErr case <-ctx.Done(): return n, ctx.Err() default: err = fmt.Errorf("failed to read frame payload: %w", err) - cr.c.closeWithErr(err) + c.closeWithErr(err) return n, err } } select { - case <-cr.c.closed: - return n, cr.c.closeErr - case cr.timeout <- context.Background(): + case <-c.closed: + return n, c.closeErr + case c.readTimeout <- context.Background(): } return n, err } -func (cr *connReader) control(ctx context.Context, h header) error { - if h.payloadLength < 0 { - err := fmt.Errorf("received header with negative payload length: %v", h.payloadLength) - cr.c.cw.error(StatusProtocolError, err) - return err - } - - if h.payloadLength > maxControlPayload { - err := fmt.Errorf("received too big control frame at %v bytes", h.payloadLength) - cr.c.cw.error(StatusProtocolError, err) +func (c *Conn) handleControl(ctx context.Context, h header) error { + if h.payloadLength < 0 || h.payloadLength > maxControlPayload { + err := fmt.Errorf("received control frame payload with invalid length: %d", h.payloadLength) + c.writeError(StatusProtocolError, err) return err } if !h.fin { err := errors.New("received fragmented control frame") - cr.c.cw.error(StatusProtocolError, err) + c.writeError(StatusProtocolError, err) return err } ctx, cancel := context.WithTimeout(ctx, time.Second*5) defer cancel() - b := cr.controlPayloadBuf[:h.payloadLength] - _, err := cr.framePayload(ctx, b) + b := c.readControlBuf[:h.payloadLength] + _, err := c.readFramePayload(ctx, b) if err != nil { return err } @@ -277,11 +226,11 @@ func (cr *connReader) control(ctx context.Context, h header) error { switch h.opcode { case opPing: - return cr.c.cw.control(ctx, opPong, b) + return c.writeControl(ctx, opPong, b) case opPong: - cr.c.activePingsMu.Lock() - pong, ok := cr.c.activePings[string(b)] - cr.c.activePingsMu.Unlock() + c.activePingsMu.Lock() + pong, ok := c.activePings[string(b)] + c.activePingsMu.Unlock() if ok { close(pong) } @@ -291,53 +240,56 @@ func (cr *connReader) control(ctx context.Context, h header) error { ce, err := parseClosePayload(b) if err != nil { err = fmt.Errorf("received invalid close payload: %w", err) - cr.c.cw.error(StatusProtocolError, err) + c.writeError(StatusProtocolError, err) return err } err = fmt.Errorf("received close frame: %w", ce) - cr.c.setCloseErr(err) - cr.c.cw.control(context.Background(), opClose, ce.bytes()) + c.setCloseErr(err) + c.writeControl(context.Background(), opClose, ce.bytes()) return err } -func (cr *connReader) reader(ctx context.Context) (MessageType, io.Reader, error) { - err := cr.mu.Lock(ctx) +func (c *Conn) reader(ctx context.Context) (_ MessageType, _ io.Reader, err error) { + defer errd.Wrap(&err, "failed to get reader") + + err = c.readMu.Lock(ctx) if err != nil { return 0, nil, err } - defer cr.mu.Unlock() + defer c.readMu.Unlock() - if !cr.mr.fin { + if !c.msgReader.fin { return 0, nil, errors.New("previous message not read to completion") } - h, err := cr.loop(ctx) + h, err := c.readLoop(ctx) if err != nil { return 0, nil, err } if h.opcode == opContinuation { err := errors.New("received continuation frame without text or binary frame") - cr.c.cw.error(StatusProtocolError, err) + c.writeError(StatusProtocolError, err) return 0, nil, err } - cr.mr.reset(ctx, h) + c.msgReader.reset(ctx, h) - return MessageType(h.opcode), cr.mr, nil + return MessageType(h.opcode), c.msgReader, nil } type msgReader struct { - cr *connReader - fr io.Reader - lr *limitReader + c *Conn ctx context.Context deflate bool + flateReader io.Reader deflateTail strings.Reader + limitReader *limitReader + payloadLength int64 maskKey uint32 fin bool @@ -348,8 +300,8 @@ func (mr *msgReader) reset(ctx context.Context, h header) { mr.deflate = h.rsv1 if mr.deflate { mr.deflateTail.Reset(deflateMessageTail) - if !mr.cr.contextTakeover() { - mr.cr.ensureFlateReader() + if !mr.contextTakeover() { + mr.ensureFlateReader() } } mr.setFrame(h) @@ -370,34 +322,42 @@ func (mr *msgReader) Read(p []byte) (_ int, err error) { } }() - err = mr.cr.mu.Lock(mr.ctx) + err = mr.c.readMu.Lock(mr.ctx) if err != nil { return 0, err } - defer mr.cr.mu.Unlock() + defer mr.c.readMu.Unlock() if mr.payloadLength == 0 && mr.fin { - if mr.cr.c.deflateNegotiated() && !mr.cr.contextTakeover() { - if mr.fr != nil { - putFlateReader(mr.fr) - mr.fr = nil + if mr.c.deflateNegotiated() && !mr.contextTakeover() { + if mr.flateReader != nil { + putFlateReader(mr.flateReader) + mr.flateReader = nil } } return 0, io.EOF } - return mr.lr.Read(p) + return mr.limitReader.Read(p) } func (mr *msgReader) read(p []byte) (int, error) { if mr.payloadLength == 0 { - h, err := mr.cr.loop(mr.ctx) + if mr.fin { + if mr.deflate { + n, _ := mr.deflateTail.Read(p[:4]) + return n, nil + } + return 0, io.EOF + } + + h, err := mr.c.readLoop(mr.ctx) if err != nil { return 0, err } if h.opcode != opContinuation { err := errors.New("received new data message without finishing the previous message") - mr.cr.c.cw.error(StatusProtocolError, err) + mr.c.writeError(StatusProtocolError, err) return 0, err } mr.setFrame(h) @@ -407,14 +367,14 @@ func (mr *msgReader) read(p []byte) (int, error) { p = p[:mr.payloadLength] } - n, err := mr.cr.framePayload(mr.ctx, p) + n, err := mr.c.readFramePayload(mr.ctx, p) if err != nil { return n, err } mr.payloadLength -= int64(n) - if !mr.cr.c.client { + if !mr.c.client { mr.maskKey = mask(mr.maskKey, p) } @@ -442,10 +402,14 @@ func (lr *limitReader) reset(r io.Reader) { lr.r = r } +func (lr *limitReader) setLimit(limit int64) { + lr.limit.Store(limit) +} + func (lr *limitReader) Read(p []byte) (int, error) { if lr.n <= 0 { err := fmt.Errorf("read limited at %v bytes", lr.limit.Load()) - lr.c.cw.error(StatusMessageTooBig, err) + lr.c.writeError(StatusMessageTooBig, err) return 0, err } diff --git a/write.go b/write.go index 9cafc5c5..0ddf11e1 100644 --- a/write.go +++ b/write.go @@ -24,7 +24,7 @@ import ( // // Never close the returned writer twice. func (c *Conn) Writer(ctx context.Context, typ MessageType) (io.WriteCloser, error) { - w, err := c.cw.writer(ctx, typ) + w, err := c.writer(ctx, typ) if err != nil { return nil, fmt.Errorf("failed to get writer: %w", err) } @@ -38,111 +38,68 @@ func (c *Conn) Writer(ctx context.Context, typ MessageType) (io.WriteCloser, err // If compression is disabled, then it is guaranteed to write the message // in a single frame. func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { - _, err := c.cw.write(ctx, typ, p) + _, err := c.write(ctx, typ, p) if err != nil { return fmt.Errorf("failed to write msg: %w", err) } return nil } -type connWriter struct { - c *Conn - bw *bufio.Writer - - writeBuf []byte - - mw *messageWriter - frameMu mu - h header - - timeout chan context.Context +func (mw *msgWriter) ensureFlateWriter() { + mw.flateWriter = getFlateWriter(mw.trimWriter) } -func (cw *connWriter) init(c *Conn, bw *bufio.Writer) { - cw.c = c - cw.bw = bw - - if cw.c.client { - cw.writeBuf = extractBufioWriterBuf(cw.bw, c.rwc) - } - - cw.timeout = make(chan context.Context) - - cw.mw = &messageWriter{ - cw: cw, +func (mw *msgWriter) contextTakeover() bool { + if mw.c.client { + return mw.c.copts.clientNoContextTakeover } - cw.mw.tw = &trimLastFourBytesWriter{ - w: writerFunc(cw.mw.write), - } - if cw.c.deflateNegotiated() && cw.mw.contextTakeover() { - cw.mw.ensureFlateWriter() - } -} - -func (mw *messageWriter) ensureFlateWriter() { - mw.fw = getFlateWriter(mw.tw) + return mw.c.copts.serverNoContextTakeover } -func (cw *connWriter) close() { - if cw.c.client { - cw.frameMu.Lock(context.Background()) - putBufioWriter(cw.bw) - } - if cw.c.deflateNegotiated() && cw.mw.contextTakeover() { - cw.mw.mu.Lock(context.Background()) - putFlateWriter(cw.mw.fw) - } -} - -func (mw *messageWriter) contextTakeover() bool { - if mw.cw.c.client { - return mw.cw.c.copts.clientNoContextTakeover - } - return mw.cw.c.copts.serverNoContextTakeover -} - -func (cw *connWriter) writer(ctx context.Context, typ MessageType) (io.WriteCloser, error) { - err := cw.mw.reset(ctx, typ) +func (c *Conn) writer(ctx context.Context, typ MessageType) (io.WriteCloser, error) { + err := c.msgWriter.reset(ctx, typ) if err != nil { return nil, err } - return cw.mw, nil + return c.msgWriter, nil } -func (cw *connWriter) write(ctx context.Context, typ MessageType, p []byte) (int, error) { - ww, err := cw.writer(ctx, typ) +func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error) { + mw, err := c.writer(ctx, typ) if err != nil { return 0, err } - if !cw.c.deflateNegotiated() { + if !c.deflateNegotiated() { // Fast single frame path. - defer cw.mw.mu.Unlock() - return cw.frame(ctx, true, cw.mw.opcode, p) + defer c.msgWriter.mu.Unlock() + return c.writeFrame(ctx, true, c.msgWriter.opcode, p) } - n, err := ww.Write(p) + n, err := mw.Write(p) if err != nil { return n, err } - err = ww.Close() + err = mw.Close() return n, err } -type messageWriter struct { - cw *connWriter +type msgWriter struct { + c *Conn - mu mu - compress bool - tw *trimLastFourBytesWriter - fw *flate.Writer - ctx context.Context - opcode opcode - closed bool + mu mu + + deflate bool + ctx context.Context + opcode opcode + closed bool + + trimWriter *trimLastFourBytesWriter + flateWriter *flate.Writer } -func (mw *messageWriter) reset(ctx context.Context, typ MessageType) error { +func (mw *msgWriter) reset(ctx context.Context, typ MessageType) error { err := mw.mu.Lock(ctx) if err != nil { return err @@ -155,30 +112,30 @@ func (mw *messageWriter) reset(ctx context.Context, typ MessageType) error { } // Write writes the given bytes to the WebSocket connection. -func (mw *messageWriter) Write(p []byte) (_ int, err error) { +func (mw *msgWriter) Write(p []byte) (_ int, err error) { defer errd.Wrap(&err, "failed to write") if mw.closed { return 0, errors.New("cannot use closed writer") } - if mw.cw.c.deflateNegotiated() { - if !mw.compress { + if mw.c.deflateNegotiated() { + if !mw.deflate { if !mw.contextTakeover() { mw.ensureFlateWriter() } - mw.tw.reset() - mw.compress = true + mw.trimWriter.reset() + mw.deflate = true } - return mw.fw.Write(p) + return mw.flateWriter.Write(p) } return mw.write(p) } -func (mw *messageWriter) write(p []byte) (int, error) { - n, err := mw.cw.frame(mw.ctx, false, mw.opcode, p) +func (mw *msgWriter) write(p []byte) (int, error) { + n, err := mw.c.writeFrame(mw.ctx, false, mw.opcode, p) if err != nil { return n, fmt.Errorf("failed to write data frame: %w", err) } @@ -187,8 +144,7 @@ func (mw *messageWriter) write(p []byte) (int, error) { } // Close flushes the frame to the connection. -// This must be called for every messageWriter. -func (mw *messageWriter) Close() (err error) { +func (mw *msgWriter) Close() (err error) { defer errd.Wrap(&err, "failed to close writer") if mw.closed { @@ -196,32 +152,39 @@ func (mw *messageWriter) Close() (err error) { } mw.closed = true - if mw.cw.c.deflateNegotiated() { - err = mw.fw.Flush() + if mw.c.deflateNegotiated() { + err = mw.flateWriter.Flush() if err != nil { return fmt.Errorf("failed to flush flate writer: %w", err) } } - _, err = mw.cw.frame(mw.ctx, true, mw.opcode, nil) + _, err = mw.c.writeFrame(mw.ctx, true, mw.opcode, nil) if err != nil { return fmt.Errorf("failed to write fin frame: %w", err) } - if mw.compress && !mw.contextTakeover() { - putFlateWriter(mw.fw) - mw.compress = false + if mw.deflate && !mw.contextTakeover() { + putFlateWriter(mw.flateWriter) + mw.deflate = false } mw.mu.Unlock() return nil } -func (cw *connWriter) control(ctx context.Context, opcode opcode, p []byte) error { +func (cw *msgWriter) close() { + if cw.c.deflateNegotiated() && cw.contextTakeover() { + cw.mu.Lock(context.Background()) + putFlateWriter(cw.flateWriter) + } +} + +func (c *Conn) writeControl(ctx context.Context, opcode opcode, p []byte) error { ctx, cancel := context.WithTimeout(ctx, time.Second*5) defer cancel() - _, err := cw.frame(ctx, true, opcode, p) + _, err := c.writeFrame(ctx, true, opcode, p) if err != nil { return fmt.Errorf("failed to write control frame %v: %w", opcode, err) } @@ -229,94 +192,94 @@ func (cw *connWriter) control(ctx context.Context, opcode opcode, p []byte) erro } // frame handles all writes to the connection. -func (cw *connWriter) frame(ctx context.Context, fin bool, opcode opcode, p []byte) (int, error) { - err := cw.frameMu.Lock(ctx) +func (c *Conn) writeFrame(ctx context.Context, fin bool, opcode opcode, p []byte) (int, error) { + err := c.writeFrameMu.Lock(ctx) if err != nil { return 0, err } - defer cw.frameMu.Unlock() + defer c.writeFrameMu.Unlock() select { - case <-cw.c.closed: - return 0, cw.c.closeErr - case cw.timeout <- ctx: + case <-c.closed: + return 0, c.closeErr + case c.writeTimeout <- ctx: } - cw.h.fin = fin - cw.h.opcode = opcode - cw.h.masked = cw.c.client - cw.h.payloadLength = int64(len(p)) - - cw.h.rsv1 = false - if cw.mw.compress && (opcode == opText || opcode == opBinary) { - cw.h.rsv1 = true - } + c.writeHeader.fin = fin + c.writeHeader.opcode = opcode + c.writeHeader.payloadLength = int64(len(p)) - if cw.h.masked { - err = binary.Read(rand.Reader, binary.LittleEndian, &cw.h.maskKey) + if c.client { + c.writeHeader.masked = true + err = binary.Read(rand.Reader, binary.LittleEndian, &c.writeHeader.maskKey) if err != nil { return 0, fmt.Errorf("failed to generate masking key: %w", err) } } - err = writeFrameHeader(cw.h, cw.bw) + c.writeHeader.rsv1 = false + if c.msgWriter.deflate && (opcode == opText || opcode == opBinary) { + c.writeHeader.rsv1 = true + } + + err = writeFrameHeader(c.writeHeader, c.bw) if err != nil { return 0, err } - n, err := cw.framePayload(p) + n, err := c.writeFramePayload(p) if err != nil { return n, err } - if cw.h.fin { - err = cw.bw.Flush() + if c.writeHeader.fin { + err = c.bw.Flush() if err != nil { return n, fmt.Errorf("failed to flush: %w", err) } } select { - case <-cw.c.closed: - return n, cw.c.closeErr - case cw.timeout <- context.Background(): + case <-c.closed: + return n, c.closeErr + case c.writeTimeout <- context.Background(): } return n, nil } -func (cw *connWriter) framePayload(p []byte) (_ int, err error) { +func (c *Conn) writeFramePayload(p []byte) (_ int, err error) { defer errd.Wrap(&err, "failed to write frame payload") - if !cw.h.masked { - return cw.bw.Write(p) + if !c.writeHeader.masked { + return c.bw.Write(p) } var n int - maskKey := cw.h.maskKey + maskKey := c.writeHeader.maskKey for len(p) > 0 { // If the buffer is full, we need to flush. - if cw.bw.Available() == 0 { - err = cw.bw.Flush() + if c.bw.Available() == 0 { + err = c.bw.Flush() if err != nil { return n, err } } // Start of next write in the buffer. - i := cw.bw.Buffered() + i := c.bw.Buffered() j := len(p) - if j > cw.bw.Available() { - j = cw.bw.Available() + if j > c.bw.Available() { + j = c.bw.Available() } - _, err := cw.bw.Write(p[:j]) + _, err := c.bw.Write(p[:j]) if err != nil { return n, err } - maskKey = mask(maskKey, cw.writeBuf[i:cw.bw.Buffered()]) + maskKey = mask(maskKey, c.writeBuf[i:c.bw.Buffered()]) p = p[j:] n += j diff --git a/wsjson/wsjson.go b/wsjson/wsjson.go index 99996a69..36dd2dfd 100644 --- a/wsjson/wsjson.go +++ b/wsjson/wsjson.go @@ -5,7 +5,6 @@ import ( "context" "encoding/json" "fmt" - "nhooyr.io/websocket" "nhooyr.io/websocket/internal/bpool" "nhooyr.io/websocket/internal/errd" From e8dfe270f06873c243fd98f2f19303093d5af85a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 29 Nov 2019 13:17:04 -0500 Subject: [PATCH 230/519] Make CI pass --- accept.go | 2 + accept_test.go | 2 + assert_test.go | 5 ++- autobahn_test.go | 2 + ci/lint.mk | 2 +- close.go | 58 +++++++++++++++++----------- close_test.go | 8 ++-- compress.go | 24 ++++++------ conn.go | 80 ++++++++++++++------------------------- conn_test.go | 6 +-- dial.go | 2 + frame.go | 2 + go.mod | 6 --- go.sum | 24 ------------ internal/assert/assert.go | 6 +++ read.go | 40 +++++++++++++++----- write.go | 44 ++++++++++++++------- ws_js.go | 19 ++++++++-- ws_js_test.go | 2 +- wsjson/wsjson.go | 1 + 20 files changed, 183 insertions(+), 152 deletions(-) diff --git a/accept.go b/accept.go index 964e0401..ea7beebd 100644 --- a/accept.go +++ b/accept.go @@ -1,3 +1,5 @@ +// +build !js + package websocket import ( diff --git a/accept_test.go b/accept_test.go index d68d4d6d..551fe4de 100644 --- a/accept_test.go +++ b/accept_test.go @@ -1,3 +1,5 @@ +// +build !js + package websocket import ( diff --git a/assert_test.go b/assert_test.go index e4319938..dd4c30cd 100644 --- a/assert_test.go +++ b/assert_test.go @@ -4,11 +4,12 @@ import ( "context" "crypto/rand" "io" + "strings" + "testing" + "nhooyr.io/websocket" "nhooyr.io/websocket/internal/assert" "nhooyr.io/websocket/wsjson" - "strings" - "testing" ) func randBytes(t *testing.T, n int) []byte { diff --git a/autobahn_test.go b/autobahn_test.go index 30c96a7c..6b3b5b72 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -1,3 +1,5 @@ +// +build !js + package websocket_test import ( diff --git a/ci/lint.mk b/ci/lint.mk index a656ea8d..031f0de3 100644 --- a/ci/lint.mk +++ b/ci/lint.mk @@ -1,4 +1,4 @@ -lint: govet golint govet-wasm golint-wasm +lint: govet golint govet: go vet ./... diff --git a/close.go b/close.go index 4c474b78..af437553 100644 --- a/close.go +++ b/close.go @@ -1,3 +1,5 @@ +// +build !js + package websocket import ( @@ -6,6 +8,8 @@ import ( "errors" "fmt" "log" + "time" + "nhooyr.io/websocket/internal/errd" ) @@ -99,19 +103,24 @@ func (c *Conn) Close(code StatusCode, reason string) error { func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { defer errd.Wrap(&err, "failed to close WebSocket") + defer c.close(nil) err = c.writeClose(code, reason) if err != nil { return err } - return c.waitClose() + err = c.waitCloseHandshake() + if CloseStatus(err) == -1 { + return err + } + return nil } func (c *Conn) writeError(code StatusCode, err error) { c.setCloseErr(err) c.writeClose(code, err.Error()) - c.closeWithErr(nil) + c.close(nil) } func (c *Conn) writeClose(code StatusCode, reason string) error { @@ -130,28 +139,33 @@ func (c *Conn) writeClose(code StatusCode, reason string) error { return c.writeControl(context.Background(), opClose, p) } -func (c *Conn) waitClose() error { - defer c.closeWithErr(nil) +func (c *Conn) waitCloseHandshake() error { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() - return nil + err := c.readMu.Lock(ctx) + if err != nil { + return err + } + defer c.readMu.Unlock() + + if c.readCloseFrameErr != nil { + return c.readCloseFrameErr + } - // ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - // defer cancel() - // - // err := cr.mu.Lock(ctx) - // if err != nil { - // return err - // } - // defer cr.mu.Unlock() - // - // b := bpool.Get() - // buf := b.Bytes() - // buf = buf[:cap(buf)] - // defer bpool.Put(b) - // - // for { - // return nil - // } + for { + h, err := c.readLoop(ctx) + if err != nil { + return err + } + + for i := int64(0); i < h.payloadLength; i++ { + _, err := c.br.ReadByte() + if err != nil { + return err + } + } + } } func parseClosePayload(p []byte) (CloseError, error) { diff --git a/close_test.go b/close_test.go index 9551699a..a2e0f67d 100644 --- a/close_test.go +++ b/close_test.go @@ -1,3 +1,5 @@ +// +build !js + package websocket import ( @@ -49,7 +51,7 @@ func TestCloseError(t *testing.T) { t.Parallel() _, err := tc.ce.bytesErr() - if (tc.success) { + if tc.success { assert.Success(t, err) } else { assert.Error(t, err) @@ -101,7 +103,7 @@ func Test_parseClosePayload(t *testing.T) { t.Parallel() ce, err := parseClosePayload(tc.p) - if (tc.success) { + if tc.success { assert.Success(t, err) assert.Equal(t, tc.ce, ce, "CloseError") } else { @@ -151,7 +153,7 @@ func Test_validWireCloseCode(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - assert.Equal(t, tc.code, validWireCloseCode(tc.code), "validWireCloseCode") + assert.Equal(t, tc.valid, validWireCloseCode(tc.code), "validWireCloseCode") }) } } diff --git a/compress.go b/compress.go index 9e075430..2410cb4e 100644 --- a/compress.go +++ b/compress.go @@ -19,16 +19,6 @@ import ( type CompressionMode int const ( - // CompressionContextTakeover uses a flate.Reader and flate.Writer per connection. - // This enables reusing the sliding window from previous messages. - // As most WebSocket protocols are repetitive, this is the default. - // - // The message will only be compressed if greater than or equal to 128 bytes. - // - // If the peer negotiates NoContextTakeover on the client or server side, it will be - // used instead as this is required by the RFC. - CompressionContextTakeover CompressionMode = iota - // CompressionNoContextTakeover grabs a new flate.Reader and flate.Writer as needed // for every message. This applies to both server and client side. // @@ -36,8 +26,18 @@ const ( // will not be used but the memory overhead will be much lower if the connections // are long lived and seldom used. // - // The message will only be compressed if greater than or equal to 512 bytes. - CompressionNoContextTakeover + // The message will only be compressed if greater than 512 bytes. + CompressionNoContextTakeover CompressionMode = iota + + // CompressionContextTakeover uses a flate.Reader and flate.Writer per connection. + // This enables reusing the sliding window from previous messages. + // As most WebSocket protocols are repetitive, this can be very efficient. + // + // The message will only be compressed if greater than 128 bytes. + // + // If the peer negotiates NoContextTakeover on the client or server side, it will be + // used instead as this is required by the RFC. + CompressionContextTakeover // CompressionDisabled disables the deflate extension. // diff --git a/conn.go b/conn.go index dc067d18..10fe2e1a 100644 --- a/conn.go +++ b/conn.go @@ -49,21 +49,21 @@ type Conn struct { writeTimeout chan context.Context // Read state. - readMu mu - readControlBuf [maxControlPayload]byte - msgReader *msgReader + readMu *mu + readControlBuf [maxControlPayload]byte + msgReader *msgReader + readCloseFrameErr error // Write state. msgWriter *msgWriter - writeFrameMu mu + writeFrameMu *mu writeBuf []byte writeHeader header - closed chan struct{} - - closeMu sync.Mutex - closeErr error - closeHandshakeErr error + closed chan struct{} + closeMu sync.Mutex + closeErr error + wroteClose int64 pingCounter int32 activePingsMu sync.Mutex @@ -90,13 +90,16 @@ func newConn(cfg connConfig) *Conn { br: cfg.br, bw: cfg.bw, - readTimeout: make(chan context.Context), + readTimeout: make(chan context.Context), writeTimeout: make(chan context.Context), - closed: make(chan struct{}), + closed: make(chan struct{}), activePings: make(map[string]chan<- struct{}), } + c.readMu = newMu(c) + c.writeFrameMu = newMu(c) + c.msgReader = newMsgReader(c) c.msgWriter = newMsgWriter(c) @@ -105,7 +108,7 @@ func newConn(cfg connConfig) *Conn { } runtime.SetFinalizer(c, func(c *Conn) { - c.closeWithErr(errors.New("connection garbage collected")) + c.close(errors.New("connection garbage collected")) }) go c.timeoutLoop() @@ -113,41 +116,13 @@ func newConn(cfg connConfig) *Conn { return c } -func newMsgReader(c *Conn) *msgReader { - mr := &msgReader{ - c: c, - fin: true, - } - - mr.limitReader = newLimitReader(c, readerFunc(mr.read), 32768) - if c.deflateNegotiated() && mr.contextTakeover() { - mr.ensureFlateReader() - } - - return mr -} - -func newMsgWriter(c *Conn) *msgWriter { - mw := &msgWriter{ - c: c, - } - mw.trimWriter = &trimLastFourBytesWriter{ - w: writerFunc(mw.write), - } - if c.deflateNegotiated() && mw.contextTakeover() { - mw.ensureFlateWriter() - } - - return mw -} - // Subprotocol returns the negotiated subprotocol. // An empty string means the default protocol. func (c *Conn) Subprotocol() string { return c.subprotocol } -func (c *Conn) closeWithErr(err error) { +func (c *Conn) close(err error) { c.closeMu.Lock() defer c.closeMu.Unlock() @@ -195,13 +170,13 @@ func (c *Conn) timeoutLoop() { c.setCloseErr(fmt.Errorf("read timed out: %w", readCtx.Err())) go c.writeError(StatusPolicyViolation, errors.New("timed out")) case <-writeCtx.Done(): - c.closeWithErr(fmt.Errorf("write timed out: %w", writeCtx.Err())) + c.close(fmt.Errorf("write timed out: %w", writeCtx.Err())) return } } } -func (c *Conn) deflateNegotiated() bool { +func (c *Conn) deflate() bool { return c.copts != nil } @@ -245,7 +220,7 @@ func (c *Conn) ping(ctx context.Context, p string) error { return c.closeErr case <-ctx.Done(): err := fmt.Errorf("failed to wait for pong: %w", ctx.Err()) - c.closeWithErr(err) + c.close(err) return err case <-pong: return nil @@ -253,19 +228,21 @@ func (c *Conn) ping(ctx context.Context, p string) error { } type mu struct { - once sync.Once - ch chan struct{} + c *Conn + ch chan struct{} } -func (m *mu) init() { - m.once.Do(func() { - m.ch = make(chan struct{}, 1) - }) +func newMu(c *Conn) *mu { + return &mu{ + c: c, + ch: make(chan struct{}, 1), + } } func (m *mu) Lock(ctx context.Context) error { - m.init() select { + case <-m.c.closed: + return m.c.closeErr case <-ctx.Done(): return ctx.Err() case m.ch <- struct{}{}: @@ -274,7 +251,6 @@ func (m *mu) Lock(ctx context.Context) error { } func (m *mu) TryLock() bool { - m.init() select { case m.ch <- struct{}{}: return true diff --git a/conn_test.go b/conn_test.go index cf2334f7..9b628cfe 100644 --- a/conn_test.go +++ b/conn_test.go @@ -25,7 +25,7 @@ func TestConn(t *testing.T) { c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ Subprotocols: []string{"echo"}, InsecureSkipVerify: true, - // CompressionMode: websocket.CompressionDisabled, + CompressionMode: websocket.CompressionNoContextTakeover, }) assert.Success(t, err) defer c.Close(websocket.StatusInternalError, "") @@ -41,8 +41,8 @@ func TestConn(t *testing.T) { defer cancel() opts := &websocket.DialOptions{ - Subprotocols: []string{"echo"}, - // CompressionMode: websocket.CompressionDisabled, + Subprotocols: []string{"echo"}, + CompressionMode: websocket.CompressionNoContextTakeover, } opts.HTTPClient = s.Client() diff --git a/dial.go b/dial.go index a1a10556..6cde30e7 100644 --- a/dial.go +++ b/dial.go @@ -1,3 +1,5 @@ +// +build !js + package websocket import ( diff --git a/frame.go b/frame.go index 0257835e..47ff40f7 100644 --- a/frame.go +++ b/frame.go @@ -1,3 +1,5 @@ +// +build !js + package websocket import ( diff --git a/go.mod b/go.mod index 1a2b08f4..6cd368b4 100644 --- a/go.mod +++ b/go.mod @@ -3,17 +3,11 @@ module nhooyr.io/websocket go 1.13 require ( - github.com/davecgh/go-spew v1.1.1 // indirect github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee // indirect github.com/gobwas/pool v0.2.0 // indirect github.com/gobwas/ws v1.0.2 github.com/golang/protobuf v1.3.2 github.com/google/go-cmp v0.3.1 github.com/gorilla/websocket v1.4.1 - github.com/kr/pretty v0.1.0 // indirect - github.com/stretchr/testify v1.4.0 // indirect - go.uber.org/atomic v1.4.0 // indirect - go.uber.org/multierr v1.1.0 // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 - gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect ) diff --git a/go.sum b/go.sum index d2f1f0e4..c639eb64 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,3 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= @@ -14,25 +10,5 @@ github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -go.uber.org/atomic v1.4.0 h1:cxzIVoETapQEqDhQu3QfnvXAV4AlzcvUCxkVUFw3+EU= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/internal/assert/assert.go b/internal/assert/assert.go index b448711a..b20d9420 100644 --- a/internal/assert/assert.go +++ b/internal/assert/assert.go @@ -1,3 +1,4 @@ +// Package assert contains helpers for test assertions. package assert import ( @@ -5,6 +6,7 @@ import ( "testing" ) +// Equal asserts exp == act. func Equal(t testing.TB, exp, act interface{}, name string) { t.Helper() diff := cmpDiff(exp, act) @@ -13,6 +15,7 @@ func Equal(t testing.TB, exp, act interface{}, name string) { } } +// NotEqual asserts exp != act. func NotEqual(t testing.TB, exp, act interface{}, name string) { t.Helper() if cmpDiff(exp, act) == "" { @@ -20,6 +23,7 @@ func NotEqual(t testing.TB, exp, act interface{}, name string) { } } +// Success asserts exp == nil. func Success(t testing.TB, err error) { t.Helper() if err != nil { @@ -27,6 +31,7 @@ func Success(t testing.TB, err error) { } } +// Error asserts exp != nil. func Error(t testing.TB, err error) { t.Helper() if err == nil { @@ -34,6 +39,7 @@ func Error(t testing.TB, err error) { } } +// ErrorContains asserts the error string from err contains sub. func ErrorContains(t testing.TB, err error, sub string) { t.Helper() Error(t, err) diff --git a/read.go b/read.go index d8691d65..c72b6c17 100644 --- a/read.go +++ b/read.go @@ -1,3 +1,5 @@ +// +build !js + package websocket import ( @@ -70,13 +72,27 @@ func (c *Conn) SetReadLimit(n int64) { c.msgReader.limitReader.setLimit(n) } -func (mr *msgReader) ensureFlateReader() { +func newMsgReader(c *Conn) *msgReader { + mr := &msgReader{ + c: c, + fin: true, + } + + mr.limitReader = newLimitReader(c, readerFunc(mr.read), 32768) + if c.deflate() && mr.contextTakeover() { + mr.initFlateReader() + } + + return mr +} + +func (mr *msgReader) initFlateReader() { mr.flateReader = getFlateReader(readerFunc(mr.read)) mr.limitReader.reset(mr.flateReader) } func (mr *msgReader) close() { - if mr.c.deflateNegotiated() && mr.contextTakeover() { + if mr.c.deflate() && mr.contextTakeover() { mr.c.readMu.Lock(context.Background()) putFlateReader(mr.flateReader) mr.c.readMu.Unlock() @@ -92,7 +108,7 @@ func (mr *msgReader) contextTakeover() bool { func (c *Conn) readRSV1Illegal(h header) bool { // If compression is enabled, rsv1 is always illegal. - if !c.deflateNegotiated() { + if !c.deflate() { return true } // rsv1 is only allowed on data frames beginning messages. @@ -154,7 +170,7 @@ func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { case <-ctx.Done(): return header{}, ctx.Err() default: - c.closeWithErr(err) + c.close(err) return header{}, err } } @@ -184,7 +200,7 @@ func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { return n, ctx.Err() default: err = fmt.Errorf("failed to read frame payload: %w", err) - c.closeWithErr(err) + c.close(err) return n, err } } @@ -198,7 +214,7 @@ func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { return n, err } -func (c *Conn) handleControl(ctx context.Context, h header) error { +func (c *Conn) handleControl(ctx context.Context, h header) (err error) { if h.payloadLength < 0 || h.payloadLength > maxControlPayload { err := fmt.Errorf("received control frame payload with invalid length: %d", h.payloadLength) c.writeError(StatusProtocolError, err) @@ -215,7 +231,7 @@ func (c *Conn) handleControl(ctx context.Context, h header) error { defer cancel() b := c.readControlBuf[:h.payloadLength] - _, err := c.readFramePayload(ctx, b) + _, err = c.readFramePayload(ctx, b) if err != nil { return err } @@ -237,6 +253,10 @@ func (c *Conn) handleControl(ctx context.Context, h header) error { return nil } + defer func() { + c.readCloseFrameErr = err + }() + ce, err := parseClosePayload(b) if err != nil { err = fmt.Errorf("received invalid close payload: %w", err) @@ -301,7 +321,7 @@ func (mr *msgReader) reset(ctx context.Context, h header) { if mr.deflate { mr.deflateTail.Reset(deflateMessageTail) if !mr.contextTakeover() { - mr.ensureFlateReader() + mr.initFlateReader() } } mr.setFrame(h) @@ -329,7 +349,7 @@ func (mr *msgReader) Read(p []byte) (_ int, err error) { defer mr.c.readMu.Unlock() if mr.payloadLength == 0 && mr.fin { - if mr.c.deflateNegotiated() && !mr.contextTakeover() { + if mr.c.deflate() && !mr.contextTakeover() { if mr.flateReader != nil { putFlateReader(mr.flateReader) mr.flateReader = nil @@ -345,7 +365,7 @@ func (mr *msgReader) read(p []byte) (int, error) { if mr.payloadLength == 0 { if mr.fin { if mr.deflate { - n, _ := mr.deflateTail.Read(p[:4]) + n, _ := mr.deflateTail.Read(p) return n, nil } return 0, io.EOF diff --git a/write.go b/write.go index 0ddf11e1..526b3b66 100644 --- a/write.go +++ b/write.go @@ -1,3 +1,5 @@ +// +build !js + package websocket import ( @@ -45,11 +47,26 @@ func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { return nil } +func newMsgWriter(c *Conn) *msgWriter { + mw := &msgWriter{ + c: c, + mu: newMu(c), + } + mw.trimWriter = &trimLastFourBytesWriter{ + w: writerFunc(mw.write), + } + if c.deflate() && mw.deflateContextTakeover() { + mw.ensureFlateWriter() + } + + return mw +} + func (mw *msgWriter) ensureFlateWriter() { mw.flateWriter = getFlateWriter(mw.trimWriter) } -func (mw *msgWriter) contextTakeover() bool { +func (mw *msgWriter) deflateContextTakeover() bool { if mw.c.client { return mw.c.copts.clientNoContextTakeover } @@ -70,7 +87,7 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error return 0, err } - if !c.deflateNegotiated() { + if !c.deflate() { // Fast single frame path. defer c.msgWriter.mu.Unlock() return c.writeFrame(ctx, true, c.msgWriter.opcode, p) @@ -88,15 +105,15 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error type msgWriter struct { c *Conn - mu mu + mu *mu deflate bool ctx context.Context opcode opcode closed bool - trimWriter *trimLastFourBytesWriter - flateWriter *flate.Writer + trimWriter *trimLastFourBytesWriter + flateWriter *flate.Writer } func (mw *msgWriter) reset(ctx context.Context, typ MessageType) error { @@ -108,6 +125,7 @@ func (mw *msgWriter) reset(ctx context.Context, typ MessageType) error { mw.closed = false mw.ctx = ctx mw.opcode = opcode(typ) + mw.deflate = false return nil } @@ -119,9 +137,9 @@ func (mw *msgWriter) Write(p []byte) (_ int, err error) { return 0, errors.New("cannot use closed writer") } - if mw.c.deflateNegotiated() { + if mw.c.deflate() { if !mw.deflate { - if !mw.contextTakeover() { + if !mw.deflateContextTakeover() { mw.ensureFlateWriter() } mw.trimWriter.reset() @@ -152,7 +170,7 @@ func (mw *msgWriter) Close() (err error) { } mw.closed = true - if mw.c.deflateNegotiated() { + if mw.c.deflate() { err = mw.flateWriter.Flush() if err != nil { return fmt.Errorf("failed to flush flate writer: %w", err) @@ -164,7 +182,7 @@ func (mw *msgWriter) Close() (err error) { return fmt.Errorf("failed to write fin frame: %w", err) } - if mw.deflate && !mw.contextTakeover() { + if mw.deflate && !mw.deflateContextTakeover() { putFlateWriter(mw.flateWriter) mw.deflate = false } @@ -173,10 +191,10 @@ func (mw *msgWriter) Close() (err error) { return nil } -func (cw *msgWriter) close() { - if cw.c.deflateNegotiated() && cw.contextTakeover() { - cw.mu.Lock(context.Background()) - putFlateWriter(cw.flateWriter) +func (mw *msgWriter) close() { + if mw.c.deflate() && mw.deflateContextTakeover() { + mw.mu.Lock(context.Background()) + putFlateWriter(mw.flateWriter) } } diff --git a/ws_js.go b/ws_js.go index 3043106b..950aa01b 100644 --- a/ws_js.go +++ b/ws_js.go @@ -13,7 +13,18 @@ import ( "nhooyr.io/websocket/internal/bpool" "nhooyr.io/websocket/internal/wsjs" - "nhooyr.io/websocket/internal/wssync" +) + +// MessageType represents the type of a WebSocket message. +// See https://tools.ietf.org/html/rfc6455#section-5.6 +type MessageType int + +// MessageType constants. +const ( + // MessageText is for UTF-8 encoded text messages like JSON. + MessageText MessageType = iota + 1 + // MessageBinary is for binary messages like Protobufs. + MessageBinary ) // Conn provides a wrapper around the browser WebSocket API. @@ -21,10 +32,10 @@ type Conn struct { ws wsjs.WebSocket // read limit for a message in bytes. - msgReadLimit *wssync.Int64 + msgReadLimit atomicInt64 closingMu sync.Mutex - isReadClosed *wssync.Int64 + isReadClosed atomicInt64 closeOnce sync.Once closed chan struct{} closeErrOnce sync.Once @@ -337,6 +348,7 @@ func (w writer) Close() error { return nil } +// CloseRead implements *Conn.CloseRead for wasm. func (c *Conn) CloseRead(ctx context.Context) context.Context { c.isReadClosed.Store(1) @@ -349,6 +361,7 @@ func (c *Conn) CloseRead(ctx context.Context) context.Context { return ctx } +// SetReadLimit implements *Conn.SetReadLimit for wasm. func (c *Conn) SetReadLimit(n int64) { c.msgReadLimit.Store(n) } diff --git a/ws_js_test.go b/ws_js_test.go index ea888b59..6e87480b 100644 --- a/ws_js_test.go +++ b/ws_js_test.go @@ -1,4 +1,4 @@ -package websocket +package websocket_test import ( "context" diff --git a/wsjson/wsjson.go b/wsjson/wsjson.go index 36dd2dfd..99996a69 100644 --- a/wsjson/wsjson.go +++ b/wsjson/wsjson.go @@ -5,6 +5,7 @@ import ( "context" "encoding/json" "fmt" + "nhooyr.io/websocket" "nhooyr.io/websocket/internal/bpool" "nhooyr.io/websocket/internal/errd" From b11e4ec6b7cd54f13132220d088a5bb055092948 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 6 Dec 2019 13:38:35 -0600 Subject: [PATCH 231/519] Improve handshake to better match RFC Closes #176 Closes #173 --- handshake.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/handshake.go b/handshake.go index 2c01cab6..11e46d8f 100644 --- a/handshake.go +++ b/handshake.go @@ -55,14 +55,18 @@ func verifyClientRequest(w http.ResponseWriter, r *http.Request) error { } if !headerValuesContainsToken(r.Header, "Connection", "Upgrade") { + w.Header().Set("Connection", "Upgrade") + w.Header().Set("Upgrade", "websocket") err := fmt.Errorf("websocket protocol violation: Connection header %q does not contain Upgrade", r.Header.Get("Connection")) - http.Error(w, err.Error(), http.StatusBadRequest) + http.Error(w, err.Error(), http.StatusUpgradeRequired) return err } if !headerValuesContainsToken(r.Header, "Upgrade", "WebSocket") { + w.Header().Set("Connection", "Upgrade") + w.Header().Set("Upgrade", "websocket") err := fmt.Errorf("websocket protocol violation: Upgrade header %q does not contain websocket", r.Header.Get("Upgrade")) - http.Error(w, err.Error(), http.StatusBadRequest) + http.Error(w, err.Error(), http.StatusUpgradeRequired) return err } @@ -73,6 +77,7 @@ func verifyClientRequest(w http.ResponseWriter, r *http.Request) error { } if r.Header.Get("Sec-WebSocket-Version") != "13" { + w.Header().Set("Sec-WebSocket-Version", "13") err := fmt.Errorf("unsupported websocket protocol version (only 13 is supported): %q", r.Header.Get("Sec-WebSocket-Version")) http.Error(w, err.Error(), http.StatusBadRequest) return err From f6137f3f404630d19a84bc0fd59570ff6a967004 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 6 Dec 2019 13:35:49 -0600 Subject: [PATCH 232/519] Add minor improvements Closes #179 --- README.md | 39 +++--- accept_test.go | 20 +-- assert_test.go | 26 ++-- ci/image/Dockerfile | 2 - ci/test.mk | 8 +- close.go | 11 +- close_test.go | 16 +-- conn.go | 6 +- conn_test.go | 7 +- doc.go | 9 +- frame_test.go | 15 ++- go.mod | 4 +- go.sum | 247 +++++++++++++++++++++++++++++++++++++- internal/assert/assert.go | 50 -------- internal/assert/cmp.go | 53 -------- read.go | 64 +++++----- 16 files changed, 359 insertions(+), 218 deletions(-) delete mode 100644 internal/assert/assert.go delete mode 100644 internal/assert/cmp.go diff --git a/README.md b/README.md index f0babdfc..e958d2ab 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # websocket -[![version](https://img.shields.io/github/v/release/nhooyr/websocket?color=6b9ded&sort=semver)](https://github.com/nhooyr/websocket/releases) -[![docs](https://godoc.org/nhooyr.io/websocket?status.svg)](https://godoc.org/nhooyr.io/websocket) +[![release](https://img.shields.io/github/v/release/nhooyr/websocket?color=6b9ded&sort=semver)](https://github.com/nhooyr/websocket/releases) +[![godoc](https://godoc.org/nhooyr.io/websocket?status.svg)](https://godoc.org/nhooyr.io/websocket) [![coverage](https://img.shields.io/coveralls/github/nhooyr/websocket?color=65d6a4)](https://coveralls.io/github/nhooyr/websocket) [![ci](https://github.com/nhooyr/websocket/workflows/ci/badge.svg)](https://github.com/nhooyr/websocket/actions) @@ -19,14 +19,14 @@ go get nhooyr.io/websocket - First class [context.Context](https://blog.golang.org/context) support - Thorough tests, fully passes the [autobahn-testsuite](https://github.com/crossbario/autobahn-testsuite) - [Zero dependencies](https://godoc.org/nhooyr.io/websocket?imports) -- JSON and ProtoBuf helpers in the [wsjson](https://godoc.org/nhooyr.io/websocket/wsjson) and [wspb](https://godoc.org/nhooyr.io/websocket/wspb) subpackages +- JSON and protobuf helpers in the [wsjson](https://godoc.org/nhooyr.io/websocket/wsjson) and [wspb](https://godoc.org/nhooyr.io/websocket/wspb) subpackages - Zero alloc reads and writes - Concurrent writes - [Close handshake](https://godoc.org/nhooyr.io/websocket#Conn.Close) - [net.Conn](https://godoc.org/nhooyr.io/websocket#NetConn) wrapper -- [Ping pong](https://godoc.org/nhooyr.io/websocket#Conn.Ping) +- [Ping pong](https://godoc.org/nhooyr.io/websocket#Conn.Ping) API - [RFC 7692](https://tools.ietf.org/html/rfc7692) permessage-deflate compression -- Compile to [Wasm](https://godoc.org/nhooyr.io/websocket#hdr-Wasm) +- Can target [Wasm](https://godoc.org/nhooyr.io/websocket#hdr-Wasm) ## Roadmap @@ -85,7 +85,11 @@ c.Close(websocket.StatusNormalClosure, "") ### gorilla/websocket -[gorilla/websocket](https://github.com/gorilla/websocket) is a widely used and mature library. +Advantages of [gorilla/websocket](https://github.com/gorilla/websocket): + +- Mature and widely used +- [Prepared writes](https://godoc.org/github.com/gorilla/websocket#PreparedMessage) +- Configurable [buffer sizes](https://godoc.org/github.com/gorilla/websocket#hdr-Buffers) Advantages of nhooyr.io/websocket: @@ -94,26 +98,26 @@ Advantages of nhooyr.io/websocket: - [net.Conn](https://godoc.org/nhooyr.io/websocket#NetConn) wrapper - Zero alloc reads and writes ([gorilla/websocket#535](https://github.com/gorilla/websocket/issues/535)) - Full [context.Context](https://blog.golang.org/context) support -- Uses [net/http.Client](https://golang.org/pkg/net/http/#Client) for dialing +- Dial uses [net/http.Client](https://golang.org/pkg/net/http/#Client) - Will enable easy HTTP/2 support in the future - - Gorilla writes directly to a net.Conn and so duplicates features from net/http.Client. + - Gorilla writes directly to a net.Conn and so duplicates features of net/http.Client. - Concurrent writes - Close handshake ([gorilla/websocket#448](https://github.com/gorilla/websocket/issues/448)) -- Idiomatic [ping](https://godoc.org/nhooyr.io/websocket#Conn.Ping) API - - gorilla/websocket requires registering a pong callback and then sending a Ping -- Wasm ([gorilla/websocket#432](https://github.com/gorilla/websocket/issues/432)) +- Idiomatic [ping pong](https://godoc.org/nhooyr.io/websocket#Conn.Ping) API + - Gorilla requires registering a pong callback before sending a Ping +- Can target Wasm ([gorilla/websocket#432](https://github.com/gorilla/websocket/issues/432)) - Transparent message buffer reuse with [wsjson](https://godoc.org/nhooyr.io/websocket/wsjson) and [wspb](https://godoc.org/nhooyr.io/websocket/wspb) subpackages - [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster WebSocket masking implementation in pure Go - - Gorilla's implementation depends on unsafe and is slower + - Gorilla's implementation is slower and uses [unsafe](https://golang.org/pkg/unsafe/). - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support - Gorilla only supports no context takeover mode -- [CloseRead](https://godoc.org/nhooyr.io/websocket#Conn.CloseRead) helper +- [CloseRead](https://godoc.org/nhooyr.io/websocket#Conn.CloseRead) helper ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492)) - Actively maintained ([gorilla/websocket#370](https://github.com/gorilla/websocket/issues/370)) #### golang.org/x/net/websocket [golang.org/x/net/websocket](https://godoc.org/golang.org/x/net/websocket) is deprecated. -See ([golang/go/issues/18152](https://github.com/golang/go/issues/18152)). +See [golang/go/issues/18152](https://github.com/golang/go/issues/18152). The [net.Conn](https://godoc.org/nhooyr.io/websocket#NetConn) wrapper will ease in transitioning to nhooyr.io/websocket. @@ -124,10 +128,3 @@ to nhooyr.io/websocket. in an event driven style for performance. See the author's [blog post](https://medium.freecodecamp.org/million-websockets-and-go-cc58418460bb). However when writing idiomatic Go, nhooyr.io/websocket will be faster and easier to use. - -## Users - -If your company or project is using this library, feel free to open an issue or PR to amend this list. - -- [Coder](https://github.com/cdr) -- [Tatsu Works](https://github.com/tatsuworks) - Ingresses 20 TB in WebSocket data every month on their Discord bot. diff --git a/accept_test.go b/accept_test.go index 551fe4de..2a784d19 100644 --- a/accept_test.go +++ b/accept_test.go @@ -7,7 +7,7 @@ import ( "strings" "testing" - "nhooyr.io/websocket/internal/assert" + "cdr.dev/slog/sloggers/slogtest/assert" ) func TestAccept(t *testing.T) { @@ -20,7 +20,7 @@ func TestAccept(t *testing.T) { r := httptest.NewRequest("GET", "/", nil) _, err := Accept(w, r, nil) - assert.ErrorContains(t, err, "protocol violation") + assert.ErrorContains(t, "Accept", err, "protocol violation") }) t.Run("requireHttpHijacker", func(t *testing.T) { @@ -34,7 +34,7 @@ func TestAccept(t *testing.T) { r.Header.Set("Sec-WebSocket-Key", "meow123") _, err := Accept(w, r, nil) - assert.ErrorContains(t, err, "http.ResponseWriter does not implement http.Hijacker") + assert.ErrorContains(t, "Accept", err, "http.ResponseWriter does not implement http.Hijacker") }) } @@ -127,9 +127,9 @@ func Test_verifyClientHandshake(t *testing.T) { err := verifyClientRequest(r) if tc.success { - assert.Success(t, err) + assert.Success(t, "verifyClientRequest", err) } else { - assert.Error(t, err) + assert.Error(t, "verifyClientRequest", err) } }) } @@ -179,7 +179,7 @@ func Test_selectSubprotocol(t *testing.T) { r.Header.Set("Sec-WebSocket-Protocol", strings.Join(tc.clientProtocols, ",")) negotiated := selectSubprotocol(r, tc.serverProtocols) - assert.Equal(t, tc.negotiated, negotiated, "negotiated") + assert.Equal(t, "negotiated", tc.negotiated, negotiated) }) } } @@ -234,10 +234,14 @@ func Test_authenticateOrigin(t *testing.T) { err := authenticateOrigin(r) if tc.success { - assert.Success(t, err) + assert.Success(t, "authenticateOrigin", err) } else { - assert.Error(t, err) + assert.Error(t, "authenticateOrigin", err) } }) } } + +func Test_acceptCompression(t *testing.T) { + +} diff --git a/assert_test.go b/assert_test.go index dd4c30cd..cd78fbb3 100644 --- a/assert_test.go +++ b/assert_test.go @@ -3,19 +3,19 @@ package websocket_test import ( "context" "crypto/rand" - "io" "strings" "testing" + "cdr.dev/slog/sloggers/slogtest/assert" + "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/assert" "nhooyr.io/websocket/wsjson" ) func randBytes(t *testing.T, n int) []byte { b := make([]byte, n) - _, err := io.ReadFull(rand.Reader, b) - assert.Success(t, err) + _, err := rand.Reader.Read(b) + assert.Success(t, "readRandBytes", err) return b } @@ -25,7 +25,7 @@ func assertJSONEcho(t *testing.T, ctx context.Context, c *websocket.Conn, n int) exp := randString(t, n) err := wsjson.Write(ctx, c, exp) - assert.Success(t, err) + assert.Success(t, "wsjson.Write", err) assertJSONRead(t, ctx, c, exp) @@ -37,9 +37,9 @@ func assertJSONRead(t *testing.T, ctx context.Context, c *websocket.Conn, exp in var act interface{} err := wsjson.Read(ctx, c, &act) - assert.Success(t, err) + assert.Success(t, "wsjson.Read", err) - assert.Equal(t, exp, act, "JSON") + assert.Equal(t, "json", exp, act) } func randString(t *testing.T, n int) string { @@ -60,19 +60,19 @@ func assertEcho(t *testing.T, ctx context.Context, c *websocket.Conn, typ websoc p := randBytes(t, n) err := c.Write(ctx, typ, p) - assert.Success(t, err) + assert.Success(t, "write", err) typ2, p2, err := c.Read(ctx) - assert.Success(t, err) + assert.Success(t, "read", err) - assert.Equal(t, typ, typ2, "data type") - assert.Equal(t, p, p2, "payload") + assert.Equal(t, "dataType", typ, typ2) + assert.Equal(t, "payload", p, p2) } func assertSubprotocol(t *testing.T, c *websocket.Conn, exp string) { t.Helper() - assert.Equal(t, exp, c.Subprotocol(), "subprotocol") + assert.Equal(t, "subprotocol", exp, c.Subprotocol()) } func assertCloseStatus(t *testing.T, exp websocket.StatusCode, err error) { @@ -82,5 +82,5 @@ func assertCloseStatus(t *testing.T, exp websocket.StatusCode, err error) { t.Logf("error: %+v", err) } }() - assert.Equal(t, exp, websocket.CloseStatus(err), "StatusCode") + assert.Equal(t, "closeStatus", exp, websocket.CloseStatus(err)) } diff --git a/ci/image/Dockerfile b/ci/image/Dockerfile index ccfac109..bfc05fc8 100644 --- a/ci/image/Dockerfile +++ b/ci/image/Dockerfile @@ -2,8 +2,6 @@ FROM golang:1 RUN apt-get update RUN apt-get install -y chromium -RUN apt-get install -y npm -RUN apt-get install -y jq ENV GOFLAGS="-mod=readonly" ENV PAGER=cat diff --git a/ci/test.mk b/ci/test.mk index f9a6e09a..95e049b2 100644 --- a/ci/test.mk +++ b/ci/test.mk @@ -9,13 +9,7 @@ ci/out/coverage.html: gotest coveralls: gotest # https://github.com/coverallsapp/github-action/blob/master/src/run.ts echo "--- coveralls" - export GIT_BRANCH="$$GITHUB_REF" - export BUILD_NUMBER="$$GITHUB_SHA" - if [[ $$GITHUB_EVENT_NAME == pull_request ]]; then - export CI_PULL_REQUEST="$$(jq .number "$$GITHUB_EVENT_PATH")" - BUILD_NUMBER="$$BUILD_NUMBER-PR-$$CI_PULL_REQUEST" - fi - goveralls -coverprofile=ci/out/coverage.prof -service=github + goveralls -coverprofile=ci/out/coverage.prof gotest: go test -covermode=count -coverprofile=ci/out/coverage.prof -coverpkg=./... $${GOTESTFLAGS-} ./... diff --git a/close.go b/close.go index af437553..7ccdb173 100644 --- a/close.go +++ b/close.go @@ -30,7 +30,7 @@ const ( StatusProtocolError StatusCode = 1002 StatusUnsupportedData StatusCode = 1003 - // 1004 is reserved and so not exported. + // 1004 is reserved and so unexported. statusReserved StatusCode = 1004 // StatusNoStatusRcvd cannot be sent in a close message. @@ -103,7 +103,6 @@ func (c *Conn) Close(code StatusCode, reason string) error { func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { defer errd.Wrap(&err, "failed to close WebSocket") - defer c.close(nil) err = c.writeClose(code, reason) if err != nil { @@ -124,6 +123,14 @@ func (c *Conn) writeError(code StatusCode, err error) { } func (c *Conn) writeClose(code StatusCode, reason string) error { + c.closeMu.Lock() + closing := c.wroteClose + c.wroteClose = true + c.closeMu.Unlock() + if closing { + return errors.New("already wrote close") + } + ce := CloseError{ Code: code, Reason: reason, diff --git a/close_test.go b/close_test.go index a2e0f67d..16b570d0 100644 --- a/close_test.go +++ b/close_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - "nhooyr.io/websocket/internal/assert" + "cdr.dev/slog/sloggers/slogtest/assert" ) func TestCloseError(t *testing.T) { @@ -52,9 +52,9 @@ func TestCloseError(t *testing.T) { _, err := tc.ce.bytesErr() if tc.success { - assert.Success(t, err) + assert.Success(t, "CloseError.bytesErr", err) } else { - assert.Error(t, err) + assert.Error(t, "CloseError.bytesErr", err) } }) } @@ -104,10 +104,10 @@ func Test_parseClosePayload(t *testing.T) { ce, err := parseClosePayload(tc.p) if tc.success { - assert.Success(t, err) - assert.Equal(t, tc.ce, ce, "CloseError") + assert.Success(t, "parse err", err) + assert.Equal(t, "ce", tc.ce, ce) } else { - assert.Error(t, err) + assert.Error(t, "parse err", err) } }) } @@ -153,7 +153,7 @@ func Test_validWireCloseCode(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - assert.Equal(t, tc.valid, validWireCloseCode(tc.code), "validWireCloseCode") + assert.Equal(t, "valid", tc.valid, validWireCloseCode(tc.code)) }) } } @@ -190,7 +190,7 @@ func TestCloseStatus(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - assert.Equal(t, tc.exp, CloseStatus(tc.in), "CloseStatus") + assert.Equal(t, "closeStatus", tc.exp, CloseStatus(tc.in)) }) } } diff --git a/conn.go b/conn.go index 10fe2e1a..061c4517 100644 --- a/conn.go +++ b/conn.go @@ -63,7 +63,7 @@ type Conn struct { closed chan struct{} closeMu sync.Mutex closeErr error - wroteClose int64 + wroteClose bool pingCounter int32 activePingsMu sync.Mutex @@ -244,7 +244,9 @@ func (m *mu) Lock(ctx context.Context) error { case <-m.c.closed: return m.c.closeErr case <-ctx.Done(): - return ctx.Err() + err := fmt.Errorf("failed to acquire lock: %w", ctx.Err()) + m.c.close(err) + return err case m.ch <- struct{}{}: return nil } diff --git a/conn_test.go b/conn_test.go index 9b628cfe..9b311a87 100644 --- a/conn_test.go +++ b/conn_test.go @@ -13,8 +13,9 @@ import ( "testing" "time" + "cdr.dev/slog/sloggers/slogtest/assert" + "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/assert" ) func TestConn(t *testing.T) { @@ -27,7 +28,7 @@ func TestConn(t *testing.T) { InsecureSkipVerify: true, CompressionMode: websocket.CompressionNoContextTakeover, }) - assert.Success(t, err) + assert.Success(t, "accept", err) defer c.Close(websocket.StatusInternalError, "") err = echoLoop(r.Context(), c) @@ -47,7 +48,7 @@ func TestConn(t *testing.T) { opts.HTTPClient = s.Client() c, _, err := websocket.Dial(ctx, wsURL, opts) - assert.Success(t, err) + assert.Success(t, "dial", err) assertJSONEcho(t, ctx, c, 2) }) } diff --git a/doc.go b/doc.go index 54b7e1ea..6847d537 100644 --- a/doc.go +++ b/doc.go @@ -4,14 +4,17 @@ // // https://tools.ietf.org/html/rfc6455 // -// Use Dial to dial a WebSocket server and Accept to accept a WebSocket client. +// Use Dial to dial a WebSocket server. +// +// Accept to accept a WebSocket client. +// // Conn represents the resulting WebSocket connection. // // The examples are the best way to understand how to correctly use the library. // // The wsjson and wspb subpackages contain helpers for JSON and Protobuf messages. // -// See https://nhooyr.io/websocket for further information. +// More documentation at https://nhooyr.io/websocket. // // Wasm // @@ -23,7 +26,7 @@ // Some important caveats to be aware of: // // - Conn.Ping is no-op -// - HTTPClient, HTTPHeader and CompressionOptions in DialOptions are no-op +// - HTTPClient, HTTPHeader and CompressionMode in DialOptions are no-op // - *http.Response from Dial is &http.Response{} on success // package websocket // import "nhooyr.io/websocket" diff --git a/frame_test.go b/frame_test.go index 68455cfa..323ea991 100644 --- a/frame_test.go +++ b/frame_test.go @@ -13,10 +13,9 @@ import ( "time" _ "unsafe" + "cdr.dev/slog/sloggers/slogtest/assert" "github.com/gobwas/ws" _ "github.com/gorilla/websocket" - - "nhooyr.io/websocket/internal/assert" ) func TestHeader(t *testing.T) { @@ -81,14 +80,14 @@ func testHeader(t *testing.T, h header) { r := bufio.NewReader(b) err := writeFrameHeader(h, w) - assert.Success(t, err) + assert.Success(t, "writeFrameHeader", err) err = w.Flush() - assert.Success(t, err) + assert.Success(t, "flush", err) h2, err := readFrameHeader(r) - assert.Success(t, err) + assert.Success(t, "readFrameHeader", err) - assert.Equal(t, h, h2, "header") + assert.Equal(t, "header", h, h2) } func Test_mask(t *testing.T) { @@ -99,8 +98,8 @@ func Test_mask(t *testing.T) { p := []byte{0xa, 0xb, 0xc, 0xf2, 0xc} gotKey32 := mask(key32, p) - assert.Equal(t, []byte{0, 0, 0, 0x0d, 0x6}, p, "mask") - assert.Equal(t, bits.RotateLeft32(key32, -8), gotKey32, "mask key") + assert.Equal(t, "mask", []byte{0, 0, 0, 0x0d, 0x6}, p) + assert.Equal(t, "maskKey", bits.RotateLeft32(key32, -8), gotKey32) } func basicMask(maskKey [4]byte, pos int, b []byte) int { diff --git a/go.mod b/go.mod index 6cd368b4..06098485 100644 --- a/go.mod +++ b/go.mod @@ -3,11 +3,13 @@ module nhooyr.io/websocket go 1.13 require ( + cdr.dev/slog v1.3.0 github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee // indirect github.com/gobwas/pool v0.2.0 // indirect github.com/gobwas/ws v1.0.2 github.com/golang/protobuf v1.3.2 - github.com/google/go-cmp v0.3.1 github.com/gorilla/websocket v1.4.1 + github.com/mattn/goveralls v0.0.4 // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 + golang.org/x/tools v0.0.0-20191218225520-84f0c7cf60ea // indirect ) diff --git a/go.sum b/go.sum index c639eb64..df11eba9 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,257 @@ +cdr.dev/slog v1.3.0 h1:MYN1BChIaVEGxdS7I5cpdyMC0+WfJfK8BETAfzfLUGQ= +cdr.dev/slog v1.3.0/go.mod h1:C5OL99WyuOK8YHZdYY57dAPN1jK2WJlCdq2VP6xeQns= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= +cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= +cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= +cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= +cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= +cloud.google.com/go v0.49.0 h1:CH+lkubJzcPYB1Ggupcq0+k8Ni2ILdG2lYjDIgavDBQ= +cloud.google.com/go v0.49.0/go.mod h1:hGvAdzcWNbyuxS3nWhD7H2cIJxjRRTRLQVB0bdputVY= +cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= +cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= +cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= +cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= +dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= +github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= +github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= +github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= +github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= +github.com/alecthomas/chroma v0.7.0 h1:z+0HgTUmkpRDRz0SRSdMaqOLfJV4F+N1FPDZUZIDUzw= +github.com/alecthomas/chroma v0.7.0/go.mod h1:1U/PfCsTALWWYHDnsIQkxEBM0+6LLe0v8+RSVMOwxeY= +github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo= +github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= +github.com/alecthomas/kong v0.1.17-0.20190424132513-439c674f7ae0/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= +github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= +github.com/alecthomas/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MRgZdU3vrFd05IQ89AxUZ0aYdF39BYoNFa324SodPCA= +github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY= +github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= +github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= +github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dlclark/regexp2 v1.1.6 h1:CqB4MjHw0MFCDj+PHHjiESmHX+N7t0tJzKvC6M97BRg= +github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk= +github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= +github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= +github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 h1:uHTyIjqVhYRhLbJ8nIiOJHkEZZ+5YoOsAbD3sk82NiE= +github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/google/go-cmp v0.3.1 h1:Xye71clBPdm5HgqGwUkwhbynsUJZhDbS20FvLhQ2izg= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.2-0.20191216170541-340f1ebe299e h1:4WfjkTUTsO6siF8ghDQQk6t7x/FPsv3w6MXkc47do7Q= +github.com/google/go-cmp v0.3.2-0.20191216170541-340f1ebe299e/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= +github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= +github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= +github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI= +github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= +github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/goveralls v0.0.4 h1:/mdWfiU2y8kZ48EtgByYev/XT3W4dkTuKLOJJsh/r+o= +github.com/mattn/goveralls v0.0.4/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= +github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= +github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= +go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= +go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= +go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs= +go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= +golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g= +golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= +golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= +golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= +golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= +golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= +golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= +golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= +golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= +golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 h1:gSbV7h1NRL2G1xTg/owz62CST1oJBmxy4QpMMregXVQ= +golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2 h1:EtTFh6h4SAKemS+CURDMTDIANuduG5zKEXShyy18bGA= +golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191218225520-84f0c7cf60ea h1:mtRJM/ln5qwEigajtnZtuARALEPOooGf5lwkM5a9tt4= +golang.org/x/tools v0.0.0-20191218225520-84f0c7cf60ea/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= +google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= +google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= +google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= +google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1 h1:aQktFqmDE2yjveXJlVIfslDFmFnUXSqG0i6KRcJAeMc= +google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.1 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8= +google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= +rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/internal/assert/assert.go b/internal/assert/assert.go deleted file mode 100644 index b20d9420..00000000 --- a/internal/assert/assert.go +++ /dev/null @@ -1,50 +0,0 @@ -// Package assert contains helpers for test assertions. -package assert - -import ( - "strings" - "testing" -) - -// Equal asserts exp == act. -func Equal(t testing.TB, exp, act interface{}, name string) { - t.Helper() - diff := cmpDiff(exp, act) - if diff != "" { - t.Fatalf("unexpected %v: %v", name, diff) - } -} - -// NotEqual asserts exp != act. -func NotEqual(t testing.TB, exp, act interface{}, name string) { - t.Helper() - if cmpDiff(exp, act) == "" { - t.Fatalf("expected different %v: %+v", name, act) - } -} - -// Success asserts exp == nil. -func Success(t testing.TB, err error) { - t.Helper() - if err != nil { - t.Fatalf("unexpected error: %+v", err) - } -} - -// Error asserts exp != nil. -func Error(t testing.TB, err error) { - t.Helper() - if err == nil { - t.Fatal("expected error") - } -} - -// ErrorContains asserts the error string from err contains sub. -func ErrorContains(t testing.TB, err error, sub string) { - t.Helper() - Error(t, err) - errs := err.Error() - if !strings.Contains(errs, sub) { - t.Fatalf("error string %q does not contain %q", errs, sub) - } -} diff --git a/internal/assert/cmp.go b/internal/assert/cmp.go deleted file mode 100644 index 39be1f4a..00000000 --- a/internal/assert/cmp.go +++ /dev/null @@ -1,53 +0,0 @@ -package assert - -import ( - "reflect" - - "github.com/google/go-cmp/cmp" -) - -// https://github.com/google/go-cmp/issues/40#issuecomment-328615283 -func cmpDiff(exp, act interface{}) string { - return cmp.Diff(exp, act, deepAllowUnexported(exp, act)) -} - -func deepAllowUnexported(vs ...interface{}) cmp.Option { - m := make(map[reflect.Type]struct{}) - for _, v := range vs { - structTypes(reflect.ValueOf(v), m) - } - var typs []interface{} - for t := range m { - typs = append(typs, reflect.New(t).Elem().Interface()) - } - return cmp.AllowUnexported(typs...) -} - -func structTypes(v reflect.Value, m map[reflect.Type]struct{}) { - if !v.IsValid() { - return - } - switch v.Kind() { - case reflect.Ptr: - if !v.IsNil() { - structTypes(v.Elem(), m) - } - case reflect.Interface: - if !v.IsNil() { - structTypes(v.Elem(), m) - } - case reflect.Slice, reflect.Array: - for i := 0; i < v.Len(); i++ { - structTypes(v.Index(i), m) - } - case reflect.Map: - for _, k := range v.MapKeys() { - structTypes(v.MapIndex(k), m) - } - case reflect.Struct: - m[v.Type()] = struct{}{} - for i := 0; i < v.NumField(); i++ { - structTypes(v.Field(i), m) - } - } -} diff --git a/read.go b/read.go index c72b6c17..dc59f9f4 100644 --- a/read.go +++ b/read.go @@ -69,7 +69,7 @@ func (c *Conn) CloseRead(ctx context.Context) context.Context { // // When the limit is hit, the connection will be closed with StatusMessageTooBig. func (c *Conn) SetReadLimit(n int64) { - c.msgReader.limitReader.setLimit(n) + c.msgReader.limitReader.limit.Store(n) } func newMsgReader(c *Conn) *msgReader { @@ -87,15 +87,17 @@ func newMsgReader(c *Conn) *msgReader { } func (mr *msgReader) initFlateReader() { - mr.flateReader = getFlateReader(readerFunc(mr.read)) - mr.limitReader.reset(mr.flateReader) + mr.deflateReader = getFlateReader(readerFunc(mr.read)) + mr.limitReader.r = mr.deflateReader } func (mr *msgReader) close() { - if mr.c.deflate() && mr.contextTakeover() { - mr.c.readMu.Lock(context.Background()) - putFlateReader(mr.flateReader) - mr.c.readMu.Unlock() + mr.c.readMu.Lock(context.Background()) + defer mr.c.readMu.Unlock() + + if mr.deflateReader != nil { + putFlateReader(mr.deflateReader) + mr.deflateReader = nil } } @@ -266,7 +268,7 @@ func (c *Conn) handleControl(ctx context.Context, h header) (err error) { err = fmt.Errorf("received close frame: %w", ce) c.setCloseErr(err) - c.writeControl(context.Background(), opClose, ce.bytes()) + c.writeClose(ce.Code, ce.Reason) return err } @@ -302,36 +304,35 @@ func (c *Conn) reader(ctx context.Context) (_ MessageType, _ io.Reader, err erro type msgReader struct { c *Conn - ctx context.Context - - deflate bool - flateReader io.Reader - deflateTail strings.Reader - - limitReader *limitReader + ctx context.Context + deflate bool + deflateReader io.Reader + deflateTail strings.Reader + limitReader *limitReader + fin bool payloadLength int64 maskKey uint32 - fin bool } func (mr *msgReader) reset(ctx context.Context, h header) { mr.ctx = ctx mr.deflate = h.rsv1 if mr.deflate { - mr.deflateTail.Reset(deflateMessageTail) if !mr.contextTakeover() { mr.initFlateReader() } + mr.deflateTail.Reset(deflateMessageTail) } + + mr.limitReader.reset() mr.setFrame(h) - mr.fin = false } func (mr *msgReader) setFrame(h header) { + mr.fin = h.fin mr.payloadLength = h.payloadLength mr.maskKey = h.maskKey - mr.fin = h.fin } func (mr *msgReader) Read(p []byte) (_ int, err error) { @@ -350,9 +351,9 @@ func (mr *msgReader) Read(p []byte) (_ int, err error) { if mr.payloadLength == 0 && mr.fin { if mr.c.deflate() && !mr.contextTakeover() { - if mr.flateReader != nil { - putFlateReader(mr.flateReader) - mr.flateReader = nil + if mr.deflateReader != nil { + putFlateReader(mr.deflateReader) + mr.deflateReader = nil } } return 0, io.EOF @@ -363,12 +364,9 @@ func (mr *msgReader) Read(p []byte) (_ int, err error) { func (mr *msgReader) read(p []byte) (int, error) { if mr.payloadLength == 0 { - if mr.fin { - if mr.deflate { - n, _ := mr.deflateTail.Read(p) - return n, nil - } - return 0, io.EOF + if mr.fin && mr.deflate { + n, _ := mr.deflateTail.Read(p) + return n, nil } h, err := mr.c.readLoop(mr.ctx) @@ -413,17 +411,13 @@ func newLimitReader(c *Conn, r io.Reader, limit int64) *limitReader { c: c, } lr.limit.Store(limit) - lr.reset(r) + lr.r = r + lr.reset() return lr } -func (lr *limitReader) reset(r io.Reader) { +func (lr *limitReader) reset() { lr.n = lr.limit.Load() - lr.r = r -} - -func (lr *limitReader) setLimit(limit int64) { - lr.limit.Store(limit) } func (lr *limitReader) Read(p []byte) (int, error) { From 6f6fa430a6e88699b3b8aef5d1b8499100f3e8b9 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 30 Dec 2019 22:03:21 -0500 Subject: [PATCH 233/519] Refactor autobahn --- accept.go | 2 - autobahn_test.go | 319 ++++++++++++++++++++++++++--------------------- close.go | 2 + compress.go | 18 +-- conn.go | 7 +- go.mod | 2 - go.sum | 7 -- read.go | 74 ++++++----- write.go | 45 ++++--- 9 files changed, 260 insertions(+), 216 deletions(-) diff --git a/accept.go b/accept.go index ea7beebd..f16180f0 100644 --- a/accept.go +++ b/accept.go @@ -37,8 +37,6 @@ type AcceptOptions struct { // If used incorrectly your WebSocket server will be open to CSRF attacks. InsecureSkipVerify bool - // CompressionMode sets the compression mode. - // See the docs on CompressionMode. CompressionMode CompressionMode } diff --git a/autobahn_test.go b/autobahn_test.go index 6b3b5b72..16384b27 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -9,7 +9,6 @@ import ( "io/ioutil" "net" "net/http" - "net/http/httptest" "os" "os/exec" "strconv" @@ -17,9 +16,27 @@ import ( "testing" "time" + "cdr.dev/slog/sloggers/slogtest/assert" + "nhooyr.io/websocket" + "nhooyr.io/websocket/internal/errd" ) +var excludedAutobahnCases = []string{ + // We skip the UTF-8 handling tests as there isn't any reason to reject invalid UTF-8, just + // more performance overhead. + "6.*", "7.5.1", + + // We skip the tests related to requestMaxWindowBits as that is unimplemented due + // to limitations in compress/flate. See https://github.com/golang/go/issues/3155 + "13.3.*", "13.4.*", "13.5.*", "13.6.*", + + "12.*", + "13.*", +} + +var autobahnCases = []string{"*"} + // https://github.com/crossbario/autobahn-python/tree/master/wstest func TestAutobahn(t *testing.T) { t.Parallel() @@ -35,19 +52,17 @@ func TestAutobahn(t *testing.T) { func testServerAutobahn(t *testing.T) { t.Parallel() - s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + s, closeFn := testServer(t, func(w http.ResponseWriter, r *http.Request) { c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ Subprotocols: []string{"echo"}, }) - if err != nil { - t.Logf("server handshake failed: %+v", err) - return - } - echoLoop(r.Context(), c) - })) - defer s.Close() + assert.Success(t, "accept", err) + err = echoLoop(r.Context(), c) + assertCloseStatus(t, websocket.StatusNormalClosure, err) + }, false) + defer closeFn() - spec := map[string]interface{}{ + specFile, err := tempJSONFile(map[string]interface{}{ "outdir": "ci/out/wstestServerReports", "servers": []interface{}{ map[string]interface{}{ @@ -55,92 +70,105 @@ func testServerAutobahn(t *testing.T) { "url": strings.Replace(s.URL, "http", "ws", 1), }, }, - "cases": []string{"*"}, - // We skip the UTF-8 handling tests as there isn't any reason to reject invalid UTF-8, just - // more performance overhead. 7.5.1 is the same. - "exclude-cases": []string{"6.*", "7.5.1"}, - } - specFile, err := ioutil.TempFile("", "websocketFuzzingClient.json") - if err != nil { - t.Fatalf("failed to create temp file for fuzzingclient.json: %v", err) - } - defer specFile.Close() - - e := json.NewEncoder(specFile) - e.SetIndent("", "\t") - err = e.Encode(spec) - if err != nil { - t.Fatalf("failed to write spec: %v", err) - } - - err = specFile.Close() - if err != nil { - t.Fatalf("failed to close file: %v", err) - } + "cases": autobahnCases, + "exclude-cases": excludedAutobahnCases, + }) + assert.Success(t, "tempJSONFile", err) - ctx := context.Background() - ctx, cancel := context.WithTimeout(ctx, time.Minute*10) + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*10) defer cancel() - args := []string{"--mode", "fuzzingclient", "--spec", specFile.Name()} + args := []string{"--mode", "fuzzingclient", "--spec", specFile} wstest := exec.CommandContext(ctx, "wstest", args...) - out, err := wstest.CombinedOutput() - if err != nil { - t.Fatalf("failed to run wstest: %v\nout:\n%s", err, out) - } + _, err = wstest.CombinedOutput() + assert.Success(t, "wstest", err) checkWSTestIndex(t, "./ci/out/wstestServerReports/index.json") } -func unusedListenAddr() (string, error) { - l, err := net.Listen("tcp", "localhost:0") - if err != nil { - return "", err - } - l.Close() - return l.Addr().String(), nil -} - func testClientAutobahn(t *testing.T) { t.Parallel() - serverAddr, err := unusedListenAddr() - if err != nil { - t.Fatalf("failed to get unused listen addr for wstest: %v", err) - } + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) + defer cancel() - wsServerURL := "ws://" + serverAddr + wstestURL, closeFn, err := wstestClientServer(ctx) + assert.Success(t, "wstestClient", err) + defer closeFn() - spec := map[string]interface{}{ - "url": wsServerURL, - "outdir": "ci/out/wstestClientReports", - "cases": []string{"*"}, - // See TestAutobahnServer for the reasons why we exclude these. - "exclude-cases": []string{"6.*", "7.5.1"}, - } - specFile, err := ioutil.TempFile("", "websocketFuzzingServer.json") - if err != nil { - t.Fatalf("failed to create temp file for fuzzingserver.json: %v", err) + err = waitWS(ctx, wstestURL) + assert.Success(t, "waitWS", err) + + cases, err := wstestCaseCount(ctx, wstestURL) + assert.Success(t, "wstestCaseCount", err) + + t.Run("cases", func(t *testing.T) { + for i := 1; i <= cases; i++ { + i := i + t.Run("", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(ctx, time.Second*45) + defer cancel() + + c, _, err := websocket.Dial(ctx, fmt.Sprintf(wstestURL+"/runCase?case=%v&agent=main", i), nil) + assert.Success(t, "autobahn dial", err) + + err = echoLoop(ctx, c) + t.Logf("echoLoop: %+v", err) + }) + } + }) + + c, _, err := websocket.Dial(ctx, fmt.Sprintf(wstestURL+"/updateReports?agent=main"), nil) + assert.Success(t, "dial", err) + c.Close(websocket.StatusNormalClosure, "") + + checkWSTestIndex(t, "./ci/out/wstestClientReports/index.json") +} + +func waitWS(ctx context.Context, url string) error { + ctx, cancel := context.WithTimeout(ctx, time.Second*5) + defer cancel() + + for ctx.Err() == nil { + c, _, err := websocket.Dial(ctx, url, nil) + if err != nil { + continue + } + c.Close(websocket.StatusNormalClosure, "") + return nil } - defer specFile.Close() - e := json.NewEncoder(specFile) - e.SetIndent("", "\t") - err = e.Encode(spec) + return ctx.Err() +} + +func wstestClientServer(ctx context.Context) (url string, closeFn func(), err error) { + serverAddr, err := unusedListenAddr() if err != nil { - t.Fatalf("failed to write spec: %v", err) + return "", nil, err } - err = specFile.Close() + url = "ws://" + serverAddr + + specFile, err := tempJSONFile(map[string]interface{}{ + "url": url, + "outdir": "ci/out/wstestClientReports", + "cases": autobahnCases, + "exclude-cases": excludedAutobahnCases, + }) if err != nil { - t.Fatalf("failed to close file: %v", err) + return "", nil, fmt.Errorf("failed to write spec: %w", err) } - ctx := context.Background() - ctx, cancel := context.WithTimeout(ctx, time.Minute*10) - defer cancel() + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) + defer func() { + if err != nil { + cancel() + } + }() - args := []string{"--mode", "fuzzingserver", "--spec", specFile.Name(), + args := []string{"--mode", "fuzzingserver", "--spec", specFile, // Disables some server that runs as part of fuzzingserver mode. // See https://github.com/crossbario/autobahn-testsuite/blob/058db3a36b7c3a1edf68c282307c6b899ca4857f/autobahntestsuite/autobahntestsuite/wstest.py#L124 "--webport=0", @@ -148,101 +176,104 @@ func testClientAutobahn(t *testing.T) { wstest := exec.CommandContext(ctx, "wstest", args...) err = wstest.Start() if err != nil { - t.Fatal(err) + return "", nil, fmt.Errorf("failed to start wstest: %w", err) } - defer func() { - err := wstest.Process.Kill() - if err != nil { - t.Error(err) - } - }() - - // Let it come up. - time.Sleep(time.Second * 5) - - var cases int - func() { - c, _, err := websocket.Dial(ctx, wsServerURL+"/getCaseCount", nil) - if err != nil { - t.Fatal(err) - } - defer c.Close(websocket.StatusInternalError, "") - - _, r, err := c.Reader(ctx) - if err != nil { - t.Fatal(err) - } - b, err := ioutil.ReadAll(r) - if err != nil { - t.Fatal(err) - } - cases, err = strconv.Atoi(string(b)) - if err != nil { - t.Fatal(err) - } - c.Close(websocket.StatusNormalClosure, "") - }() + return url, func() { + wstest.Process.Kill() + }, nil +} - for i := 1; i <= cases; i++ { - func() { - ctx, cancel := context.WithTimeout(ctx, time.Second*45) - defer cancel() +func wstestCaseCount(ctx context.Context, url string) (cases int, err error) { + defer errd.Wrap(&err, "failed to get case count") - c, _, err := websocket.Dial(ctx, fmt.Sprintf(wsServerURL+"/runCase?case=%v&agent=main", i), nil) - if err != nil { - t.Fatal(err) - } - echoLoop(ctx, c) - }() + c, _, err := websocket.Dial(ctx, url+"/getCaseCount", nil) + if err != nil { + return 0, err } + defer c.Close(websocket.StatusInternalError, "") - c, _, err := websocket.Dial(ctx, fmt.Sprintf(wsServerURL+"/updateReports?agent=main"), nil) + _, r, err := c.Reader(ctx) + if err != nil { + return 0, err + } + b, err := ioutil.ReadAll(r) + if err != nil { + return 0, err + } + cases, err = strconv.Atoi(string(b)) if err != nil { - t.Fatal(err) + return 0, err } + c.Close(websocket.StatusNormalClosure, "") - checkWSTestIndex(t, "./ci/out/wstestClientReports/index.json") + return cases, nil } func checkWSTestIndex(t *testing.T, path string) { wstestOut, err := ioutil.ReadFile(path) - if err != nil { - t.Fatalf("failed to read index.json: %v", err) - } + assert.Success(t, "ioutil.ReadFile", err) var indexJSON map[string]map[string]struct { Behavior string `json:"behavior"` BehaviorClose string `json:"behaviorClose"` } err = json.Unmarshal(wstestOut, &indexJSON) - if err != nil { - t.Fatalf("failed to unmarshal index.json: %v", err) - } + assert.Success(t, "json.Unmarshal", err) - var failed bool for _, tests := range indexJSON { for test, result := range tests { - switch result.Behavior { - case "OK", "NON-STRICT", "INFORMATIONAL": - default: - failed = true - t.Errorf("test %v failed", test) - } - switch result.BehaviorClose { - case "OK", "INFORMATIONAL": - default: - failed = true - t.Errorf("bad close behaviour for test %v", test) - } + t.Run(test, func(t *testing.T) { + switch result.BehaviorClose { + case "OK", "INFORMATIONAL": + default: + t.Errorf("bad close behaviour") + } + + switch result.Behavior { + case "OK", "NON-STRICT", "INFORMATIONAL": + default: + t.Errorf("failed") + } + }) } } - if failed { - path = strings.Replace(path, ".json", ".html", 1) - if os.Getenv("CI") == "" { - t.Errorf("wstest found failure, see %q (output as an artifact in CI)", path) - } + if t.Failed() { + htmlPath := strings.Replace(path, ".json", ".html", 1) + t.Errorf("detected autobahn violation, see %q", htmlPath) } } + +func unusedListenAddr() (_ string, err error) { + defer errd.Wrap(&err, "failed to get unused listen address") + l, err := net.Listen("tcp", "localhost:0") + if err != nil { + return "", err + } + l.Close() + return l.Addr().String(), nil +} + +func tempJSONFile(v interface{}) (string, error) { + f, err := ioutil.TempFile("", "temp.json") + if err != nil { + return "", fmt.Errorf("temp file: %w", err) + } + defer f.Close() + + e := json.NewEncoder(f) + e.SetIndent("", "\t") + err = e.Encode(v) + if err != nil { + return "", fmt.Errorf("json encode: %w", err) + } + + err = f.Close() + if err != nil { + return "", fmt.Errorf("close temp file: %w", err) + } + + return f.Name(), nil +} diff --git a/close.go b/close.go index 7ccdb173..c5c51c6e 100644 --- a/close.go +++ b/close.go @@ -147,6 +147,8 @@ func (c *Conn) writeClose(code StatusCode, reason string) error { } func (c *Conn) waitCloseHandshake() error { + defer c.close(nil) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() diff --git a/compress.go b/compress.go index 2410cb4e..8c4dbe28 100644 --- a/compress.go +++ b/compress.go @@ -9,6 +9,14 @@ import ( "sync" ) +type CompressionOptions struct { + // Mode controls the compression mode. + Mode CompressionMode + + // Threshold controls the minimum size of a message before compression is applied. + Threshold int +} + // CompressionMode controls the modes available RFC 7692's deflate extension. // See https://tools.ietf.org/html/rfc7692 // @@ -29,14 +37,8 @@ const ( // The message will only be compressed if greater than 512 bytes. CompressionNoContextTakeover CompressionMode = iota - // CompressionContextTakeover uses a flate.Reader and flate.Writer per connection. - // This enables reusing the sliding window from previous messages. - // As most WebSocket protocols are repetitive, this can be very efficient. - // - // The message will only be compressed if greater than 128 bytes. - // - // If the peer negotiates NoContextTakeover on the client or server side, it will be - // used instead as this is required by the RFC. + // Unimplemented for now due to limitations in compress/flate. + // See https://github.com/golang/go/issues/31514#issuecomment-569668619 CompressionContextTakeover // CompressionDisabled disables the deflate extension. diff --git a/conn.go b/conn.go index 061c4517..5ccf9f91 100644 --- a/conn.go +++ b/conn.go @@ -176,7 +176,7 @@ func (c *Conn) timeoutLoop() { } } -func (c *Conn) deflate() bool { +func (c *Conn) flate() bool { return c.copts != nil } @@ -262,5 +262,8 @@ func (m *mu) TryLock() bool { } func (m *mu) Unlock() { - <-m.ch + select { + case <-m.ch: + default: + } } diff --git a/go.mod b/go.mod index 06098485..01ec18f7 100644 --- a/go.mod +++ b/go.mod @@ -9,7 +9,5 @@ require ( github.com/gobwas/ws v1.0.2 github.com/golang/protobuf v1.3.2 github.com/gorilla/websocket v1.4.1 - github.com/mattn/goveralls v0.0.4 // indirect golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 - golang.org/x/tools v0.0.0-20191218225520-84f0c7cf60ea // indirect ) diff --git a/go.sum b/go.sum index df11eba9..864efaa7 100644 --- a/go.sum +++ b/go.sum @@ -102,8 +102,6 @@ github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/goveralls v0.0.4 h1:/mdWfiU2y8kZ48EtgByYev/XT3W4dkTuKLOJJsh/r+o= -github.com/mattn/goveralls v0.0.4/go.mod h1:8d1ZMHsd7fW6IRPKQh46F2WRpyib5/X4FOpevwGNQEw= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -129,7 +127,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -150,7 +147,6 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -209,11 +205,8 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2 h1:EtTFh6h4SAKemS+CURDMTDIANuduG5zKEXShyy18bGA= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191218225520-84f0c7cf60ea h1:mtRJM/ln5qwEigajtnZtuARALEPOooGf5lwkM5a9tt4= -golang.org/x/tools v0.0.0-20191218225520-84f0c7cf60ea/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= diff --git a/read.go b/read.go index dc59f9f4..517022b5 100644 --- a/read.go +++ b/read.go @@ -79,7 +79,7 @@ func newMsgReader(c *Conn) *msgReader { } mr.limitReader = newLimitReader(c, readerFunc(mr.read), 32768) - if c.deflate() && mr.contextTakeover() { + if c.flate() && mr.flateContextTakeover() { mr.initFlateReader() } @@ -87,30 +87,27 @@ func newMsgReader(c *Conn) *msgReader { } func (mr *msgReader) initFlateReader() { - mr.deflateReader = getFlateReader(readerFunc(mr.read)) - mr.limitReader.r = mr.deflateReader + mr.flateReader = getFlateReader(readerFunc(mr.read)) + mr.limitReader.r = mr.flateReader } func (mr *msgReader) close() { mr.c.readMu.Lock(context.Background()) defer mr.c.readMu.Unlock() - if mr.deflateReader != nil { - putFlateReader(mr.deflateReader) - mr.deflateReader = nil - } + mr.returnFlateReader() } -func (mr *msgReader) contextTakeover() bool { +func (mr *msgReader) flateContextTakeover() bool { if mr.c.client { - return mr.c.copts.serverNoContextTakeover + return !mr.c.copts.serverNoContextTakeover } - return mr.c.copts.clientNoContextTakeover + return !mr.c.copts.clientNoContextTakeover } func (c *Conn) readRSV1Illegal(h header) bool { // If compression is enabled, rsv1 is always illegal. - if !c.deflate() { + if !c.flate() { return true } // rsv1 is only allowed on data frames beginning messages. @@ -269,6 +266,7 @@ func (c *Conn) handleControl(ctx context.Context, h header) (err error) { err = fmt.Errorf("received close frame: %w", ce) c.setCloseErr(err) c.writeClose(ce.Code, ce.Reason) + c.close(err) return err } @@ -304,11 +302,11 @@ func (c *Conn) reader(ctx context.Context) (_ MessageType, _ io.Reader, err erro type msgReader struct { c *Conn - ctx context.Context - deflate bool - deflateReader io.Reader - deflateTail strings.Reader - limitReader *limitReader + ctx context.Context + deflate bool + flateReader io.Reader + deflateTail strings.Reader + limitReader *limitReader fin bool payloadLength int64 @@ -319,7 +317,7 @@ func (mr *msgReader) reset(ctx context.Context, h header) { mr.ctx = ctx mr.deflate = h.rsv1 if mr.deflate { - if !mr.contextTakeover() { + if !mr.flateContextTakeover() { mr.initFlateReader() } mr.deflateTail.Reset(deflateMessageTail) @@ -335,8 +333,19 @@ func (mr *msgReader) setFrame(h header) { mr.maskKey = h.maskKey } -func (mr *msgReader) Read(p []byte) (_ int, err error) { +func (mr *msgReader) Read(p []byte) (n int, err error) { defer func() { + r := recover() + if r != nil { + if r != "ANMOL" { + panic(r) + } + err = io.EOF + if !mr.flateContextTakeover() { + mr.returnFlateReader() + } + } + errd.Wrap(&err, "failed to read") if errors.Is(err, io.EOF) { err = io.EOF @@ -349,24 +358,27 @@ func (mr *msgReader) Read(p []byte) (_ int, err error) { } defer mr.c.readMu.Unlock() - if mr.payloadLength == 0 && mr.fin { - if mr.c.deflate() && !mr.contextTakeover() { - if mr.deflateReader != nil { - putFlateReader(mr.deflateReader) - mr.deflateReader = nil - } - } - return 0, io.EOF - } - return mr.limitReader.Read(p) } +func (mr *msgReader) returnFlateReader() { + if mr.flateReader != nil { + putFlateReader(mr.flateReader) + mr.flateReader = nil + } +} + func (mr *msgReader) read(p []byte) (int, error) { if mr.payloadLength == 0 { - if mr.fin && mr.deflate { - n, _ := mr.deflateTail.Read(p) - return n, nil + if mr.fin { + if mr.deflate { + if mr.deflateTail.Len() == 0 { + panic("ANMOL") + } + n, _ := mr.deflateTail.Read(p) + return n, nil + } + return 0, io.EOF } h, err := mr.c.readLoop(mr.ctx) diff --git a/write.go b/write.go index 526b3b66..de20e041 100644 --- a/write.go +++ b/write.go @@ -55,7 +55,7 @@ func newMsgWriter(c *Conn) *msgWriter { mw.trimWriter = &trimLastFourBytesWriter{ w: writerFunc(mw.write), } - if c.deflate() && mw.deflateContextTakeover() { + if c.flate() && mw.flateContextTakeover() { mw.ensureFlateWriter() } @@ -63,14 +63,16 @@ func newMsgWriter(c *Conn) *msgWriter { } func (mw *msgWriter) ensureFlateWriter() { - mw.flateWriter = getFlateWriter(mw.trimWriter) + if mw.flateWriter == nil { + mw.flateWriter = getFlateWriter(mw.trimWriter) + } } -func (mw *msgWriter) deflateContextTakeover() bool { +func (mw *msgWriter) flateContextTakeover() bool { if mw.c.client { - return mw.c.copts.clientNoContextTakeover + return !mw.c.copts.clientNoContextTakeover } - return mw.c.copts.serverNoContextTakeover + return !mw.c.copts.serverNoContextTakeover } func (c *Conn) writer(ctx context.Context, typ MessageType) (io.WriteCloser, error) { @@ -87,7 +89,7 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error return 0, err } - if !c.deflate() { + if !c.flate() { // Fast single frame path. defer c.msgWriter.mu.Unlock() return c.writeFrame(ctx, true, c.msgWriter.opcode, p) @@ -107,11 +109,12 @@ type msgWriter struct { mu *mu - deflate bool - ctx context.Context - opcode opcode - closed bool + ctx context.Context + opcode opcode + closed bool + // TODO pass down into writeFrame + flate bool trimWriter *trimLastFourBytesWriter flateWriter *flate.Writer } @@ -125,7 +128,7 @@ func (mw *msgWriter) reset(ctx context.Context, typ MessageType) error { mw.closed = false mw.ctx = ctx mw.opcode = opcode(typ) - mw.deflate = false + mw.flate = false return nil } @@ -137,13 +140,14 @@ func (mw *msgWriter) Write(p []byte) (_ int, err error) { return 0, errors.New("cannot use closed writer") } - if mw.c.deflate() { - if !mw.deflate { - if !mw.deflateContextTakeover() { + if mw.c.flate() { + if !mw.flate { + mw.flate = true + + if !mw.flateContextTakeover() { mw.ensureFlateWriter() } mw.trimWriter.reset() - mw.deflate = true } return mw.flateWriter.Write(p) @@ -170,7 +174,7 @@ func (mw *msgWriter) Close() (err error) { } mw.closed = true - if mw.c.deflate() { + if mw.flate { err = mw.flateWriter.Flush() if err != nil { return fmt.Errorf("failed to flush flate writer: %w", err) @@ -182,9 +186,9 @@ func (mw *msgWriter) Close() (err error) { return fmt.Errorf("failed to write fin frame: %w", err) } - if mw.deflate && !mw.deflateContextTakeover() { + if mw.c.flate() && !mw.flateContextTakeover() && mw.flateWriter != nil { putFlateWriter(mw.flateWriter) - mw.deflate = false + mw.flateWriter = nil } mw.mu.Unlock() @@ -192,9 +196,10 @@ func (mw *msgWriter) Close() (err error) { } func (mw *msgWriter) close() { - if mw.c.deflate() && mw.deflateContextTakeover() { + if mw.flateWriter != nil && mw.flateContextTakeover() { mw.mu.Lock(context.Background()) putFlateWriter(mw.flateWriter) + mw.flateWriter = nil } } @@ -236,7 +241,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, opcode opcode, p []byte } c.writeHeader.rsv1 = false - if c.msgWriter.deflate && (opcode == opText || opcode == opBinary) { + if c.flate() && (opcode == opText || opcode == opBinary) { c.writeHeader.rsv1 = true } From 8c87970e1fbf809deab88d9a9637822616a6f676 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 4 Jan 2020 02:19:14 -0500 Subject: [PATCH 234/519] Add slidingWindowReader --- accept.go | 4 +++- autobahn_test.go | 8 ++++++++ compress.go | 52 ++++++++++++++++++++++++++++++++++++++++++++---- dial.go | 6 +++--- 4 files changed, 62 insertions(+), 8 deletions(-) diff --git a/accept.go b/accept.go index f16180f0..f030e4aa 100644 --- a/accept.go +++ b/accept.go @@ -37,7 +37,9 @@ type AcceptOptions struct { // If used incorrectly your WebSocket server will be open to CSRF attacks. InsecureSkipVerify bool - CompressionMode CompressionMode + // CompressionOptions controls the compression options. + // See docs on the CompressionOptions type. + CompressionOptions CompressionOptions } // Accept accepts a WebSocket handshake from a client and upgrades the diff --git a/autobahn_test.go b/autobahn_test.go index 16384b27..1c39887c 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -103,11 +103,19 @@ func testClientAutobahn(t *testing.T) { assert.Success(t, "wstestCaseCount", err) t.Run("cases", func(t *testing.T) { + // Max 8 cases running at a time. + mu := make(chan struct{}, 8) + for i := 1; i <= cases; i++ { i := i t.Run("", func(t *testing.T) { t.Parallel() + mu <- struct{}{} + defer func() { + <-mu + }() + ctx, cancel := context.WithTimeout(ctx, time.Second*45) defer cancel() diff --git a/compress.go b/compress.go index 8c4dbe28..62cc9cd3 100644 --- a/compress.go +++ b/compress.go @@ -37,8 +37,14 @@ const ( // The message will only be compressed if greater than 512 bytes. CompressionNoContextTakeover CompressionMode = iota - // Unimplemented for now due to limitations in compress/flate. - // See https://github.com/golang/go/issues/31514#issuecomment-569668619 + // CompressionContextTakeover uses a flate.Reader and flate.Writer per connection. + // This enables reusing the sliding window from previous messages. + // As most WebSocket protocols are repetitive, this can be very efficient. + // + // The message will only be compressed if greater than 128 bytes. + // + // If the peer negotiates NoContextTakeover on the client or server side, it will be + // used instead as this is required by the RFC. CompressionContextTakeover // CompressionDisabled disables the deflate extension. @@ -151,10 +157,10 @@ func putFlateReader(fr io.Reader) { var flateWriterPool sync.Pool -func getFlateWriter(w io.Writer) *flate.Writer { +func getFlateWriter(w io.Writer, dict []byte) *flate.Writer { fw, ok := flateWriterPool.Get().(*flate.Writer) if !ok { - fw, _ = flate.NewWriter(w, flate.BestSpeed) + fw, _ = flate.NewWriterDict(w, flate.BestSpeed, dict) return fw } fw.Reset(w) @@ -164,3 +170,41 @@ func getFlateWriter(w io.Writer) *flate.Writer { func putFlateWriter(w *flate.Writer) { flateWriterPool.Put(w) } + +type slidingWindowReader struct { + window []byte + + r io.Reader +} + +func (r slidingWindowReader) Read(p []byte) (int, error) { + n, err := r.r.Read(p) + p = p[:n] + + r.append(p) + + return n, err +} + +func (r slidingWindowReader) append(p []byte) { + if len(r.window) <= cap(r.window) { + r.window = append(r.window, p...) + } + + if len(p) > cap(r.window) { + p = p[len(p)-cap(r.window):] + } + + // p now contains at max the last window bytes + // so we need to be able to append all of it to r.window. + // Shift as many bytes from r.window as needed. + + // Maximum window size minus current window minus extra gives + // us the number of bytes that need to be shifted. + off := len(r.window) + len(p) - cap(r.window) + + r.window = append(r.window[:0], r.window[off:]...) + copy(r.window, r.window[off:]) + copy(r.window[len(r.window)-len(p):], p) + return +} diff --git a/dial.go b/dial.go index 6cde30e7..43408f20 100644 --- a/dial.go +++ b/dial.go @@ -33,9 +33,9 @@ type DialOptions struct { // Subprotocols lists the WebSocket subprotocols to negotiate with the server. Subprotocols []string - // CompressionMode sets the compression mode. - // See the docs on CompressionMode. - CompressionMode CompressionMode + // CompressionOptions controls the compression options. + // See docs on the CompressionOptions type. + CompressionOptions CompressionOptions } // Dial performs a WebSocket handshake on url. From aaf4b458c6a66df98da8375425cb54ec47e9540b Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 25 Jan 2020 20:58:09 -0600 Subject: [PATCH 235/519] Up test coverage of accept.go to 100% --- accept.go | 6 ++- accept_test.go | 140 +++++++++++++++++++++++++++++++++++++++++++++++++ compress.go | 14 +++-- conn_test.go | 10 ++-- dial.go | 4 +- write.go | 2 +- 6 files changed, 164 insertions(+), 12 deletions(-) diff --git a/accept.go b/accept.go index f030e4aa..d9b4bf90 100644 --- a/accept.go +++ b/accept.go @@ -92,7 +92,7 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con w.Header().Set("Sec-WebSocket-Protocol", subproto) } - copts, err := acceptCompression(r, w, opts.CompressionMode) + copts, err := acceptCompression(r, w, opts.CompressionOptions.Mode) if err != nil { return nil, err } @@ -201,7 +201,9 @@ func acceptDeflate(w http.ResponseWriter, ext websocketExtension, mode Compressi case "server_no_context_takeover": copts.serverNoContextTakeover = true continue - case "client_max_window_bits", "server-max-window-bits": + } + + if strings.HasPrefix(p, "client_max_window_bits") || strings.HasPrefix(p, "server_max_window_bits") { continue } diff --git a/accept_test.go b/accept_test.go index 2a784d19..8a9e9198 100644 --- a/accept_test.go +++ b/accept_test.go @@ -3,6 +3,10 @@ package websocket import ( + "bufio" + "errors" + "net" + "net/http" "net/http/httptest" "strings" "testing" @@ -23,6 +27,38 @@ func TestAccept(t *testing.T) { assert.ErrorContains(t, "Accept", err, "protocol violation") }) + t.Run("badOrigin", func(t *testing.T) { + t.Parallel() + + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/", nil) + r.Header.Set("Connection", "Upgrade") + r.Header.Set("Upgrade", "websocket") + r.Header.Set("Sec-WebSocket-Version", "13") + r.Header.Set("Sec-WebSocket-Key", "meow123") + r.Header.Set("Origin", "harhar.com") + + _, err := Accept(w, r, nil) + assert.ErrorContains(t, "Accept", err, "request Origin \"harhar.com\" is not authorized for Host") + }) + + t.Run("badCompression", func(t *testing.T) { + t.Parallel() + + w := mockHijacker{ + ResponseWriter: httptest.NewRecorder(), + } + r := httptest.NewRequest("GET", "/", nil) + r.Header.Set("Connection", "Upgrade") + r.Header.Set("Upgrade", "websocket") + r.Header.Set("Sec-WebSocket-Version", "13") + r.Header.Set("Sec-WebSocket-Key", "meow123") + r.Header.Set("Sec-WebSocket-Extensions", "permessage-deflate; harharhar") + + _, err := Accept(w, r, nil) + assert.ErrorContains(t, "Accept", err, "unsupported permessage-deflate parameter") + }) + t.Run("requireHttpHijacker", func(t *testing.T) { t.Parallel() @@ -36,6 +72,26 @@ func TestAccept(t *testing.T) { _, err := Accept(w, r, nil) assert.ErrorContains(t, "Accept", err, "http.ResponseWriter does not implement http.Hijacker") }) + + t.Run("badHijack", func(t *testing.T) { + t.Parallel() + + w := mockHijacker{ + ResponseWriter: httptest.NewRecorder(), + hijack: func() (conn net.Conn, writer *bufio.ReadWriter, err error) { + return nil, nil, errors.New("haha") + }, + } + + r := httptest.NewRequest("GET", "/", nil) + r.Header.Set("Connection", "Upgrade") + r.Header.Set("Upgrade", "websocket") + r.Header.Set("Sec-WebSocket-Version", "13") + r.Header.Set("Sec-WebSocket-Key", "meow123") + + _, err := Accept(w, r, nil) + assert.ErrorContains(t, "Accept", err, "failed to hijack connection") + }) } func Test_verifyClientHandshake(t *testing.T) { @@ -243,5 +299,89 @@ func Test_authenticateOrigin(t *testing.T) { } func Test_acceptCompression(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + mode CompressionMode + reqSecWebSocketExtensions string + respSecWebSocketExtensions string + expCopts *compressionOptions + error bool + }{ + { + name: "disabled", + mode: CompressionDisabled, + expCopts: nil, + }, + { + name: "noClientSupport", + mode: CompressionNoContextTakeover, + expCopts: nil, + }, + { + name: "permessage-deflate", + mode: CompressionNoContextTakeover, + reqSecWebSocketExtensions: "permessage-deflate; client_max_window_bits", + respSecWebSocketExtensions: "permessage-deflate; client_no_context_takeover; server_no_context_takeover", + expCopts: &compressionOptions{ + clientNoContextTakeover: true, + serverNoContextTakeover: true, + }, + }, + { + name: "permessage-deflate/error", + mode: CompressionNoContextTakeover, + reqSecWebSocketExtensions: "permessage-deflate; meow", + error: true, + }, + { + name: "x-webkit-deflate-frame", + mode: CompressionNoContextTakeover, + reqSecWebSocketExtensions: "x-webkit-deflate-frame; no_context_takeover", + respSecWebSocketExtensions: "x-webkit-deflate-frame; no_context_takeover", + expCopts: &compressionOptions{ + clientNoContextTakeover: true, + serverNoContextTakeover: true, + }, + }, + { + name: "x-webkit-deflate/error", + mode: CompressionNoContextTakeover, + reqSecWebSocketExtensions: "x-webkit-deflate-frame; max_window_bits", + error: true, + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + r := httptest.NewRequest(http.MethodGet, "/", nil) + r.Header.Set("Sec-WebSocket-Extensions", tc.reqSecWebSocketExtensions) + + w := httptest.NewRecorder() + copts, err := acceptCompression(r, w, tc.mode) + if tc.error { + assert.Error(t, "acceptCompression", err) + return + } + + assert.Success(t, "acceptCompression", err) + assert.Equal(t, "compresssionOpts", tc.expCopts, copts) + assert.Equal(t, "respHeader", tc.respSecWebSocketExtensions, w.Header().Get("Sec-WebSocket-Extensions")) + }) + } +} + +type mockHijacker struct { + http.ResponseWriter + hijack func() (net.Conn, *bufio.ReadWriter, error) +} + +var _ http.Hijacker = mockHijacker{} +func (mj mockHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) { + return mj.hijack() } diff --git a/compress.go b/compress.go index 62cc9cd3..fd2535cc 100644 --- a/compress.go +++ b/compress.go @@ -9,15 +9,22 @@ import ( "sync" ) +// CompressionOptions represents the available deflate extension options. +// See https://tools.ietf.org/html/rfc7692 type CompressionOptions struct { // Mode controls the compression mode. + // + // See docs on CompressionMode. Mode CompressionMode // Threshold controls the minimum size of a message before compression is applied. + // + // Defaults to 512 bytes for CompressionNoContextTakeover and 256 bytes + // for CompressionContextTakeover. Threshold int } -// CompressionMode controls the modes available RFC 7692's deflate extension. +// CompressionMode represents the modes available to the deflate extension. // See https://tools.ietf.org/html/rfc7692 // // A compatibility layer is implemented for the older deflate-frame extension used @@ -31,7 +38,7 @@ const ( // for every message. This applies to both server and client side. // // This means less efficient compression as the sliding window from previous messages - // will not be used but the memory overhead will be much lower if the connections + // will not be used but the memory overhead will be lower if the connections // are long lived and seldom used. // // The message will only be compressed if greater than 512 bytes. @@ -40,8 +47,7 @@ const ( // CompressionContextTakeover uses a flate.Reader and flate.Writer per connection. // This enables reusing the sliding window from previous messages. // As most WebSocket protocols are repetitive, this can be very efficient. - // - // The message will only be compressed if greater than 128 bytes. + // It carries an overhead of 64 kB for every connection compared to CompressionNoContextTakeover. // // If the peer negotiates NoContextTakeover on the client or server side, it will be // used instead as this is required by the RFC. diff --git a/conn_test.go b/conn_test.go index 9b311a87..c8663b47 100644 --- a/conn_test.go +++ b/conn_test.go @@ -26,7 +26,9 @@ func TestConn(t *testing.T) { c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ Subprotocols: []string{"echo"}, InsecureSkipVerify: true, - CompressionMode: websocket.CompressionNoContextTakeover, + CompressionOptions: websocket.CompressionOptions{ + Mode: websocket.CompressionNoContextTakeover, + }, }) assert.Success(t, "accept", err) defer c.Close(websocket.StatusInternalError, "") @@ -42,8 +44,10 @@ func TestConn(t *testing.T) { defer cancel() opts := &websocket.DialOptions{ - Subprotocols: []string{"echo"}, - CompressionMode: websocket.CompressionNoContextTakeover, + Subprotocols: []string{"echo"}, + CompressionOptions: websocket.CompressionOptions{ + Mode: websocket.CompressionNoContextTakeover, + }, } opts.HTTPClient = s.Client() diff --git a/dial.go b/dial.go index 43408f20..af945011 100644 --- a/dial.go +++ b/dial.go @@ -136,8 +136,8 @@ func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, secWe if len(opts.Subprotocols) > 0 { req.Header.Set("Sec-WebSocket-Protocol", strings.Join(opts.Subprotocols, ",")) } - if opts.CompressionMode != CompressionDisabled { - copts := opts.CompressionMode.opts() + if opts.CompressionOptions.Mode != CompressionDisabled { + copts := opts.CompressionOptions.Mode.opts() copts.setHeader(req.Header) } diff --git a/write.go b/write.go index de20e041..33d20c1d 100644 --- a/write.go +++ b/write.go @@ -64,7 +64,7 @@ func newMsgWriter(c *Conn) *msgWriter { func (mw *msgWriter) ensureFlateWriter() { if mw.flateWriter == nil { - mw.flateWriter = getFlateWriter(mw.trimWriter) + mw.flateWriter = getFlateWriter(mw.trimWriter, nil) } } From 6b765363d1e5ce21e6ca3bdb7bde03ecba1a2a98 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 29 Jan 2020 22:08:29 -0600 Subject: [PATCH 236/519] Up dial coverage to 100% --- .github/ISSUE_TEMPLATE.md | 3 - ci/image/Dockerfile | 2 +- conn.go | 2 +- dial.go | 13 +-- dial_test.go | 165 +++++++++++++++++++++++++++++--------- doc.go | 3 +- internal/bpool/bpool.go | 6 +- ws_js.go | 2 +- wspb/wspb.go | 16 ++-- 9 files changed, 151 insertions(+), 61 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md deleted file mode 100644 index 7b580937..00000000 --- a/.github/ISSUE_TEMPLATE.md +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/ci/image/Dockerfile b/ci/image/Dockerfile index bfc05fc8..070c50e6 100644 --- a/ci/image/Dockerfile +++ b/ci/image/Dockerfile @@ -6,7 +6,7 @@ RUN apt-get install -y chromium ENV GOFLAGS="-mod=readonly" ENV PAGER=cat ENV CI=true -ENV MAKEFLAGS="--jobs=8 --output-sync=target" +ENV MAKEFLAGS="--jobs=16 --output-sync=target" RUN npm install -g prettier RUN go get golang.org/x/tools/cmd/stringer diff --git a/conn.go b/conn.go index 5ccf9f91..a0176495 100644 --- a/conn.go +++ b/conn.go @@ -22,7 +22,7 @@ type MessageType int const ( // MessageText is for UTF-8 encoded text messages like JSON. MessageText MessageType = iota + 1 - // MessageBinary is for binary messages like Protobufs. + // MessageBinary is for binary messages like protobufs. MessageBinary ) diff --git a/dial.go b/dial.go index af945011..58c0a9c5 100644 --- a/dial.go +++ b/dial.go @@ -50,10 +50,10 @@ type DialOptions struct { // in net/http to perform WebSocket handshakes. // See docs on the HTTPClient option and https://github.com/golang/go/issues/26937#issuecomment-415855861 func Dial(ctx context.Context, u string, opts *DialOptions) (*Conn, *http.Response, error) { - return dial(ctx, u, opts) + return dial(ctx, u, opts, nil) } -func dial(ctx context.Context, urls string, opts *DialOptions) (_ *Conn, _ *http.Response, err error) { +func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) (_ *Conn, _ *http.Response, err error) { defer errd.Wrap(&err, "failed to WebSocket dial") if opts == nil { @@ -67,7 +67,7 @@ func dial(ctx context.Context, urls string, opts *DialOptions) (_ *Conn, _ *http opts.HTTPHeader = http.Header{} } - secWebSocketKey, err := secWebSocketKey() + secWebSocketKey, err := secWebSocketKey(rand) if err != nil { return nil, nil, fmt.Errorf("failed to generate Sec-WebSocket-Key: %w", err) } @@ -148,9 +148,12 @@ func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, secWe return resp, nil } -func secWebSocketKey() (string, error) { +func secWebSocketKey(rr io.Reader) (string, error) { + if rr == nil { + rr = rand.Reader + } b := make([]byte, 16) - _, err := io.ReadFull(rand.Reader, b) + _, err := io.ReadFull(rr, b) if err != nil { return "", fmt.Errorf("failed to read random data from rand.Reader: %w", err) } diff --git a/dial_test.go b/dial_test.go index 6286f0ff..4314f98e 100644 --- a/dial_test.go +++ b/dial_test.go @@ -4,58 +4,117 @@ package websocket import ( "context" + "crypto/rand" + "io" + "io/ioutil" "net/http" "net/http/httptest" "strings" "testing" "time" + + "cdr.dev/slog/sloggers/slogtest/assert" ) func TestBadDials(t *testing.T) { t.Parallel() - testCases := []struct { - name string - url string - opts *DialOptions - }{ - { - name: "badURL", - url: "://noscheme", - }, - { - name: "badURLScheme", - url: "ftp://nhooyr.io", - }, - { - name: "badHTTPClient", - url: "ws://nhooyr.io", - opts: &DialOptions{ - HTTPClient: &http.Client{ - Timeout: time.Minute, + t.Run("badReq", func(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + url string + opts *DialOptions + rand readerFunc + }{ + { + name: "badURL", + url: "://noscheme", + }, + { + name: "badURLScheme", + url: "ftp://nhooyr.io", + }, + { + name: "badHTTPClient", + url: "ws://nhooyr.io", + opts: &DialOptions{ + HTTPClient: &http.Client{ + Timeout: time.Minute, + }, }, }, - }, - { - name: "badTLS", - url: "wss://totallyfake.nhooyr.io", - }, - } + { + name: "badTLS", + url: "wss://totallyfake.nhooyr.io", + }, + { + name: "badReader", + rand: func(p []byte) (int, error) { + return 0, io.EOF + }, + }, + } - for _, tc := range testCases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - defer cancel() + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() - _, _, err := Dial(ctx, tc.url, tc.opts) - if err == nil { - t.Fatalf("expected non nil error: %+v", err) - } + if tc.rand == nil { + tc.rand = rand.Reader.Read + } + + _, _, err := dial(ctx, tc.url, tc.opts, tc.rand) + assert.Error(t, "dial", err) + }) + } + }) + + t.Run("badResponse", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + _, _, err := Dial(ctx, "ws://example.com", &DialOptions{ + HTTPClient: mockHTTPClient(func(*http.Request) (*http.Response, error) { + return &http.Response{ + Body: ioutil.NopCloser(strings.NewReader("hi")), + }, nil + }), }) - } + assert.ErrorContains(t, "dial", err, "failed to WebSocket dial: expected handshake response status code 101 but got 0") + }) + + t.Run("badBody", func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + rt := func(r *http.Request) (*http.Response, error) { + h := http.Header{} + h.Set("Connection", "Upgrade") + h.Set("Upgrade", "websocket") + h.Set("Sec-WebSocket-Accept", secWebSocketAccept(r.Header.Get("Sec-WebSocket-Key"))) + + return &http.Response{ + StatusCode: http.StatusSwitchingProtocols, + Header: h, + Body: ioutil.NopCloser(strings.NewReader("hi")), + }, nil + } + + _, _, err := Dial(ctx, "ws://example.com", &DialOptions{ + HTTPClient: mockHTTPClient(rt), + }) + assert.ErrorContains(t, "dial", err, "response body is not a io.ReadWriteCloser") + }) } func Test_verifyServerHandshake(t *testing.T) { @@ -110,6 +169,26 @@ func Test_verifyServerHandshake(t *testing.T) { }, success: false, }, + { + name: "unsupportedExtension", + response: func(w http.ResponseWriter) { + w.Header().Set("Connection", "Upgrade") + w.Header().Set("Upgrade", "websocket") + w.Header().Set("Sec-WebSocket-Extensions", "meow") + w.WriteHeader(http.StatusSwitchingProtocols) + }, + success: false, + }, + { + name: "unsupportedDeflateParam", + response: func(w http.ResponseWriter) { + w.Header().Set("Connection", "Upgrade") + w.Header().Set("Upgrade", "websocket") + w.Header().Set("Sec-WebSocket-Extensions", "permessage-deflate; meow") + w.WriteHeader(http.StatusSwitchingProtocols) + }, + success: false, + }, { name: "success", response: func(w http.ResponseWriter) { @@ -131,7 +210,7 @@ func Test_verifyServerHandshake(t *testing.T) { resp := w.Result() r := httptest.NewRequest("GET", "/", nil) - key, err := secWebSocketKey() + key, err := secWebSocketKey(rand.Reader) if err != nil { t.Fatal(err) } @@ -151,3 +230,15 @@ func Test_verifyServerHandshake(t *testing.T) { }) } } + +func mockHTTPClient(fn roundTripperFunc) *http.Client { + return &http.Client{ + Transport: fn, + } +} + +type roundTripperFunc func(*http.Request) (*http.Response, error) + +func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { + return f(r) +} diff --git a/doc.go b/doc.go index 6847d537..c8f5550b 100644 --- a/doc.go +++ b/doc.go @@ -12,7 +12,7 @@ // // The examples are the best way to understand how to correctly use the library. // -// The wsjson and wspb subpackages contain helpers for JSON and Protobuf messages. +// The wsjson and wspb subpackages contain helpers for JSON and protobuf messages. // // More documentation at https://nhooyr.io/websocket. // @@ -28,5 +28,4 @@ // - Conn.Ping is no-op // - HTTPClient, HTTPHeader and CompressionMode in DialOptions are no-op // - *http.Response from Dial is &http.Response{} on success -// package websocket // import "nhooyr.io/websocket" diff --git a/internal/bpool/bpool.go b/internal/bpool/bpool.go index e2c5f76a..aa826fba 100644 --- a/internal/bpool/bpool.go +++ b/internal/bpool/bpool.go @@ -5,12 +5,12 @@ import ( "sync" ) -var pool sync.Pool +var bpool sync.Pool // Get returns a buffer from the pool or creates a new one if // the pool is empty. func Get() *bytes.Buffer { - b := pool.Get() + b := bpool.Get() if b == nil { return &bytes.Buffer{} } @@ -20,5 +20,5 @@ func Get() *bytes.Buffer { // Put returns a buffer into the pool. func Put(b *bytes.Buffer) { b.Reset() - pool.Put(b) + bpool.Put(b) } diff --git a/ws_js.go b/ws_js.go index 950aa01b..2aaef738 100644 --- a/ws_js.go +++ b/ws_js.go @@ -23,7 +23,7 @@ type MessageType int const ( // MessageText is for UTF-8 encoded text messages like JSON. MessageText MessageType = iota + 1 - // MessageBinary is for binary messages like Protobufs. + // MessageBinary is for binary messages like protobufs. MessageBinary ) diff --git a/wspb/wspb.go b/wspb/wspb.go index 666c6fa5..e43042d5 100644 --- a/wspb/wspb.go +++ b/wspb/wspb.go @@ -13,14 +13,14 @@ import ( "nhooyr.io/websocket/internal/errd" ) -// Read reads a Protobuf message from c into v. +// Read reads a protobuf message from c into v. // It will reuse buffers in between calls to avoid allocations. func Read(ctx context.Context, c *websocket.Conn, v proto.Message) error { return read(ctx, c, v) } func read(ctx context.Context, c *websocket.Conn, v proto.Message) (err error) { - defer errd.Wrap(&err, "failed to read Protobuf message") + defer errd.Wrap(&err, "failed to read protobuf message") typ, r, err := c.Reader(ctx) if err != nil { @@ -29,7 +29,7 @@ func read(ctx context.Context, c *websocket.Conn, v proto.Message) (err error) { if typ != websocket.MessageBinary { c.Close(websocket.StatusUnsupportedData, "expected binary message") - return fmt.Errorf("expected binary message for Protobuf but got: %v", typ) + return fmt.Errorf("expected binary message for protobuf but got: %v", typ) } b := bpool.Get() @@ -42,21 +42,21 @@ func read(ctx context.Context, c *websocket.Conn, v proto.Message) (err error) { err = proto.Unmarshal(b.Bytes(), v) if err != nil { - c.Close(websocket.StatusInvalidFramePayloadData, "failed to unmarshal Protobuf") - return fmt.Errorf("failed to unmarshal Protobuf: %w", err) + c.Close(websocket.StatusInvalidFramePayloadData, "failed to unmarshal protobuf") + return fmt.Errorf("failed to unmarshal protobuf: %w", err) } return nil } -// Write writes the Protobuf message v to c. +// Write writes the protobuf message v to c. // It will reuse buffers in between calls to avoid allocations. func Write(ctx context.Context, c *websocket.Conn, v proto.Message) error { return write(ctx, c, v) } func write(ctx context.Context, c *websocket.Conn, v proto.Message) (err error) { - defer errd.Wrap(&err, "failed to write Protobuf message") + defer errd.Wrap(&err, "failed to write protobuf message") b := bpool.Get() pb := proto.NewBuffer(b.Bytes()) @@ -66,7 +66,7 @@ func write(ctx context.Context, c *websocket.Conn, v proto.Message) (err error) err = pb.Marshal(v) if err != nil { - return fmt.Errorf("failed to marshal Protobuf: %w", err) + return fmt.Errorf("failed to marshal protobuf: %w", err) } return c.Write(ctx, websocket.MessageBinary, pb.Bytes()) From 0f115ed9aa51110aa5dad556673d9dab606bd479 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 30 Jan 2020 22:19:15 -0600 Subject: [PATCH 237/519] Add Go 1.12 support Closes #182 --- accept.go | 28 ++++++++++++++-------------- accept_test.go | 4 ++-- autobahn_test.go | 11 ++++++----- close.go | 23 ++++++++++++----------- conn.go | 18 +++++++++--------- conn_test.go | 10 ++++------ dial.go | 32 ++++++++++++++++---------------- dial_test.go | 4 +--- example_echo_test.go | 8 ++++---- go.mod | 3 ++- internal/errd/wrap.go | 8 +++----- netconn.go | 5 +++-- read.go | 38 ++++++++++++++++++-------------------- write.go | 41 ++++++++++++++++++----------------------- ws_js.go | 36 ++++++++++++++++++------------------ wsjson/wsjson.go | 9 +++++---- wspb/wspb.go | 8 ++++---- 17 files changed, 139 insertions(+), 147 deletions(-) diff --git a/accept.go b/accept.go index d9b4bf90..ac7f2de1 100644 --- a/accept.go +++ b/accept.go @@ -6,14 +6,14 @@ import ( "bytes" "crypto/sha1" "encoding/base64" - "errors" - "fmt" "io" "net/http" "net/textproto" "net/url" "strings" + "golang.org/x/xerrors" + "nhooyr.io/websocket/internal/errd" ) @@ -76,7 +76,7 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con hj, ok := w.(http.Hijacker) if !ok { - err = errors.New("http.ResponseWriter does not implement http.Hijacker") + err = xerrors.New("http.ResponseWriter does not implement http.Hijacker") http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented) return nil, err } @@ -101,7 +101,7 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con netConn, brw, err := hj.Hijack() if err != nil { - err = fmt.Errorf("failed to hijack connection: %w", err) + err = xerrors.Errorf("failed to hijack connection: %w", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return nil, err } @@ -122,27 +122,27 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con func verifyClientRequest(r *http.Request) error { if !r.ProtoAtLeast(1, 1) { - return fmt.Errorf("WebSocket protocol violation: handshake request must be at least HTTP/1.1: %q", r.Proto) + return xerrors.Errorf("WebSocket protocol violation: handshake request must be at least HTTP/1.1: %q", r.Proto) } if !headerContainsToken(r.Header, "Connection", "Upgrade") { - return fmt.Errorf("WebSocket protocol violation: Connection header %q does not contain Upgrade", r.Header.Get("Connection")) + return xerrors.Errorf("WebSocket protocol violation: Connection header %q does not contain Upgrade", r.Header.Get("Connection")) } if !headerContainsToken(r.Header, "Upgrade", "websocket") { - return fmt.Errorf("WebSocket protocol violation: Upgrade header %q does not contain websocket", r.Header.Get("Upgrade")) + return xerrors.Errorf("WebSocket protocol violation: Upgrade header %q does not contain websocket", r.Header.Get("Upgrade")) } if r.Method != "GET" { - return fmt.Errorf("WebSocket protocol violation: handshake request method is not GET but %q", r.Method) + return xerrors.Errorf("WebSocket protocol violation: handshake request method is not GET but %q", r.Method) } if r.Header.Get("Sec-WebSocket-Version") != "13" { - return fmt.Errorf("unsupported WebSocket protocol version (only 13 is supported): %q", r.Header.Get("Sec-WebSocket-Version")) + return xerrors.Errorf("unsupported WebSocket protocol version (only 13 is supported): %q", r.Header.Get("Sec-WebSocket-Version")) } if r.Header.Get("Sec-WebSocket-Key") == "" { - return errors.New("WebSocket protocol violation: missing Sec-WebSocket-Key") + return xerrors.New("WebSocket protocol violation: missing Sec-WebSocket-Key") } return nil @@ -153,10 +153,10 @@ func authenticateOrigin(r *http.Request) error { if origin != "" { u, err := url.Parse(origin) if err != nil { - return fmt.Errorf("failed to parse Origin header %q: %w", origin, err) + return xerrors.Errorf("failed to parse Origin header %q: %w", origin, err) } if !strings.EqualFold(u.Host, r.Host) { - return fmt.Errorf("request Origin %q is not authorized for Host %q", origin, r.Host) + return xerrors.Errorf("request Origin %q is not authorized for Host %q", origin, r.Host) } } return nil @@ -207,7 +207,7 @@ func acceptDeflate(w http.ResponseWriter, ext websocketExtension, mode Compressi continue } - err := fmt.Errorf("unsupported permessage-deflate parameter: %q", p) + err := xerrors.Errorf("unsupported permessage-deflate parameter: %q", p) http.Error(w, err.Error(), http.StatusBadRequest) return nil, err } @@ -237,7 +237,7 @@ func acceptWebkitDeflate(w http.ResponseWriter, ext websocketExtension, mode Com // // Either way, we're only implementing this for webkit which never sends the max_window_bits // parameter so we don't need to worry about it. - err := fmt.Errorf("unsupported x-webkit-deflate-frame parameter: %q", p) + err := xerrors.Errorf("unsupported x-webkit-deflate-frame parameter: %q", p) http.Error(w, err.Error(), http.StatusBadRequest) return nil, err } diff --git a/accept_test.go b/accept_test.go index 8a9e9198..3e8b1f46 100644 --- a/accept_test.go +++ b/accept_test.go @@ -4,7 +4,6 @@ package websocket import ( "bufio" - "errors" "net" "net/http" "net/http/httptest" @@ -12,6 +11,7 @@ import ( "testing" "cdr.dev/slog/sloggers/slogtest/assert" + "golang.org/x/xerrors" ) func TestAccept(t *testing.T) { @@ -79,7 +79,7 @@ func TestAccept(t *testing.T) { w := mockHijacker{ ResponseWriter: httptest.NewRecorder(), hijack: func() (conn net.Conn, writer *bufio.ReadWriter, err error) { - return nil, nil, errors.New("haha") + return nil, nil, xerrors.New("haha") }, } diff --git a/autobahn_test.go b/autobahn_test.go index 1c39887c..bcbf8671 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -17,6 +17,7 @@ import ( "time" "cdr.dev/slog/sloggers/slogtest/assert" + "golang.org/x/xerrors" "nhooyr.io/websocket" "nhooyr.io/websocket/internal/errd" @@ -166,7 +167,7 @@ func wstestClientServer(ctx context.Context) (url string, closeFn func(), err er "exclude-cases": excludedAutobahnCases, }) if err != nil { - return "", nil, fmt.Errorf("failed to write spec: %w", err) + return "", nil, xerrors.Errorf("failed to write spec: %w", err) } ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) @@ -184,7 +185,7 @@ func wstestClientServer(ctx context.Context) (url string, closeFn func(), err er wstest := exec.CommandContext(ctx, "wstest", args...) err = wstest.Start() if err != nil { - return "", nil, fmt.Errorf("failed to start wstest: %w", err) + return "", nil, xerrors.Errorf("failed to start wstest: %w", err) } return url, func() { @@ -267,7 +268,7 @@ func unusedListenAddr() (_ string, err error) { func tempJSONFile(v interface{}) (string, error) { f, err := ioutil.TempFile("", "temp.json") if err != nil { - return "", fmt.Errorf("temp file: %w", err) + return "", xerrors.Errorf("temp file: %w", err) } defer f.Close() @@ -275,12 +276,12 @@ func tempJSONFile(v interface{}) (string, error) { e.SetIndent("", "\t") err = e.Encode(v) if err != nil { - return "", fmt.Errorf("json encode: %w", err) + return "", xerrors.Errorf("json encode: %w", err) } err = f.Close() if err != nil { - return "", fmt.Errorf("close temp file: %w", err) + return "", xerrors.Errorf("close temp file: %w", err) } return f.Name(), nil diff --git a/close.go b/close.go index c5c51c6e..931160e6 100644 --- a/close.go +++ b/close.go @@ -5,11 +5,12 @@ package websocket import ( "context" "encoding/binary" - "errors" "fmt" "log" "time" + "golang.org/x/xerrors" + "nhooyr.io/websocket/internal/errd" ) @@ -60,7 +61,7 @@ const ( // CloseError is returned when the connection is closed with a status and reason. // -// Use Go 1.13's errors.As to check for this error. +// Use Go 1.13's xerrors.As to check for this error. // Also see the CloseStatus helper. type CloseError struct { Code StatusCode @@ -71,13 +72,13 @@ func (ce CloseError) Error() string { return fmt.Sprintf("status = %v and reason = %q", ce.Code, ce.Reason) } -// CloseStatus is a convenience wrapper around Go 1.13's errors.As to grab +// CloseStatus is a convenience wrapper around Go 1.13's xerrors.As to grab // the status code from a CloseError. // // -1 will be returned if the passed error is nil or not a CloseError. func CloseStatus(err error) StatusCode { var ce CloseError - if errors.As(err, &ce) { + if xerrors.As(err, &ce) { return ce.Code } return -1 @@ -128,7 +129,7 @@ func (c *Conn) writeClose(code StatusCode, reason string) error { c.wroteClose = true c.closeMu.Unlock() if closing { - return errors.New("already wrote close") + return xerrors.New("already wrote close") } ce := CloseError{ @@ -136,7 +137,7 @@ func (c *Conn) writeClose(code StatusCode, reason string) error { Reason: reason, } - c.setCloseErr(fmt.Errorf("sent close frame: %w", ce)) + c.setCloseErr(xerrors.Errorf("sent close frame: %w", ce)) var p []byte if ce.Code != StatusNoStatusRcvd { @@ -185,7 +186,7 @@ func parseClosePayload(p []byte) (CloseError, error) { } if len(p) < 2 { - return CloseError{}, fmt.Errorf("close payload %q too small, cannot even contain the 2 byte status code", p) + return CloseError{}, xerrors.Errorf("close payload %q too small, cannot even contain the 2 byte status code", p) } ce := CloseError{ @@ -194,7 +195,7 @@ func parseClosePayload(p []byte) (CloseError, error) { } if !validWireCloseCode(ce.Code) { - return CloseError{}, fmt.Errorf("invalid status code %v", ce.Code) + return CloseError{}, xerrors.Errorf("invalid status code %v", ce.Code) } return ce, nil @@ -234,11 +235,11 @@ const maxCloseReason = maxControlPayload - 2 func (ce CloseError) bytesErr() ([]byte, error) { if len(ce.Reason) > maxCloseReason { - return nil, fmt.Errorf("reason string max is %v but got %q with length %v", maxCloseReason, ce.Reason, len(ce.Reason)) + return nil, xerrors.Errorf("reason string max is %v but got %q with length %v", maxCloseReason, ce.Reason, len(ce.Reason)) } if !validWireCloseCode(ce.Code) { - return nil, fmt.Errorf("status code %v cannot be set", ce.Code) + return nil, xerrors.Errorf("status code %v cannot be set", ce.Code) } buf := make([]byte, 2+len(ce.Reason)) @@ -255,7 +256,7 @@ func (c *Conn) setCloseErr(err error) { func (c *Conn) setCloseErrLocked(err error) { if c.closeErr == nil { - c.closeErr = fmt.Errorf("WebSocket closed: %w", err) + c.closeErr = xerrors.Errorf("WebSocket closed: %w", err) } } diff --git a/conn.go b/conn.go index a0176495..ab93e4e6 100644 --- a/conn.go +++ b/conn.go @@ -5,13 +5,13 @@ package websocket import ( "bufio" "context" - "errors" - "fmt" "io" "runtime" "strconv" "sync" "sync/atomic" + + "golang.org/x/xerrors" ) // MessageType represents the type of a WebSocket message. @@ -108,7 +108,7 @@ func newConn(cfg connConfig) *Conn { } runtime.SetFinalizer(c, func(c *Conn) { - c.close(errors.New("connection garbage collected")) + c.close(xerrors.New("connection garbage collected")) }) go c.timeoutLoop() @@ -167,10 +167,10 @@ func (c *Conn) timeoutLoop() { case readCtx = <-c.readTimeout: case <-readCtx.Done(): - c.setCloseErr(fmt.Errorf("read timed out: %w", readCtx.Err())) - go c.writeError(StatusPolicyViolation, errors.New("timed out")) + c.setCloseErr(xerrors.Errorf("read timed out: %w", readCtx.Err())) + go c.writeError(StatusPolicyViolation, xerrors.New("timed out")) case <-writeCtx.Done(): - c.close(fmt.Errorf("write timed out: %w", writeCtx.Err())) + c.close(xerrors.Errorf("write timed out: %w", writeCtx.Err())) return } } @@ -192,7 +192,7 @@ func (c *Conn) Ping(ctx context.Context) error { err := c.ping(ctx, strconv.Itoa(int(p))) if err != nil { - return fmt.Errorf("failed to ping: %w", err) + return xerrors.Errorf("failed to ping: %w", err) } return nil } @@ -219,7 +219,7 @@ func (c *Conn) ping(ctx context.Context, p string) error { case <-c.closed: return c.closeErr case <-ctx.Done(): - err := fmt.Errorf("failed to wait for pong: %w", ctx.Err()) + err := xerrors.Errorf("failed to wait for pong: %w", ctx.Err()) c.close(err) return err case <-pong: @@ -244,7 +244,7 @@ func (m *mu) Lock(ctx context.Context) error { case <-m.c.closed: return m.c.closeErr case <-ctx.Done(): - err := fmt.Errorf("failed to acquire lock: %w", ctx.Err()) + err := xerrors.Errorf("failed to acquire lock: %w", ctx.Err()) m.c.close(err) return err case m.ch <- struct{}{}: diff --git a/conn_test.go b/conn_test.go index c8663b47..a65c332c 100644 --- a/conn_test.go +++ b/conn_test.go @@ -4,7 +4,6 @@ package websocket_test import ( "context" - "fmt" "io" "net/http" "net/http/httptest" @@ -14,6 +13,7 @@ import ( "time" "cdr.dev/slog/sloggers/slogtest/assert" + "golang.org/x/xerrors" "nhooyr.io/websocket" ) @@ -67,9 +67,7 @@ func testServer(tb testing.TB, fn func(w http.ResponseWriter, r *http.Request), closeFn2 := wsgrace(s.Config) return s, func() { err := closeFn2() - if err != nil { - tb.Fatal(err) - } + assert.Success(tb, "closeFn", err) } } @@ -96,7 +94,7 @@ func wsgrace(s *http.Server) (closeFn func() error) { err := s.Shutdown(ctx) if err != nil { - return fmt.Errorf("server shutdown failed: %v", err) + return xerrors.Errorf("server shutdown failed: %v", err) } t := time.NewTicker(time.Millisecond * 10) @@ -108,7 +106,7 @@ func wsgrace(s *http.Server) (closeFn func() error) { return nil } case <-ctx.Done(): - return fmt.Errorf("failed to wait for WebSocket connections: %v", ctx.Err()) + return xerrors.Errorf("failed to wait for WebSocket connections: %v", ctx.Err()) } } } diff --git a/dial.go b/dial.go index 58c0a9c5..f53d30ee 100644 --- a/dial.go +++ b/dial.go @@ -8,8 +8,6 @@ import ( "context" "crypto/rand" "encoding/base64" - "errors" - "fmt" "io" "io/ioutil" "net/http" @@ -17,6 +15,8 @@ import ( "strings" "sync" + "golang.org/x/xerrors" + "nhooyr.io/websocket/internal/errd" ) @@ -69,7 +69,7 @@ func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) ( secWebSocketKey, err := secWebSocketKey(rand) if err != nil { - return nil, nil, fmt.Errorf("failed to generate Sec-WebSocket-Key: %w", err) + return nil, nil, xerrors.Errorf("failed to generate Sec-WebSocket-Key: %w", err) } resp, err := handshakeRequest(ctx, urls, opts, secWebSocketKey) @@ -95,7 +95,7 @@ func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) ( rwc, ok := respBody.(io.ReadWriteCloser) if !ok { - return nil, resp, fmt.Errorf("response body is not a io.ReadWriteCloser: %T", respBody) + return nil, resp, xerrors.Errorf("response body is not a io.ReadWriteCloser: %T", respBody) } return newConn(connConfig{ @@ -110,12 +110,12 @@ func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) ( func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, secWebSocketKey string) (*http.Response, error) { if opts.HTTPClient.Timeout > 0 { - return nil, errors.New("use context for cancellation instead of http.Client.Timeout; see https://github.com/nhooyr/websocket/issues/67") + return nil, xerrors.New("use context for cancellation instead of http.Client.Timeout; see https://github.com/nhooyr/websocket/issues/67") } u, err := url.Parse(urls) if err != nil { - return nil, fmt.Errorf("failed to parse url: %w", err) + return nil, xerrors.Errorf("failed to parse url: %w", err) } switch u.Scheme { @@ -124,7 +124,7 @@ func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, secWe case "wss": u.Scheme = "https" default: - return nil, fmt.Errorf("unexpected url scheme: %q", u.Scheme) + return nil, xerrors.Errorf("unexpected url scheme: %q", u.Scheme) } req, _ := http.NewRequestWithContext(ctx, "GET", u.String(), nil) @@ -143,7 +143,7 @@ func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, secWe resp, err := opts.HTTPClient.Do(req) if err != nil { - return nil, fmt.Errorf("failed to send handshake request: %w", err) + return nil, xerrors.Errorf("failed to send handshake request: %w", err) } return resp, nil } @@ -155,26 +155,26 @@ func secWebSocketKey(rr io.Reader) (string, error) { b := make([]byte, 16) _, err := io.ReadFull(rr, b) if err != nil { - return "", fmt.Errorf("failed to read random data from rand.Reader: %w", err) + return "", xerrors.Errorf("failed to read random data from rand.Reader: %w", err) } return base64.StdEncoding.EncodeToString(b), nil } func verifyServerResponse(opts *DialOptions, secWebSocketKey string, resp *http.Response) (*compressionOptions, error) { if resp.StatusCode != http.StatusSwitchingProtocols { - return nil, fmt.Errorf("expected handshake response status code %v but got %v", http.StatusSwitchingProtocols, resp.StatusCode) + return nil, xerrors.Errorf("expected handshake response status code %v but got %v", http.StatusSwitchingProtocols, resp.StatusCode) } if !headerContainsToken(resp.Header, "Connection", "Upgrade") { - return nil, fmt.Errorf("WebSocket protocol violation: Connection header %q does not contain Upgrade", resp.Header.Get("Connection")) + return nil, xerrors.Errorf("WebSocket protocol violation: Connection header %q does not contain Upgrade", resp.Header.Get("Connection")) } if !headerContainsToken(resp.Header, "Upgrade", "WebSocket") { - return nil, fmt.Errorf("WebSocket protocol violation: Upgrade header %q does not contain websocket", resp.Header.Get("Upgrade")) + return nil, xerrors.Errorf("WebSocket protocol violation: Upgrade header %q does not contain websocket", resp.Header.Get("Upgrade")) } if resp.Header.Get("Sec-WebSocket-Accept") != secWebSocketAccept(secWebSocketKey) { - return nil, fmt.Errorf("WebSocket protocol violation: invalid Sec-WebSocket-Accept %q, key %q", + return nil, xerrors.Errorf("WebSocket protocol violation: invalid Sec-WebSocket-Accept %q, key %q", resp.Header.Get("Sec-WebSocket-Accept"), secWebSocketKey, ) @@ -200,7 +200,7 @@ func verifySubprotocol(subprotos []string, resp *http.Response) error { } } - return fmt.Errorf("WebSocket protocol violation: unexpected Sec-WebSocket-Protocol from server: %q", proto) + return xerrors.Errorf("WebSocket protocol violation: unexpected Sec-WebSocket-Protocol from server: %q", proto) } func verifyServerExtensions(h http.Header) (*compressionOptions, error) { @@ -211,7 +211,7 @@ func verifyServerExtensions(h http.Header) (*compressionOptions, error) { ext := exts[0] if ext.name != "permessage-deflate" || len(exts) > 1 { - return nil, fmt.Errorf("WebSocket protcol violation: unsupported extensions from server: %+v", exts[1:]) + return nil, xerrors.Errorf("WebSocket protcol violation: unsupported extensions from server: %+v", exts[1:]) } copts := &compressionOptions{} @@ -222,7 +222,7 @@ func verifyServerExtensions(h http.Header) (*compressionOptions, error) { case "server_no_context_takeover": copts.serverNoContextTakeover = true default: - return nil, fmt.Errorf("unsupported permessage-deflate parameter: %q", p) + return nil, xerrors.Errorf("unsupported permessage-deflate parameter: %q", p) } } diff --git a/dial_test.go b/dial_test.go index 4314f98e..3be52208 100644 --- a/dial_test.go +++ b/dial_test.go @@ -211,9 +211,7 @@ func Test_verifyServerHandshake(t *testing.T) { r := httptest.NewRequest("GET", "/", nil) key, err := secWebSocketKey(rand.Reader) - if err != nil { - t.Fatal(err) - } + assert.Success(t, "secWebSocketKey", err) r.Header.Set("Sec-WebSocket-Key", key) if resp.Header.Get("Sec-WebSocket-Accept") == "" { diff --git a/example_echo_test.go b/example_echo_test.go index cd195d2e..1daec8a5 100644 --- a/example_echo_test.go +++ b/example_echo_test.go @@ -4,7 +4,6 @@ package websocket_test import ( "context" - "errors" "fmt" "io" "log" @@ -13,6 +12,7 @@ import ( "time" "golang.org/x/time/rate" + "golang.org/x/xerrors" "nhooyr.io/websocket" "nhooyr.io/websocket/wsjson" @@ -78,7 +78,7 @@ func echoServer(w http.ResponseWriter, r *http.Request) error { if c.Subprotocol() != "echo" { c.Close(websocket.StatusPolicyViolation, "client must speak the echo subprotocol") - return errors.New("client does not speak echo sub protocol") + return xerrors.New("client does not speak echo sub protocol") } l := rate.NewLimiter(rate.Every(time.Millisecond*100), 10) @@ -88,7 +88,7 @@ func echoServer(w http.ResponseWriter, r *http.Request) error { return nil } if err != nil { - return fmt.Errorf("failed to echo with %v: %w", r.RemoteAddr, err) + return xerrors.Errorf("failed to echo with %v: %w", r.RemoteAddr, err) } } } @@ -117,7 +117,7 @@ func echo(ctx context.Context, c *websocket.Conn, l *rate.Limiter) error { _, err = io.Copy(w, r) if err != nil { - return fmt.Errorf("failed to io.Copy: %w", err) + return xerrors.Errorf("failed to io.Copy: %w", err) } err = w.Close() diff --git a/go.mod b/go.mod index 01ec18f7..5dc9b261 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module nhooyr.io/websocket -go 1.13 +go 1.12 require ( cdr.dev/slog v1.3.0 @@ -10,4 +10,5 @@ require ( github.com/golang/protobuf v1.3.2 github.com/gorilla/websocket v1.4.1 golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 + golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 ) diff --git a/internal/errd/wrap.go b/internal/errd/wrap.go index 6e779131..20de7743 100644 --- a/internal/errd/wrap.go +++ b/internal/errd/wrap.go @@ -1,14 +1,12 @@ package errd -import ( - "fmt" -) +import "golang.org/x/xerrors" -// Wrap wraps err with fmt.Errorf if err is non nil. +// Wrap wraps err with xerrors.Errorf if err is non nil. // Intended for use with defer and a named error return. // Inspired by https://github.com/golang/go/issues/32676. func Wrap(err *error, f string, v ...interface{}) { if *err != nil { - *err = fmt.Errorf(f+": %w", append(v, *err)...) + *err = xerrors.Errorf(f+": %w", append(v, *err)...) } } diff --git a/netconn.go b/netconn.go index 64aadf0b..a2d8f4f3 100644 --- a/netconn.go +++ b/netconn.go @@ -2,12 +2,13 @@ package websocket import ( "context" - "fmt" "io" "math" "net" "sync" "time" + + "golang.org/x/xerrors" ) // NetConn converts a *websocket.Conn into a net.Conn. @@ -107,7 +108,7 @@ func (c *netConn) Read(p []byte) (int, error) { return 0, err } if typ != c.msgType { - err := fmt.Errorf("unexpected frame type read (expected %v): %v", c.msgType, typ) + err := xerrors.Errorf("unexpected frame type read (expected %v): %v", c.msgType, typ) c.c.Close(StatusUnsupportedData, err.Error()) return 0, err } diff --git a/read.go b/read.go index 517022b5..4b94f067 100644 --- a/read.go +++ b/read.go @@ -4,14 +4,14 @@ package websocket import ( "context" - "errors" - "fmt" "io" "io/ioutil" "strings" "sync/atomic" "time" + "golang.org/x/xerrors" + "nhooyr.io/websocket/internal/errd" ) @@ -79,10 +79,6 @@ func newMsgReader(c *Conn) *msgReader { } mr.limitReader = newLimitReader(c, readerFunc(mr.read), 32768) - if c.flate() && mr.flateContextTakeover() { - mr.initFlateReader() - } - return mr } @@ -125,13 +121,13 @@ func (c *Conn) readLoop(ctx context.Context) (header, error) { } if h.rsv1 && c.readRSV1Illegal(h) || h.rsv2 || h.rsv3 { - err := fmt.Errorf("received header with unexpected rsv bits set: %v:%v:%v", h.rsv1, h.rsv2, h.rsv3) + err := xerrors.Errorf("received header with unexpected rsv bits set: %v:%v:%v", h.rsv1, h.rsv2, h.rsv3) c.writeError(StatusProtocolError, err) return header{}, err } if !c.client && !h.masked { - return header{}, errors.New("received unmasked frame from client") + return header{}, xerrors.New("received unmasked frame from client") } switch h.opcode { @@ -142,12 +138,12 @@ func (c *Conn) readLoop(ctx context.Context) (header, error) { if h.opcode == opClose && CloseStatus(err) != -1 { return header{}, err } - return header{}, fmt.Errorf("failed to handle control frame %v: %w", h.opcode, err) + return header{}, xerrors.Errorf("failed to handle control frame %v: %w", h.opcode, err) } case opContinuation, opText, opBinary: return h, nil default: - err := fmt.Errorf("received unknown opcode %v", h.opcode) + err := xerrors.Errorf("received unknown opcode %v", h.opcode) c.writeError(StatusProtocolError, err) return header{}, err } @@ -198,7 +194,7 @@ func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { case <-ctx.Done(): return n, ctx.Err() default: - err = fmt.Errorf("failed to read frame payload: %w", err) + err = xerrors.Errorf("failed to read frame payload: %w", err) c.close(err) return n, err } @@ -215,13 +211,13 @@ func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { func (c *Conn) handleControl(ctx context.Context, h header) (err error) { if h.payloadLength < 0 || h.payloadLength > maxControlPayload { - err := fmt.Errorf("received control frame payload with invalid length: %d", h.payloadLength) + err := xerrors.Errorf("received control frame payload with invalid length: %d", h.payloadLength) c.writeError(StatusProtocolError, err) return err } if !h.fin { - err := errors.New("received fragmented control frame") + err := xerrors.New("received fragmented control frame") c.writeError(StatusProtocolError, err) return err } @@ -258,12 +254,12 @@ func (c *Conn) handleControl(ctx context.Context, h header) (err error) { ce, err := parseClosePayload(b) if err != nil { - err = fmt.Errorf("received invalid close payload: %w", err) + err = xerrors.Errorf("received invalid close payload: %w", err) c.writeError(StatusProtocolError, err) return err } - err = fmt.Errorf("received close frame: %w", ce) + err = xerrors.Errorf("received close frame: %w", ce) c.setCloseErr(err) c.writeClose(ce.Code, ce.Reason) c.close(err) @@ -280,7 +276,7 @@ func (c *Conn) reader(ctx context.Context) (_ MessageType, _ io.Reader, err erro defer c.readMu.Unlock() if !c.msgReader.fin { - return 0, nil, errors.New("previous message not read to completion") + return 0, nil, xerrors.New("previous message not read to completion") } h, err := c.readLoop(ctx) @@ -289,7 +285,7 @@ func (c *Conn) reader(ctx context.Context) (_ MessageType, _ io.Reader, err erro } if h.opcode == opContinuation { - err := errors.New("received continuation frame without text or binary frame") + err := xerrors.New("received continuation frame without text or binary frame") c.writeError(StatusProtocolError, err) return 0, nil, err } @@ -347,7 +343,7 @@ func (mr *msgReader) Read(p []byte) (n int, err error) { } errd.Wrap(&err, "failed to read") - if errors.Is(err, io.EOF) { + if xerrors.Is(err, io.EOF) { err = io.EOF } }() @@ -386,7 +382,7 @@ func (mr *msgReader) read(p []byte) (int, error) { return 0, err } if h.opcode != opContinuation { - err := errors.New("received new data message without finishing the previous message") + err := xerrors.New("received new data message without finishing the previous message") mr.c.writeError(StatusProtocolError, err) return 0, err } @@ -434,7 +430,7 @@ func (lr *limitReader) reset() { func (lr *limitReader) Read(p []byte) (int, error) { if lr.n <= 0 { - err := fmt.Errorf("read limited at %v bytes", lr.limit.Load()) + err := xerrors.Errorf("read limited at %v bytes", lr.limit.Load()) lr.c.writeError(StatusMessageTooBig, err) return 0, err } @@ -448,6 +444,8 @@ func (lr *limitReader) Read(p []byte) (int, error) { } type atomicInt64 struct { + // We do not use atomic.Load/StoreInt64 since it does not + // work on 32 bit computers but we need 64 bit integers. i atomic.Value } diff --git a/write.go b/write.go index 33d20c1d..db47ddbc 100644 --- a/write.go +++ b/write.go @@ -8,11 +8,11 @@ import ( "context" "crypto/rand" "encoding/binary" - "errors" - "fmt" "io" "time" + "golang.org/x/xerrors" + "nhooyr.io/websocket/internal/errd" ) @@ -28,7 +28,7 @@ import ( func (c *Conn) Writer(ctx context.Context, typ MessageType) (io.WriteCloser, error) { w, err := c.writer(ctx, typ) if err != nil { - return nil, fmt.Errorf("failed to get writer: %w", err) + return nil, xerrors.Errorf("failed to get writer: %w", err) } return w, nil } @@ -42,7 +42,7 @@ func (c *Conn) Writer(ctx context.Context, typ MessageType) (io.WriteCloser, err func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { _, err := c.write(ctx, typ, p) if err != nil { - return fmt.Errorf("failed to write msg: %w", err) + return xerrors.Errorf("failed to write msg: %w", err) } return nil } @@ -55,10 +55,6 @@ func newMsgWriter(c *Conn) *msgWriter { mw.trimWriter = &trimLastFourBytesWriter{ w: writerFunc(mw.write), } - if c.flate() && mw.flateContextTakeover() { - mw.ensureFlateWriter() - } - return mw } @@ -92,7 +88,7 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error if !c.flate() { // Fast single frame path. defer c.msgWriter.mu.Unlock() - return c.writeFrame(ctx, true, c.msgWriter.opcode, p) + return c.writeFrame(ctx, true, false, c.msgWriter.opcode, p) } n, err := mw.Write(p) @@ -113,7 +109,6 @@ type msgWriter struct { opcode opcode closed bool - // TODO pass down into writeFrame flate bool trimWriter *trimLastFourBytesWriter flateWriter *flate.Writer @@ -137,7 +132,7 @@ func (mw *msgWriter) Write(p []byte) (_ int, err error) { defer errd.Wrap(&err, "failed to write") if mw.closed { - return 0, errors.New("cannot use closed writer") + return 0, xerrors.New("cannot use closed writer") } if mw.c.flate() { @@ -157,9 +152,9 @@ func (mw *msgWriter) Write(p []byte) (_ int, err error) { } func (mw *msgWriter) write(p []byte) (int, error) { - n, err := mw.c.writeFrame(mw.ctx, false, mw.opcode, p) + n, err := mw.c.writeFrame(mw.ctx, false, mw.flate, mw.opcode, p) if err != nil { - return n, fmt.Errorf("failed to write data frame: %w", err) + return n, xerrors.Errorf("failed to write data frame: %w", err) } mw.opcode = opContinuation return n, nil @@ -170,20 +165,20 @@ func (mw *msgWriter) Close() (err error) { defer errd.Wrap(&err, "failed to close writer") if mw.closed { - return errors.New("cannot use closed writer") + return xerrors.New("cannot use closed writer") } mw.closed = true if mw.flate { err = mw.flateWriter.Flush() if err != nil { - return fmt.Errorf("failed to flush flate writer: %w", err) + return xerrors.Errorf("failed to flush flate writer: %w", err) } } - _, err = mw.c.writeFrame(mw.ctx, true, mw.opcode, nil) + _, err = mw.c.writeFrame(mw.ctx, true, mw.flate, mw.opcode, nil) if err != nil { - return fmt.Errorf("failed to write fin frame: %w", err) + return xerrors.Errorf("failed to write fin frame: %w", err) } if mw.c.flate() && !mw.flateContextTakeover() && mw.flateWriter != nil { @@ -207,15 +202,15 @@ func (c *Conn) writeControl(ctx context.Context, opcode opcode, p []byte) error ctx, cancel := context.WithTimeout(ctx, time.Second*5) defer cancel() - _, err := c.writeFrame(ctx, true, opcode, p) + _, err := c.writeFrame(ctx, true, false, opcode, p) if err != nil { - return fmt.Errorf("failed to write control frame %v: %w", opcode, err) + return xerrors.Errorf("failed to write control frame %v: %w", opcode, err) } return nil } // frame handles all writes to the connection. -func (c *Conn) writeFrame(ctx context.Context, fin bool, opcode opcode, p []byte) (int, error) { +func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opcode, p []byte) (int, error) { err := c.writeFrameMu.Lock(ctx) if err != nil { return 0, err @@ -236,12 +231,12 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, opcode opcode, p []byte c.writeHeader.masked = true err = binary.Read(rand.Reader, binary.LittleEndian, &c.writeHeader.maskKey) if err != nil { - return 0, fmt.Errorf("failed to generate masking key: %w", err) + return 0, xerrors.Errorf("failed to generate masking key: %w", err) } } c.writeHeader.rsv1 = false - if c.flate() && (opcode == opText || opcode == opBinary) { + if flate && (opcode == opText || opcode == opBinary) { c.writeHeader.rsv1 = true } @@ -258,7 +253,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, opcode opcode, p []byte if c.writeHeader.fin { err = c.bw.Flush() if err != nil { - return n, fmt.Errorf("failed to flush: %w", err) + return n, xerrors.Errorf("failed to flush: %w", err) } } diff --git a/ws_js.go b/ws_js.go index 2aaef738..3ce6f34d 100644 --- a/ws_js.go +++ b/ws_js.go @@ -3,14 +3,14 @@ package websocket // import "nhooyr.io/websocket" import ( "bytes" "context" - "errors" - "fmt" "io" "reflect" "runtime" "sync" "syscall/js" + "golang.org/x/xerrors" + "nhooyr.io/websocket/internal/bpool" "nhooyr.io/websocket/internal/wsjs" ) @@ -55,7 +55,7 @@ func (c *Conn) close(err error, wasClean bool) { runtime.SetFinalizer(c, nil) if !wasClean { - err = fmt.Errorf("unclean connection close: %w", err) + err = xerrors.Errorf("unclean connection close: %w", err) } c.setCloseErr(err) c.closeWasClean = wasClean @@ -100,7 +100,7 @@ func (c *Conn) init() { }) runtime.SetFinalizer(c, func(c *Conn) { - c.setCloseErr(errors.New("connection garbage collected")) + c.setCloseErr(xerrors.New("connection garbage collected")) c.closeWithInternal() }) } @@ -113,15 +113,15 @@ func (c *Conn) closeWithInternal() { // The maximum time spent waiting is bounded by the context. func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { if c.isReadClosed.Load() == 1 { - return 0, nil, errors.New("WebSocket connection read closed") + return 0, nil, xerrors.New("WebSocket connection read closed") } typ, p, err := c.read(ctx) if err != nil { - return 0, nil, fmt.Errorf("failed to read: %w", err) + return 0, nil, xerrors.Errorf("failed to read: %w", err) } if int64(len(p)) > c.msgReadLimit.Load() { - err := fmt.Errorf("read limited at %v bytes", c.msgReadLimit) + err := xerrors.Errorf("read limited at %v bytes", c.msgReadLimit) c.Close(StatusMessageTooBig, err.Error()) return 0, nil, err } @@ -174,7 +174,7 @@ func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { // to match the Go API. It can only error if the message type // is unexpected or the passed bytes contain invalid UTF-8 for // MessageText. - err := fmt.Errorf("failed to write: %w", err) + err := xerrors.Errorf("failed to write: %w", err) c.setCloseErr(err) c.closeWithInternal() return err @@ -192,7 +192,7 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) error { case MessageText: return c.ws.SendText(string(p)) default: - return fmt.Errorf("unexpected message type: %v", typ) + return xerrors.Errorf("unexpected message type: %v", typ) } } @@ -203,7 +203,7 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) error { func (c *Conn) Close(code StatusCode, reason string) error { err := c.exportedClose(code, reason) if err != nil { - return fmt.Errorf("failed to close WebSocket: %w", err) + return xerrors.Errorf("failed to close WebSocket: %w", err) } return nil } @@ -212,13 +212,13 @@ func (c *Conn) exportedClose(code StatusCode, reason string) error { c.closingMu.Lock() defer c.closingMu.Unlock() - ce := fmt.Errorf("sent close: %w", CloseError{ + ce := xerrors.Errorf("sent close: %w", CloseError{ Code: code, Reason: reason, }) if c.isClosed() { - return fmt.Errorf("tried to close with %q but connection already closed: %w", ce, c.closeErr) + return xerrors.Errorf("tried to close with %q but connection already closed: %w", ce, c.closeErr) } c.setCloseErr(ce) @@ -253,7 +253,7 @@ type DialOptions struct { func Dial(ctx context.Context, url string, opts *DialOptions) (*Conn, error) { c, err := dial(ctx, url, opts) if err != nil { - return nil, resp, fmt.Errorf("failed to WebSocket dial %q: %w", url, err) + return nil, resp, xerrors.Errorf("failed to WebSocket dial %q: %w", url, err) } return c, nil } @@ -325,25 +325,25 @@ type writer struct { func (w writer) Write(p []byte) (int, error) { if w.closed { - return 0, errors.New("cannot write to closed writer") + return 0, xerrors.New("cannot write to closed writer") } n, err := w.b.Write(p) if err != nil { - return n, fmt.Errorf("failed to write message: %w", err) + return n, xerrors.Errorf("failed to write message: %w", err) } return n, nil } func (w writer) Close() error { if w.closed { - return errors.New("cannot close closed writer") + return xerrors.New("cannot close closed writer") } w.closed = true defer bpool.Put(w.b) err := w.c.Write(w.ctx, w.typ, w.b.Bytes()) if err != nil { - return fmt.Errorf("failed to close writer: %w", err) + return xerrors.Errorf("failed to close writer: %w", err) } return nil } @@ -368,7 +368,7 @@ func (c *Conn) SetReadLimit(n int64) { func (c *Conn) setCloseErr(err error) { c.closeErrOnce.Do(func() { - c.closeErr = fmt.Errorf("WebSocket closed: %w", err) + c.closeErr = xerrors.Errorf("WebSocket closed: %w", err) }) } diff --git a/wsjson/wsjson.go b/wsjson/wsjson.go index 99996a69..e6f06a2f 100644 --- a/wsjson/wsjson.go +++ b/wsjson/wsjson.go @@ -4,7 +4,8 @@ package wsjson // import "nhooyr.io/websocket/wsjson" import ( "context" "encoding/json" - "fmt" + + "golang.org/x/xerrors" "nhooyr.io/websocket" "nhooyr.io/websocket/internal/bpool" @@ -27,7 +28,7 @@ func read(ctx context.Context, c *websocket.Conn, v interface{}) (err error) { if typ != websocket.MessageText { c.Close(websocket.StatusUnsupportedData, "expected text message") - return fmt.Errorf("expected text message for JSON but got: %v", typ) + return xerrors.Errorf("expected text message for JSON but got: %v", typ) } b := bpool.Get() @@ -41,7 +42,7 @@ func read(ctx context.Context, c *websocket.Conn, v interface{}) (err error) { err = json.Unmarshal(b.Bytes(), v) if err != nil { c.Close(websocket.StatusInvalidFramePayloadData, "failed to unmarshal JSON") - return fmt.Errorf("failed to unmarshal JSON: %w", err) + return xerrors.Errorf("failed to unmarshal JSON: %w", err) } return nil @@ -65,7 +66,7 @@ func write(ctx context.Context, c *websocket.Conn, v interface{}) (err error) { // a copy of the byte slice but Encoder does as it directly writes to w. err = json.NewEncoder(w).Encode(v) if err != nil { - return fmt.Errorf("failed to marshal JSON: %w", err) + return xerrors.Errorf("failed to marshal JSON: %w", err) } return w.Close() diff --git a/wspb/wspb.go b/wspb/wspb.go index e43042d5..06ac3368 100644 --- a/wspb/wspb.go +++ b/wspb/wspb.go @@ -4,9 +4,9 @@ package wspb // import "nhooyr.io/websocket/wspb" import ( "bytes" "context" - "fmt" "github.com/golang/protobuf/proto" + "golang.org/x/xerrors" "nhooyr.io/websocket" "nhooyr.io/websocket/internal/bpool" @@ -29,7 +29,7 @@ func read(ctx context.Context, c *websocket.Conn, v proto.Message) (err error) { if typ != websocket.MessageBinary { c.Close(websocket.StatusUnsupportedData, "expected binary message") - return fmt.Errorf("expected binary message for protobuf but got: %v", typ) + return xerrors.Errorf("expected binary message for protobuf but got: %v", typ) } b := bpool.Get() @@ -43,7 +43,7 @@ func read(ctx context.Context, c *websocket.Conn, v proto.Message) (err error) { err = proto.Unmarshal(b.Bytes(), v) if err != nil { c.Close(websocket.StatusInvalidFramePayloadData, "failed to unmarshal protobuf") - return fmt.Errorf("failed to unmarshal protobuf: %w", err) + return xerrors.Errorf("failed to unmarshal protobuf: %w", err) } return nil @@ -66,7 +66,7 @@ func write(ctx context.Context, c *websocket.Conn, v proto.Message) (err error) err = pb.Marshal(v) if err != nil { - return fmt.Errorf("failed to marshal protobuf: %w", err) + return xerrors.Errorf("failed to marshal protobuf: %w", err) } return c.Write(ctx, websocket.MessageBinary, pb.Bytes()) From b6b56b7499ee09561b87ad3de17709a59f839952 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 5 Feb 2020 00:21:26 -0600 Subject: [PATCH 238/519] Both modes seem to work :) --- accept.go | 14 ++++---- assert_test.go | 3 +- compress.go | 58 +++++++++++++++------------------ compress_test.go | 45 ++++++++++++++++++++++++++ conn.go | 41 ++++++++++++++---------- conn_test.go | 7 ++-- dial.go | 13 ++++---- read.go | 74 ++++++++++++++++++++++-------------------- write.go | 83 ++++++++++++++++++++++++------------------------ 9 files changed, 196 insertions(+), 142 deletions(-) create mode 100644 compress_test.go diff --git a/accept.go b/accept.go index ac7f2de1..0394fa6d 100644 --- a/accept.go +++ b/accept.go @@ -111,12 +111,14 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con brw.Reader.Reset(io.MultiReader(bytes.NewReader(b), netConn)) return newConn(connConfig{ - subprotocol: w.Header().Get("Sec-WebSocket-Protocol"), - rwc: netConn, - client: false, - copts: copts, - br: brw.Reader, - bw: brw.Writer, + subprotocol: w.Header().Get("Sec-WebSocket-Protocol"), + rwc: netConn, + client: false, + copts: copts, + flateThreshold: opts.CompressionOptions.Threshold, + + br: brw.Reader, + bw: brw.Writer, }), nil } diff --git a/assert_test.go b/assert_test.go index cd78fbb3..5307ee8e 100644 --- a/assert_test.go +++ b/assert_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "cdr.dev/slog" "cdr.dev/slog/sloggers/slogtest/assert" "nhooyr.io/websocket" @@ -33,7 +34,7 @@ func assertJSONEcho(t *testing.T, ctx context.Context, c *websocket.Conn, n int) } func assertJSONRead(t *testing.T, ctx context.Context, c *websocket.Conn, exp interface{}) { - t.Helper() + slog.Helper() var act interface{} err := wsjson.Read(ctx, c, &act) diff --git a/compress.go b/compress.go index fd2535cc..efd89b33 100644 --- a/compress.go +++ b/compress.go @@ -148,12 +148,12 @@ func (tw *trimLastFourBytesWriter) Write(p []byte) (int, error) { var flateReaderPool sync.Pool -func getFlateReader(r io.Reader) io.Reader { +func getFlateReader(r io.Reader, dict []byte) io.Reader { fr, ok := flateReaderPool.Get().(io.Reader) if !ok { - return flate.NewReader(r) + return flate.NewReaderDict(r, dict) } - fr.(flate.Resetter).Reset(r, nil) + fr.(flate.Resetter).Reset(r, dict) return fr } @@ -163,10 +163,10 @@ func putFlateReader(fr io.Reader) { var flateWriterPool sync.Pool -func getFlateWriter(w io.Writer, dict []byte) *flate.Writer { +func getFlateWriter(w io.Writer) *flate.Writer { fw, ok := flateWriterPool.Get().(*flate.Writer) if !ok { - fw, _ = flate.NewWriterDict(w, flate.BestSpeed, dict) + fw, _ = flate.NewWriter(w, flate.BestSpeed) return fw } fw.Reset(w) @@ -177,40 +177,32 @@ func putFlateWriter(w *flate.Writer) { flateWriterPool.Put(w) } -type slidingWindowReader struct { - window []byte - - r io.Reader +type slidingWindow struct { + r io.Reader + buf []byte } -func (r slidingWindowReader) Read(p []byte) (int, error) { - n, err := r.r.Read(p) - p = p[:n] - - r.append(p) - - return n, err +func newSlidingWindow(n int) *slidingWindow { + return &slidingWindow{ + buf: make([]byte, 0, n), + } } -func (r slidingWindowReader) append(p []byte) { - if len(r.window) <= cap(r.window) { - r.window = append(r.window, p...) +func (w *slidingWindow) write(p []byte) { + if len(p) >= cap(w.buf) { + w.buf = w.buf[:cap(w.buf)] + p = p[len(p)-cap(w.buf):] + copy(w.buf, p) + return } - if len(p) > cap(r.window) { - p = p[len(p)-cap(r.window):] + left := cap(w.buf) - len(w.buf) + if left < len(p) { + // We need to shift spaceNeeded bytes from the end to make room for p at the end. + spaceNeeded := len(p) - left + copy(w.buf, w.buf[spaceNeeded:]) + w.buf = w.buf[:len(w.buf)-spaceNeeded] } - // p now contains at max the last window bytes - // so we need to be able to append all of it to r.window. - // Shift as many bytes from r.window as needed. - - // Maximum window size minus current window minus extra gives - // us the number of bytes that need to be shifted. - off := len(r.window) + len(p) - cap(r.window) - - r.window = append(r.window[:0], r.window[off:]...) - copy(r.window, r.window[off:]) - copy(r.window[len(r.window)-len(p):], p) - return + w.buf = append(w.buf, p...) } diff --git a/compress_test.go b/compress_test.go new file mode 100644 index 00000000..6edfcb1a --- /dev/null +++ b/compress_test.go @@ -0,0 +1,45 @@ +package websocket + +import ( + "crypto/rand" + "encoding/base64" + "math/big" + "strings" + "testing" + + "cdr.dev/slog/sloggers/slogtest/assert" +) + +func Test_slidingWindow(t *testing.T) { + t.Parallel() + + const testCount = 99 + const maxWindow = 99999 + for i := 0; i < testCount; i++ { + input := randStr(t, maxWindow) + windowLength := randInt(t, maxWindow) + r := newSlidingWindow(windowLength) + r.write([]byte(input)) + + if cap(r.buf) != windowLength { + t.Fatalf("sliding window length changed somehow: %q and windowLength %d", input, windowLength) + } + assert.True(t, "hasSuffix", strings.HasSuffix(input, string(r.buf))) + } +} + +func randStr(t *testing.T, max int) string { + n := randInt(t, max) + + b := make([]byte, n) + _, err := rand.Read(b) + assert.Success(t, "rand.Read", err) + + return base64.StdEncoding.EncodeToString(b) +} + +func randInt(t *testing.T, max int) int { + x, err := rand.Int(rand.Reader, big.NewInt(int64(max))) + assert.Success(t, "rand.Int", err) + return int(x.Int64()) +} diff --git a/conn.go b/conn.go index ab93e4e6..2d36123f 100644 --- a/conn.go +++ b/conn.go @@ -38,12 +38,13 @@ const ( // On any error from any method, the connection is closed // with an appropriate reason. type Conn struct { - subprotocol string - rwc io.ReadWriteCloser - client bool - copts *compressionOptions - br *bufio.Reader - bw *bufio.Writer + subprotocol string + rwc io.ReadWriteCloser + client bool + copts *compressionOptions + flateThreshold int + br *bufio.Reader + bw *bufio.Writer readTimeout chan context.Context writeTimeout chan context.Context @@ -71,10 +72,11 @@ type Conn struct { } type connConfig struct { - subprotocol string - rwc io.ReadWriteCloser - client bool - copts *compressionOptions + subprotocol string + rwc io.ReadWriteCloser + client bool + copts *compressionOptions + flateThreshold int br *bufio.Reader bw *bufio.Writer @@ -82,10 +84,11 @@ type connConfig struct { func newConn(cfg connConfig) *Conn { c := &Conn{ - subprotocol: cfg.subprotocol, - rwc: cfg.rwc, - client: cfg.client, - copts: cfg.copts, + subprotocol: cfg.subprotocol, + rwc: cfg.rwc, + client: cfg.client, + copts: cfg.copts, + flateThreshold: cfg.flateThreshold, br: cfg.br, bw: cfg.bw, @@ -96,6 +99,12 @@ func newConn(cfg connConfig) *Conn { closed: make(chan struct{}), activePings: make(map[string]chan<- struct{}), } + if c.flateThreshold == 0 { + c.flateThreshold = 256 + if c.writeNoContextTakeOver() { + c.flateThreshold = 512 + } + } c.readMu = newMu(c) c.writeFrameMu = newMu(c) @@ -145,12 +154,10 @@ func (c *Conn) close(err error) { } c.msgWriter.close() + c.msgReader.close() if c.client { - c.readMu.Lock(context.Background()) putBufioReader(c.br) - c.readMu.Unlock() } - c.msgReader.close() }() } diff --git a/conn_test.go b/conn_test.go index a65c332c..7186da8a 100644 --- a/conn_test.go +++ b/conn_test.go @@ -27,13 +27,15 @@ func TestConn(t *testing.T) { Subprotocols: []string{"echo"}, InsecureSkipVerify: true, CompressionOptions: websocket.CompressionOptions{ - Mode: websocket.CompressionNoContextTakeover, + Mode: websocket.CompressionContextTakeover, + Threshold: 1, }, }) assert.Success(t, "accept", err) defer c.Close(websocket.StatusInternalError, "") err = echoLoop(r.Context(), c) + t.Logf("server: %v", err) assertCloseStatus(t, websocket.StatusNormalClosure, err) }, false) defer closeFn() @@ -46,7 +48,8 @@ func TestConn(t *testing.T) { opts := &websocket.DialOptions{ Subprotocols: []string{"echo"}, CompressionOptions: websocket.CompressionOptions{ - Mode: websocket.CompressionNoContextTakeover, + Mode: websocket.CompressionContextTakeover, + Threshold: 1, }, } opts.HTTPClient = s.Client() diff --git a/dial.go b/dial.go index f53d30ee..4557602e 100644 --- a/dial.go +++ b/dial.go @@ -99,12 +99,13 @@ func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) ( } return newConn(connConfig{ - subprotocol: resp.Header.Get("Sec-WebSocket-Protocol"), - rwc: rwc, - client: true, - copts: copts, - br: getBufioReader(rwc), - bw: getBufioWriter(rwc), + subprotocol: resp.Header.Get("Sec-WebSocket-Protocol"), + rwc: rwc, + client: true, + copts: copts, + flateThreshold: opts.CompressionOptions.Threshold, + br: getBufioReader(rwc), + bw: getBufioWriter(rwc), }), resp, nil } diff --git a/read.go b/read.go index 4b94f067..73ec0b32 100644 --- a/read.go +++ b/read.go @@ -72,25 +72,40 @@ func (c *Conn) SetReadLimit(n int64) { c.msgReader.limitReader.limit.Store(n) } +const defaultReadLimit = 32768 + func newMsgReader(c *Conn) *msgReader { mr := &msgReader{ c: c, fin: true, } - mr.limitReader = newLimitReader(c, readerFunc(mr.read), 32768) + mr.limitReader = newLimitReader(c, readerFunc(mr.read), defaultReadLimit) return mr } -func (mr *msgReader) initFlateReader() { - mr.flateReader = getFlateReader(readerFunc(mr.read)) +func (mr *msgReader) ensureFlate() { + if mr.flateContextTakeover() && mr.dict == nil { + mr.dict = newSlidingWindow(32768) + } + + if mr.flateContextTakeover() { + mr.flateReader = getFlateReader(readerFunc(mr.read), mr.dict.buf) + } else { + mr.flateReader = getFlateReader(readerFunc(mr.read), nil) + } mr.limitReader.r = mr.flateReader } +func (mr *msgReader) returnFlateReader() { + if mr.flateReader != nil { + putFlateReader(mr.flateReader) + mr.flateReader = nil + } +} + func (mr *msgReader) close() { mr.c.readMu.Lock(context.Background()) - defer mr.c.readMu.Unlock() - mr.returnFlateReader() } @@ -299,10 +314,11 @@ type msgReader struct { c *Conn ctx context.Context - deflate bool + flate bool flateReader io.Reader - deflateTail strings.Reader + flateTail strings.Reader limitReader *limitReader + dict *slidingWindow fin bool payloadLength int64 @@ -311,12 +327,10 @@ type msgReader struct { func (mr *msgReader) reset(ctx context.Context, h header) { mr.ctx = ctx - mr.deflate = h.rsv1 - if mr.deflate { - if !mr.flateContextTakeover() { - mr.initFlateReader() - } - mr.deflateTail.Reset(deflateMessageTail) + mr.flate = h.rsv1 + if mr.flate { + mr.ensureFlate() + mr.flateTail.Reset(deflateMessageTail) } mr.limitReader.reset() @@ -331,18 +345,10 @@ func (mr *msgReader) setFrame(h header) { func (mr *msgReader) Read(p []byte) (n int, err error) { defer func() { - r := recover() - if r != nil { - if r != "ANMOL" { - panic(r) - } + errd.Wrap(&err, "failed to read") + if xerrors.Is(err, io.ErrUnexpectedEOF) && mr.fin && mr.flate { err = io.EOF - if !mr.flateContextTakeover() { - mr.returnFlateReader() - } } - - errd.Wrap(&err, "failed to read") if xerrors.Is(err, io.EOF) { err = io.EOF } @@ -354,25 +360,23 @@ func (mr *msgReader) Read(p []byte) (n int, err error) { } defer mr.c.readMu.Unlock() - return mr.limitReader.Read(p) -} - -func (mr *msgReader) returnFlateReader() { - if mr.flateReader != nil { - putFlateReader(mr.flateReader) - mr.flateReader = nil + n, err = mr.limitReader.Read(p) + if mr.flateContextTakeover() { + p = p[:n] + mr.dict.write(p) } + return n, err } func (mr *msgReader) read(p []byte) (int, error) { if mr.payloadLength == 0 { if mr.fin { - if mr.deflate { - if mr.deflateTail.Len() == 0 { - panic("ANMOL") + if mr.flate { + n, err := mr.flateTail.Read(p) + if xerrors.Is(err, io.EOF) { + mr.returnFlateReader() } - n, _ := mr.deflateTail.Read(p) - return n, nil + return n, err } return 0, io.EOF } diff --git a/write.go b/write.go index db47ddbc..a7fa5f5a 100644 --- a/write.go +++ b/write.go @@ -37,8 +37,8 @@ func (c *Conn) Writer(ctx context.Context, typ MessageType) (io.WriteCloser, err // // See the Writer method if you want to stream a message. // -// If compression is disabled, then it is guaranteed to write the message -// in a single frame. +// If compression is disabled or the threshold is not met, then it +// will write the message in a single frame. func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { _, err := c.write(ctx, typ, p) if err != nil { @@ -47,20 +47,38 @@ func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { return nil } +type msgWriter struct { + c *Conn + + mu *mu + + ctx context.Context + opcode opcode + closed bool + flate bool + + trimWriter *trimLastFourBytesWriter + flateWriter *flate.Writer +} + func newMsgWriter(c *Conn) *msgWriter { mw := &msgWriter{ c: c, mu: newMu(c), } - mw.trimWriter = &trimLastFourBytesWriter{ - w: writerFunc(mw.write), - } return mw } -func (mw *msgWriter) ensureFlateWriter() { +func (mw *msgWriter) ensureFlate() { if mw.flateWriter == nil { - mw.flateWriter = getFlateWriter(mw.trimWriter, nil) + if mw.trimWriter == nil { + mw.trimWriter = &trimLastFourBytesWriter{ + w: writerFunc(mw.write), + } + } + + mw.flateWriter = getFlateWriter(mw.trimWriter) + mw.flate = true } } @@ -85,8 +103,7 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error return 0, err } - if !c.flate() { - // Fast single frame path. + if !c.flate() || len(p) < c.flateThreshold { defer c.msgWriter.mu.Unlock() return c.writeFrame(ctx, true, false, c.msgWriter.opcode, p) } @@ -100,20 +117,6 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error return n, err } -type msgWriter struct { - c *Conn - - mu *mu - - ctx context.Context - opcode opcode - closed bool - - flate bool - trimWriter *trimLastFourBytesWriter - flateWriter *flate.Writer -} - func (mw *msgWriter) reset(ctx context.Context, typ MessageType) error { err := mw.mu.Lock(ctx) if err != nil { @@ -127,6 +130,13 @@ func (mw *msgWriter) reset(ctx context.Context, typ MessageType) error { return nil } +func (mw *msgWriter) returnFlateWriter() { + if mw.flateWriter != nil { + putFlateWriter(mw.flateWriter) + mw.flateWriter = nil + } +} + // Write writes the given bytes to the WebSocket connection. func (mw *msgWriter) Write(p []byte) (_ int, err error) { defer errd.Wrap(&err, "failed to write") @@ -135,16 +145,10 @@ func (mw *msgWriter) Write(p []byte) (_ int, err error) { return 0, xerrors.New("cannot use closed writer") } - if mw.c.flate() { - if !mw.flate { - mw.flate = true - - if !mw.flateContextTakeover() { - mw.ensureFlateWriter() - } - mw.trimWriter.reset() - } - + // TODO can make threshold detection robust across writes by writing to buffer + if mw.flate || + mw.c.flate() && len(p) >= mw.c.flateThreshold { + mw.ensureFlate() return mw.flateWriter.Write(p) } @@ -181,21 +185,16 @@ func (mw *msgWriter) Close() (err error) { return xerrors.Errorf("failed to write fin frame: %w", err) } - if mw.c.flate() && !mw.flateContextTakeover() && mw.flateWriter != nil { - putFlateWriter(mw.flateWriter) - mw.flateWriter = nil + if mw.c.flate() && !mw.flateContextTakeover() { + mw.returnFlateWriter() } - mw.mu.Unlock() return nil } func (mw *msgWriter) close() { - if mw.flateWriter != nil && mw.flateContextTakeover() { - mw.mu.Lock(context.Background()) - putFlateWriter(mw.flateWriter) - mw.flateWriter = nil - } + mw.mu.Lock(context.Background()) + mw.returnFlateWriter() } func (c *Conn) writeControl(ctx context.Context, opcode opcode, p []byte) error { From 9e32354f05c6a12cfbdf1256f43c7e05551116e7 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 5 Feb 2020 21:53:03 -0600 Subject: [PATCH 239/519] Fix randString method in tests --- assert_test.go | 1 + ci/image/Dockerfile | 2 +- conn_test.go | 52 ++++++++++++++++++++++++++++++++++--------- internal/errd/wrap.go | 34 ++++++++++++++++++++++++++-- write.go | 2 +- 5 files changed, 76 insertions(+), 15 deletions(-) diff --git a/assert_test.go b/assert_test.go index 5307ee8e..6cfd9264 100644 --- a/assert_test.go +++ b/assert_test.go @@ -45,6 +45,7 @@ func assertJSONRead(t *testing.T, ctx context.Context, c *websocket.Conn, exp in func randString(t *testing.T, n int) string { s := strings.ToValidUTF8(string(randBytes(t, n)), "_") + s = strings.ReplaceAll(s, "\x00", "_") if len(s) > n { return s[:n] } diff --git a/ci/image/Dockerfile b/ci/image/Dockerfile index 070c50e6..88c96502 100644 --- a/ci/image/Dockerfile +++ b/ci/image/Dockerfile @@ -1,7 +1,7 @@ FROM golang:1 RUN apt-get update -RUN apt-get install -y chromium +RUN apt-get install -y chromium npm ENV GOFLAGS="-mod=readonly" ENV PAGER=cat diff --git a/conn_test.go b/conn_test.go index 7186da8a..4720cba9 100644 --- a/conn_test.go +++ b/conn_test.go @@ -18,45 +18,71 @@ import ( "nhooyr.io/websocket" ) +func TestFuzz(t *testing.T) { + t.Parallel() + + s, closeFn := testServer(t, func(w http.ResponseWriter, r *http.Request) { + c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + CompressionOptions: websocket.CompressionOptions{ + Mode: websocket.CompressionContextTakeover, + }, + }) + assert.Success(t, "accept", err) + defer c.Close(websocket.StatusInternalError, "") + + err = echoLoop(r.Context(), c) + assertCloseStatus(t, websocket.StatusNormalClosure, err) + }, false) + defer closeFn() + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + opts := &websocket.DialOptions{ + CompressionOptions: websocket.CompressionOptions{ + Mode: websocket.CompressionContextTakeover, + }, + } + opts.HTTPClient = s.Client() + + c, _, err := websocket.Dial(ctx, wsURL(s), opts) + assert.Success(t, "dial", err) + assertJSONEcho(t, ctx, c, 8393) +} + func TestConn(t *testing.T) { t.Parallel() t.Run("json", func(t *testing.T) { s, closeFn := testServer(t, func(w http.ResponseWriter, r *http.Request) { c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ - Subprotocols: []string{"echo"}, - InsecureSkipVerify: true, + Subprotocols: []string{"echo"}, CompressionOptions: websocket.CompressionOptions{ - Mode: websocket.CompressionContextTakeover, - Threshold: 1, + Mode: websocket.CompressionContextTakeover, }, }) assert.Success(t, "accept", err) defer c.Close(websocket.StatusInternalError, "") err = echoLoop(r.Context(), c) - t.Logf("server: %v", err) assertCloseStatus(t, websocket.StatusNormalClosure, err) }, false) defer closeFn() - wsURL := strings.Replace(s.URL, "http", "ws", 1) - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() opts := &websocket.DialOptions{ Subprotocols: []string{"echo"}, CompressionOptions: websocket.CompressionOptions{ - Mode: websocket.CompressionContextTakeover, - Threshold: 1, + Mode: websocket.CompressionContextTakeover, }, } opts.HTTPClient = s.Client() - c, _, err := websocket.Dial(ctx, wsURL, opts) + c, _, err := websocket.Dial(ctx, wsURL(s), opts) assert.Success(t, "dial", err) - assertJSONEcho(t, ctx, c, 2) + assertJSONEcho(t, ctx, c, 8393) }) } @@ -149,3 +175,7 @@ func echoLoop(ctx context.Context, c *websocket.Conn) error { } } } + +func wsURL(s *httptest.Server) string { + return strings.Replace(s.URL, "http", "ws", 1) +} diff --git a/internal/errd/wrap.go b/internal/errd/wrap.go index 20de7743..ed0b7754 100644 --- a/internal/errd/wrap.go +++ b/internal/errd/wrap.go @@ -1,12 +1,42 @@ package errd -import "golang.org/x/xerrors" +import ( + "fmt" + + "golang.org/x/xerrors" +) + +type wrapError struct { + msg string + err error + frame xerrors.Frame +} + +func (e *wrapError) Error() string { + return fmt.Sprint(e) +} + +func (e *wrapError) Format(s fmt.State, v rune) { xerrors.FormatError(e, s, v) } + +func (e *wrapError) FormatError(p xerrors.Printer) (next error) { + p.Print(e.msg) + e.frame.Format(p) + return e.err +} + +func (e *wrapError) Unwrap() error { + return e.err +} // Wrap wraps err with xerrors.Errorf if err is non nil. // Intended for use with defer and a named error return. // Inspired by https://github.com/golang/go/issues/32676. func Wrap(err *error, f string, v ...interface{}) { if *err != nil { - *err = xerrors.Errorf(f+": %w", append(v, *err)...) + *err = &wrapError{ + msg: fmt.Sprintf(f, v...), + err: *err, + frame: xerrors.Caller(1), + } } } diff --git a/write.go b/write.go index a7fa5f5a..4a756fa9 100644 --- a/write.go +++ b/write.go @@ -145,7 +145,7 @@ func (mw *msgWriter) Write(p []byte) (_ int, err error) { return 0, xerrors.New("cannot use closed writer") } - // TODO can make threshold detection robust across writes by writing to buffer + // TODO can make threshold detection robust across writes by writing to bufio writer if mw.flate || mw.c.flate() && len(p) >= mw.c.flateThreshold { mw.ensureFlate() From 78da35ec5b221d5ec664ee9cbf0a8fb034d46f4c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 7 Feb 2020 00:58:57 -0600 Subject: [PATCH 240/519] Get test with multiple messages working --- README.md | 2 +- assert_test.go | 79 +++++++++++++++++++++++++++++++----------- autobahn_test.go | 16 +++++---- conn_test.go | 89 +++++++++++++++++------------------------------- example_test.go | 2 -- read.go | 11 +++--- write.go | 1 + ws_js_test.go | 2 +- 8 files changed, 109 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index e958d2ab..2569383a 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ go get nhooyr.io/websocket - [net.Conn](https://godoc.org/nhooyr.io/websocket#NetConn) wrapper - [Ping pong](https://godoc.org/nhooyr.io/websocket#Conn.Ping) API - [RFC 7692](https://tools.ietf.org/html/rfc7692) permessage-deflate compression -- Can target [Wasm](https://godoc.org/nhooyr.io/websocket#hdr-Wasm) +- Compile to [Wasm](https://godoc.org/nhooyr.io/websocket#hdr-Wasm) ## Roadmap diff --git a/assert_test.go b/assert_test.go index 6cfd9264..3727d995 100644 --- a/assert_test.go +++ b/assert_test.go @@ -3,10 +3,15 @@ package websocket_test import ( "context" "crypto/rand" + "fmt" + "net/http" + "net/http/httptest" "strings" "testing" + "time" "cdr.dev/slog" + "cdr.dev/slog/sloggers/slogtest" "cdr.dev/slog/sloggers/slogtest/assert" "nhooyr.io/websocket" @@ -20,26 +25,31 @@ func randBytes(t *testing.T, n int) []byte { return b } -func assertJSONEcho(t *testing.T, ctx context.Context, c *websocket.Conn, n int) { - t.Helper() - defer c.Close(websocket.StatusInternalError, "") +func echoJSON(t *testing.T, c *websocket.Conn, n int) { + slog.Helper() - exp := randString(t, n) - err := wsjson.Write(ctx, c, exp) - assert.Success(t, "wsjson.Write", err) + s := randString(t, n) + writeJSON(t, c, s) + readJSON(t, c, s) +} - assertJSONRead(t, ctx, c, exp) +func writeJSON(t *testing.T, c *websocket.Conn, v interface{}) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() - c.Close(websocket.StatusNormalClosure, "") + err := wsjson.Write(ctx, c, v) + assert.Success(t, "wsjson.Write", err) } -func assertJSONRead(t *testing.T, ctx context.Context, c *websocket.Conn, exp interface{}) { +func readJSON(t *testing.T, c *websocket.Conn, exp interface{}) { slog.Helper() + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + var act interface{} err := wsjson.Read(ctx, c, &act) assert.Success(t, "wsjson.Read", err) - assert.Equal(t, "json", exp, act) } @@ -58,7 +68,7 @@ func randString(t *testing.T, n int) string { } func assertEcho(t *testing.T, ctx context.Context, c *websocket.Conn, typ websocket.MessageType, n int) { - t.Helper() + slog.Helper() p := randBytes(t, n) err := c.Write(ctx, typ, p) @@ -72,17 +82,46 @@ func assertEcho(t *testing.T, ctx context.Context, c *websocket.Conn, typ websoc } func assertSubprotocol(t *testing.T, c *websocket.Conn, exp string) { - t.Helper() + slog.Helper() assert.Equal(t, "subprotocol", exp, c.Subprotocol()) } -func assertCloseStatus(t *testing.T, exp websocket.StatusCode, err error) { - t.Helper() - defer func() { - if t.Failed() { - t.Logf("error: %+v", err) - } - }() - assert.Equal(t, "closeStatus", exp, websocket.CloseStatus(err)) +func assertCloseStatus(t testing.TB, exp websocket.StatusCode, err error) { + slog.Helper() + + if websocket.CloseStatus(err) == -1 { + slogtest.Fatal(t, "expected websocket.CloseError", slogType(err), slog.Error(err)) + } + if websocket.CloseStatus(err) != exp { + slogtest.Error(t, "unexpected close status", + slog.F("exp", exp), + slog.F("act", err), + ) + } + +} + +func acceptWebSocket(t testing.TB, r *http.Request, w http.ResponseWriter, opts *websocket.AcceptOptions) *websocket.Conn { + c, err := websocket.Accept(w, r, opts) + assert.Success(t, "websocket.Accept", err) + return c +} + +func dialWebSocket(t testing.TB, s *httptest.Server, opts *websocket.DialOptions) (*websocket.Conn, *http.Response) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + if opts == nil { + opts = &websocket.DialOptions{} + } + opts.HTTPClient = s.Client() + + c, resp, err := websocket.Dial(ctx, wsURL(s), opts) + assert.Success(t, "websocket.Dial", err) + return c, resp +} + +func slogType(v interface{}) slog.Field { + return slog.F("type", fmt.Sprintf("%T", v)) } diff --git a/autobahn_test.go b/autobahn_test.go index bcbf8671..dd9887f6 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -9,6 +9,7 @@ import ( "io/ioutil" "net" "net/http" + "net/http/httptest" "os" "os/exec" "strconv" @@ -53,15 +54,18 @@ func TestAutobahn(t *testing.T) { func testServerAutobahn(t *testing.T) { t.Parallel() - s, closeFn := testServer(t, func(w http.ResponseWriter, r *http.Request) { - c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c := acceptWebSocket(t, r, w, &websocket.AcceptOptions{ Subprotocols: []string{"echo"}, }) - assert.Success(t, "accept", err) - err = echoLoop(r.Context(), c) + err := echoLoop(r.Context(), c) assertCloseStatus(t, websocket.StatusNormalClosure, err) - }, false) - defer closeFn() + })) + closeFn := wsgrace(s.Config) + defer func() { + err := closeFn() + assert.Success(t, "closeFn", err) + }() specFile, err := tempJSONFile(map[string]interface{}{ "outdir": "ci/out/wstestServerReports", diff --git a/conn_test.go b/conn_test.go index 4720cba9..6f6b8d5d 100644 --- a/conn_test.go +++ b/conn_test.go @@ -4,7 +4,9 @@ package websocket_test import ( "context" + "crypto/rand" "io" + "math/big" "net/http" "net/http/httptest" "strings" @@ -18,77 +20,32 @@ import ( "nhooyr.io/websocket" ) -func TestFuzz(t *testing.T) { - t.Parallel() - - s, closeFn := testServer(t, func(w http.ResponseWriter, r *http.Request) { - c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ - CompressionOptions: websocket.CompressionOptions{ - Mode: websocket.CompressionContextTakeover, - }, - }) - assert.Success(t, "accept", err) - defer c.Close(websocket.StatusInternalError, "") - - err = echoLoop(r.Context(), c) - assertCloseStatus(t, websocket.StatusNormalClosure, err) - }, false) - defer closeFn() - - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - - opts := &websocket.DialOptions{ - CompressionOptions: websocket.CompressionOptions{ - Mode: websocket.CompressionContextTakeover, - }, - } - opts.HTTPClient = s.Client() - - c, _, err := websocket.Dial(ctx, wsURL(s), opts) - assert.Success(t, "dial", err) - assertJSONEcho(t, ctx, c, 8393) -} - func TestConn(t *testing.T) { t.Parallel() t.Run("json", func(t *testing.T) { - s, closeFn := testServer(t, func(w http.ResponseWriter, r *http.Request) { - c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ - Subprotocols: []string{"echo"}, - CompressionOptions: websocket.CompressionOptions{ - Mode: websocket.CompressionContextTakeover, - }, - }) - assert.Success(t, "accept", err) - defer c.Close(websocket.StatusInternalError, "") - - err = echoLoop(r.Context(), c) - assertCloseStatus(t, websocket.StatusNormalClosure, err) - }, false) + t.Parallel() + + s, closeFn := testEchoLoop(t) defer closeFn() - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() + c, _ := dialWebSocket(t, s, nil) + defer c.Close(websocket.StatusInternalError, "") - opts := &websocket.DialOptions{ - Subprotocols: []string{"echo"}, - CompressionOptions: websocket.CompressionOptions{ - Mode: websocket.CompressionContextTakeover, - }, + c.SetReadLimit(1 << 30) + + for i := 0; i < 10; i++ { + n := randInt(t, 1_048_576) + echoJSON(t, c, n) } - opts.HTTPClient = s.Client() - c, _, err := websocket.Dial(ctx, wsURL(s), opts) - assert.Success(t, "dial", err) - assertJSONEcho(t, ctx, c, 8393) + c.Close(websocket.StatusNormalClosure, "") }) } -func testServer(tb testing.TB, fn func(w http.ResponseWriter, r *http.Request), tls bool) (s *httptest.Server, closeFn func()) { +func testServer(tb testing.TB, fn func(w http.ResponseWriter, r *http.Request)) (s *httptest.Server, closeFn func()) { h := http.HandlerFunc(fn) - if tls { + if randInt(tb, 2) == 1 { s = httptest.NewTLSServer(h) } else { s = httptest.NewServer(h) @@ -179,3 +136,19 @@ func echoLoop(ctx context.Context, c *websocket.Conn) error { func wsURL(s *httptest.Server) string { return strings.Replace(s.URL, "http", "ws", 1) } + +func testEchoLoop(t testing.TB) (*httptest.Server, func()) { + return testServer(t, func(w http.ResponseWriter, r *http.Request) { + c := acceptWebSocket(t, r, w, nil) + defer c.Close(websocket.StatusInternalError, "") + + err := echoLoop(r.Context(), c) + assertCloseStatus(t, websocket.StatusNormalClosure, err) + }) +} + +func randInt(t testing.TB, max int) int { + x, err := rand.Int(rand.Reader, big.NewInt(int64(max))) + assert.Success(t, "rand.Int", err) + return int(x.Int64()) +} diff --git a/example_test.go b/example_test.go index bc603aff..1842b765 100644 --- a/example_test.go +++ b/example_test.go @@ -33,8 +33,6 @@ func ExampleAccept() { return } - log.Printf("received: %v", v) - c.Close(websocket.StatusNormalClosure, "") }) diff --git a/read.go b/read.go index 73ec0b32..7e74894a 100644 --- a/read.go +++ b/read.go @@ -95,6 +95,7 @@ func (mr *msgReader) ensureFlate() { mr.flateReader = getFlateReader(readerFunc(mr.read), nil) } mr.limitReader.r = mr.flateReader + mr.flateTail.Reset(deflateMessageTail) } func (mr *msgReader) returnFlateReader() { @@ -328,12 +329,12 @@ type msgReader struct { func (mr *msgReader) reset(ctx context.Context, h header) { mr.ctx = ctx mr.flate = h.rsv1 + mr.limitReader.reset(readerFunc(mr.read)) + if mr.flate { mr.ensureFlate() - mr.flateTail.Reset(deflateMessageTail) } - mr.limitReader.reset() mr.setFrame(h) } @@ -423,13 +424,13 @@ func newLimitReader(c *Conn, r io.Reader, limit int64) *limitReader { c: c, } lr.limit.Store(limit) - lr.r = r - lr.reset() + lr.reset(r) return lr } -func (lr *limitReader) reset() { +func (lr *limitReader) reset(r io.Reader) { lr.n = lr.limit.Load() + lr.r = r } func (lr *limitReader) Read(p []byte) (int, error) { diff --git a/write.go b/write.go index 4a756fa9..34543486 100644 --- a/write.go +++ b/write.go @@ -76,6 +76,7 @@ func (mw *msgWriter) ensureFlate() { w: writerFunc(mw.write), } } + mw.trimWriter.reset() mw.flateWriter = getFlateWriter(mw.trimWriter) mw.flate = true diff --git a/ws_js_test.go b/ws_js_test.go index 6e87480b..9f725a57 100644 --- a/ws_js_test.go +++ b/ws_js_test.go @@ -24,7 +24,7 @@ func TestEcho(t *testing.T) { assertSubprotocol(t, c, "echo") assert.Equalf(t, &http.Response{}, resp, "http.Response") - assertJSONEcho(t, ctx, c, 1024) + echoJSON(t, ctx, c, 1024) assertEcho(t, ctx, c, websocket.MessageBinary, 1024) err = c.Close(websocket.StatusNormalClosure, "") From d09268649e33ce5b3afde49006d39508a28cbe12 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 8 Feb 2020 15:29:08 -0500 Subject: [PATCH 241/519] Autobahn tests fully pass :) --- assert_test.go | 15 ---- autobahn_test.go | 76 ++------------------ conn.go | 2 +- conn_test.go | 177 ++++++++++++++++++++++++++--------------------- read.go | 6 +- write.go | 31 +++++---- 6 files changed, 127 insertions(+), 180 deletions(-) diff --git a/assert_test.go b/assert_test.go index 3727d995..22814e3b 100644 --- a/assert_test.go +++ b/assert_test.go @@ -5,7 +5,6 @@ import ( "crypto/rand" "fmt" "net/http" - "net/http/httptest" "strings" "testing" "time" @@ -108,20 +107,6 @@ func acceptWebSocket(t testing.TB, r *http.Request, w http.ResponseWriter, opts return c } -func dialWebSocket(t testing.TB, s *httptest.Server, opts *websocket.DialOptions) (*websocket.Conn, *http.Response) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) - defer cancel() - - if opts == nil { - opts = &websocket.DialOptions{} - } - opts.HTTPClient = s.Client() - - c, resp, err := websocket.Dial(ctx, wsURL(s), opts) - assert.Success(t, "websocket.Dial", err) - return c, resp -} - func slogType(v interface{}) slog.Field { return slog.F("type", fmt.Sprintf("%T", v)) } diff --git a/autobahn_test.go b/autobahn_test.go index dd9887f6..71d22be7 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -8,9 +8,6 @@ import ( "fmt" "io/ioutil" "net" - "net/http" - "net/http/httptest" - "os" "os/exec" "strconv" "strings" @@ -32,69 +29,14 @@ var excludedAutobahnCases = []string{ // We skip the tests related to requestMaxWindowBits as that is unimplemented due // to limitations in compress/flate. See https://github.com/golang/go/issues/3155 "13.3.*", "13.4.*", "13.5.*", "13.6.*", - - "12.*", - "13.*", } var autobahnCases = []string{"*"} -// https://github.com/crossbario/autobahn-python/tree/master/wstest func TestAutobahn(t *testing.T) { t.Parallel() - if os.Getenv("AUTOBAHN") == "" { - t.Skip("Set $AUTOBAHN to run tests against the autobahn test suite") - } - - t.Run("server", testServerAutobahn) - t.Run("client", testClientAutobahn) -} - -func testServerAutobahn(t *testing.T) { - t.Parallel() - - s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c := acceptWebSocket(t, r, w, &websocket.AcceptOptions{ - Subprotocols: []string{"echo"}, - }) - err := echoLoop(r.Context(), c) - assertCloseStatus(t, websocket.StatusNormalClosure, err) - })) - closeFn := wsgrace(s.Config) - defer func() { - err := closeFn() - assert.Success(t, "closeFn", err) - }() - - specFile, err := tempJSONFile(map[string]interface{}{ - "outdir": "ci/out/wstestServerReports", - "servers": []interface{}{ - map[string]interface{}{ - "agent": "main", - "url": strings.Replace(s.URL, "http", "ws", 1), - }, - }, - "cases": autobahnCases, - "exclude-cases": excludedAutobahnCases, - }) - assert.Success(t, "tempJSONFile", err) - - ctx, cancel := context.WithTimeout(context.Background(), time.Minute*10) - defer cancel() - - args := []string{"--mode", "fuzzingclient", "--spec", specFile} - wstest := exec.CommandContext(ctx, "wstest", args...) - _, err = wstest.CombinedOutput() - assert.Success(t, "wstest", err) - - checkWSTestIndex(t, "./ci/out/wstestServerReports/index.json") -} - -func testClientAutobahn(t *testing.T) { - t.Parallel() - - ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*15) defer cancel() wstestURL, closeFn, err := wstestClientServer(ctx) @@ -108,27 +50,17 @@ func testClientAutobahn(t *testing.T) { assert.Success(t, "wstestCaseCount", err) t.Run("cases", func(t *testing.T) { - // Max 8 cases running at a time. - mu := make(chan struct{}, 8) - for i := 1; i <= cases; i++ { i := i t.Run("", func(t *testing.T) { - t.Parallel() - - mu <- struct{}{} - defer func() { - <-mu - }() - - ctx, cancel := context.WithTimeout(ctx, time.Second*45) + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) defer cancel() c, _, err := websocket.Dial(ctx, fmt.Sprintf(wstestURL+"/runCase?case=%v&agent=main", i), nil) assert.Success(t, "autobahn dial", err) err = echoLoop(ctx, c) - t.Logf("echoLoop: %+v", err) + t.Logf("echoLoop: %v", err) }) } }) @@ -174,7 +106,7 @@ func wstestClientServer(ctx context.Context) (url string, closeFn func(), err er return "", nil, xerrors.Errorf("failed to write spec: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*15) defer func() { if err != nil { cancel() diff --git a/conn.go b/conn.go index 2d36123f..163802bb 100644 --- a/conn.go +++ b/conn.go @@ -99,7 +99,7 @@ func newConn(cfg connConfig) *Conn { closed: make(chan struct{}), activePings: make(map[string]chan<- struct{}), } - if c.flateThreshold == 0 { + if c.flate() && c.flateThreshold == 0 { c.flateThreshold = 256 if c.writeNoContextTakeOver() { c.flateThreshold = 512 diff --git a/conn_test.go b/conn_test.go index 6f6b8d5d..aceac3fd 100644 --- a/conn_test.go +++ b/conn_test.go @@ -3,99 +3,70 @@ package websocket_test import ( + "bufio" "context" "crypto/rand" "io" "math/big" + "net" "net/http" "net/http/httptest" - "strings" - "sync/atomic" "testing" "time" "cdr.dev/slog/sloggers/slogtest/assert" - "golang.org/x/xerrors" "nhooyr.io/websocket" ) +func goFn(fn func()) func() { + done := make(chan struct{}) + go func() { + defer close(done) + fn() + }() + + return func() { + <-done + } +} + func TestConn(t *testing.T) { t.Parallel() t.Run("json", func(t *testing.T) { t.Parallel() - s, closeFn := testEchoLoop(t) - defer closeFn() + for i := 0; i < 1; i++ { + t.Run("", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() - c, _ := dialWebSocket(t, s, nil) - defer c.Close(websocket.StatusInternalError, "") - - c.SetReadLimit(1 << 30) - - for i := 0; i < 10; i++ { - n := randInt(t, 1_048_576) - echoJSON(t, c, n) - } + c1, c2 := websocketPipe(t) - c.Close(websocket.StatusNormalClosure, "") - }) -} - -func testServer(tb testing.TB, fn func(w http.ResponseWriter, r *http.Request)) (s *httptest.Server, closeFn func()) { - h := http.HandlerFunc(fn) - if randInt(tb, 2) == 1 { - s = httptest.NewTLSServer(h) - } else { - s = httptest.NewServer(h) - } - closeFn2 := wsgrace(s.Config) - return s, func() { - err := closeFn2() - assert.Success(tb, "closeFn", err) - } -} + wait := goFn(func() { + err := echoLoop(ctx, c1) + assertCloseStatus(t, websocket.StatusNormalClosure, err) + }) + defer wait() -// grace wraps s.Handler to gracefully shutdown WebSocket connections. -// The returned function must be used to close the server instead of s.Close. -func wsgrace(s *http.Server) (closeFn func() error) { - h := s.Handler - var conns int64 - s.Handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - atomic.AddInt64(&conns, 1) - defer atomic.AddInt64(&conns, -1) + c2.SetReadLimit(1 << 30) - ctx, cancel := context.WithTimeout(r.Context(), time.Second*5) - defer cancel() - - r = r.WithContext(ctx) + for i := 0; i < 10; i++ { + n := randInt(t, 131_072) + echoJSON(t, c2, n) + } - h.ServeHTTP(w, r) + c2.Close(websocket.StatusNormalClosure, "") + }) + } }) +} - return func() error { - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - - err := s.Shutdown(ctx) - if err != nil { - return xerrors.Errorf("server shutdown failed: %v", err) - } +type writerFunc func(p []byte) (int, error) - t := time.NewTicker(time.Millisecond * 10) - defer t.Stop() - for { - select { - case <-t.C: - if atomic.LoadInt64(&conns) == 0 { - return nil - } - case <-ctx.Done(): - return xerrors.Errorf("failed to wait for WebSocket connections: %v", ctx.Err()) - } - } - } +func (f writerFunc) Write(p []byte) (int, error) { + return f(p) } // echoLoop echos every msg received from c until an error @@ -133,18 +104,8 @@ func echoLoop(ctx context.Context, c *websocket.Conn) error { } } -func wsURL(s *httptest.Server) string { - return strings.Replace(s.URL, "http", "ws", 1) -} - -func testEchoLoop(t testing.TB) (*httptest.Server, func()) { - return testServer(t, func(w http.ResponseWriter, r *http.Request) { - c := acceptWebSocket(t, r, w, nil) - defer c.Close(websocket.StatusInternalError, "") - - err := echoLoop(r.Context(), c) - assertCloseStatus(t, websocket.StatusNormalClosure, err) - }) +func randBool(t testing.TB) bool { + return randInt(t, 2) == 1 } func randInt(t testing.TB, max int) int { @@ -152,3 +113,65 @@ func randInt(t testing.TB, max int) int { assert.Success(t, "rand.Int", err) return int(x.Int64()) } + +type testHijacker struct { + *httptest.ResponseRecorder + serverConn net.Conn + hijacked chan struct{} +} + +var _ http.Hijacker = testHijacker{} + +func (hj testHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) { + close(hj.hijacked) + return hj.serverConn, bufio.NewReadWriter(bufio.NewReader(hj.serverConn), bufio.NewWriter(hj.serverConn)), nil +} + +func websocketPipe(t *testing.T) (*websocket.Conn, *websocket.Conn) { + var serverConn *websocket.Conn + tt := testTransport{ + h: func(w http.ResponseWriter, r *http.Request) { + serverConn = acceptWebSocket(t, r, w, nil) + }, + } + + dialOpts := &websocket.DialOptions{ + HTTPClient: &http.Client{ + Transport: tt, + }, + } + + clientConn, _, err := websocket.Dial(context.Background(), "ws://example.com", dialOpts) + assert.Success(t, "websocket.Dial", err) + + if randBool(t) { + return serverConn, clientConn + } + return clientConn, serverConn +} + +type testTransport struct { + h http.HandlerFunc +} + +func (t testTransport) RoundTrip(r *http.Request) (*http.Response, error) { + clientConn, serverConn := net.Pipe() + + hj := testHijacker{ + ResponseRecorder: httptest.NewRecorder(), + serverConn: serverConn, + hijacked: make(chan struct{}), + } + + done := make(chan struct{}) + t.h.ServeHTTP(hj, r) + + select { + case <-hj.hijacked: + resp := hj.ResponseRecorder.Result() + resp.Body = clientConn + return resp, nil + case <-done: + return hj.ResponseRecorder.Result(), nil + } +} diff --git a/read.go b/read.go index 7e74894a..b681a944 100644 --- a/read.go +++ b/read.go @@ -84,7 +84,7 @@ func newMsgReader(c *Conn) *msgReader { return mr } -func (mr *msgReader) ensureFlate() { +func (mr *msgReader) resetFlate() { if mr.flateContextTakeover() && mr.dict == nil { mr.dict = newSlidingWindow(32768) } @@ -332,7 +332,7 @@ func (mr *msgReader) reset(ctx context.Context, h header) { mr.limitReader.reset(readerFunc(mr.read)) if mr.flate { - mr.ensureFlate() + mr.resetFlate() } mr.setFrame(h) @@ -362,7 +362,7 @@ func (mr *msgReader) Read(p []byte) (n int, err error) { defer mr.c.readMu.Unlock() n, err = mr.limitReader.Read(p) - if mr.flateContextTakeover() { + if mr.flate && mr.flateContextTakeover() { p = p[:n] mr.dict.write(p) } diff --git a/write.go b/write.go index 34543486..70656b9f 100644 --- a/write.go +++ b/write.go @@ -70,17 +70,17 @@ func newMsgWriter(c *Conn) *msgWriter { } func (mw *msgWriter) ensureFlate() { - if mw.flateWriter == nil { - if mw.trimWriter == nil { - mw.trimWriter = &trimLastFourBytesWriter{ - w: writerFunc(mw.write), - } + if mw.trimWriter == nil { + mw.trimWriter = &trimLastFourBytesWriter{ + w: writerFunc(mw.write), } - mw.trimWriter.reset() + } + if mw.flateWriter == nil { mw.flateWriter = getFlateWriter(mw.trimWriter) - mw.flate = true } + + mw.flate = true } func (mw *msgWriter) flateContextTakeover() bool { @@ -128,6 +128,11 @@ func (mw *msgWriter) reset(ctx context.Context, typ MessageType) error { mw.ctx = ctx mw.opcode = opcode(typ) mw.flate = false + + if mw.trimWriter != nil { + mw.trimWriter.reset() + } + return nil } @@ -146,9 +151,8 @@ func (mw *msgWriter) Write(p []byte) (_ int, err error) { return 0, xerrors.New("cannot use closed writer") } - // TODO can make threshold detection robust across writes by writing to bufio writer - if mw.flate || - mw.c.flate() && len(p) >= mw.c.flateThreshold { + // TODO Write to buffer to detect whether to enable flate or not for this message. + if mw.c.flate() { mw.ensureFlate() return mw.flateWriter.Write(p) } @@ -172,7 +176,6 @@ func (mw *msgWriter) Close() (err error) { if mw.closed { return xerrors.New("cannot use closed writer") } - mw.closed = true if mw.flate { err = mw.flateWriter.Flush() @@ -181,12 +184,16 @@ func (mw *msgWriter) Close() (err error) { } } + // We set closed after flushing the flate writer to ensure Write + // can succeed. + mw.closed = true + _, err = mw.c.writeFrame(mw.ctx, true, mw.flate, mw.opcode, nil) if err != nil { return xerrors.Errorf("failed to write fin frame: %w", err) } - if mw.c.flate() && !mw.flateContextTakeover() { + if mw.flate && !mw.flateContextTakeover() { mw.returnFlateWriter() } mw.mu.Unlock() From 6975801d4df4be481b0a76ae48928c402496df45 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 8 Feb 2020 22:14:00 -0500 Subject: [PATCH 242/519] Fix race in tests --- assert_test.go | 2 +- autobahn_test.go | 5 +++++ conn_test.go | 15 +++++---------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/assert_test.go b/assert_test.go index 22814e3b..a51b2c3d 100644 --- a/assert_test.go +++ b/assert_test.go @@ -28,7 +28,7 @@ func echoJSON(t *testing.T, c *websocket.Conn, n int) { slog.Helper() s := randString(t, n) - writeJSON(t, c, s) + go writeJSON(t, c, s) readJSON(t, c, s) } diff --git a/autobahn_test.go b/autobahn_test.go index 71d22be7..d730cf4a 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -8,6 +8,7 @@ import ( "fmt" "io/ioutil" "net" + "os" "os/exec" "strconv" "strings" @@ -36,6 +37,10 @@ var autobahnCases = []string{"*"} func TestAutobahn(t *testing.T) { t.Parallel() + if os.Getenv("AUTOBAHN_TEST") == "" { + t.SkipNow() + } + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*15) defer cancel() diff --git a/conn_test.go b/conn_test.go index aceac3fd..f1361adc 100644 --- a/conn_test.go +++ b/conn_test.go @@ -39,7 +39,7 @@ func TestConn(t *testing.T) { for i := 0; i < 1; i++ { t.Run("", func(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() c1, c2 := websocketPipe(t) @@ -49,6 +49,7 @@ func TestConn(t *testing.T) { assertCloseStatus(t, websocket.StatusNormalClosure, err) }) defer wait() + defer cancel() c2.SetReadLimit(1 << 30) @@ -63,12 +64,6 @@ func TestConn(t *testing.T) { }) } -type writerFunc func(p []byte) (int, error) - -func (f writerFunc) Write(p []byte) (int, error) { - return f(p) -} - // echoLoop echos every msg received from c until an error // occurs or the context expires. // The read limit is set to 1 << 30. @@ -104,7 +99,7 @@ func echoLoop(ctx context.Context, c *websocket.Conn) error { } } -func randBool(t testing.TB) bool { +func randBool(t testing.TB) bool { return randInt(t, 2) == 1 } @@ -117,7 +112,7 @@ func randInt(t testing.TB, max int) int { type testHijacker struct { *httptest.ResponseRecorder serverConn net.Conn - hijacked chan struct{} + hijacked chan struct{} } var _ http.Hijacker = testHijacker{} @@ -154,7 +149,7 @@ type testTransport struct { h http.HandlerFunc } -func (t testTransport) RoundTrip(r *http.Request) (*http.Response, error) { +func (t testTransport) RoundTrip(r *http.Request) (*http.Response, error) { clientConn, serverConn := net.Pipe() hj := testHijacker{ From bbaf469750cf0996a4a7bd1b6ddcf01f88943c3d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 8 Feb 2020 22:28:13 -0500 Subject: [PATCH 243/519] Fix test step --- .github/workflows/ci.yml | 2 +- go.mod | 12 ++++++++++-- go.sum | 21 +++++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 865c67f0..074e5246 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: with: args: make test env: - COVERALLS_TOKEN: ${{ secrets.github_token }} + COVERALLS_TOKEN: ${{ secrets.COVERALLS_TOKEN }} - name: Upload coverage.html uses: actions/upload-artifact@master with: diff --git a/go.mod b/go.mod index 5dc9b261..ee1708a2 100644 --- a/go.mod +++ b/go.mod @@ -4,11 +4,19 @@ go 1.12 require ( cdr.dev/slog v1.3.0 + github.com/alecthomas/chroma v0.7.1 // indirect + github.com/fatih/color v1.9.0 // indirect github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee // indirect github.com/gobwas/pool v0.2.0 // indirect github.com/gobwas/ws v1.0.2 - github.com/golang/protobuf v1.3.2 + github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect + github.com/golang/protobuf v1.3.3 + github.com/google/go-cmp v0.4.0 // indirect github.com/gorilla/websocket v1.4.1 - golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 + github.com/mattn/go-isatty v0.0.12 // indirect + go.opencensus.io v0.22.3 // indirect + golang.org/x/crypto v0.0.0-20200208060501-ecb85df21340 // indirect + golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 // indirect + golang.org/x/time v0.0.0-20191024005414-555d28b269f0 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 ) diff --git a/go.sum b/go.sum index 864efaa7..1d1dc3a6 100644 --- a/go.sum +++ b/go.sum @@ -23,6 +23,8 @@ github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= github.com/alecthomas/chroma v0.7.0 h1:z+0HgTUmkpRDRz0SRSdMaqOLfJV4F+N1FPDZUZIDUzw= github.com/alecthomas/chroma v0.7.0/go.mod h1:1U/PfCsTALWWYHDnsIQkxEBM0+6LLe0v8+RSVMOwxeY= +github.com/alecthomas/chroma v0.7.1 h1:G1i02OhUbRi2nJxcNkwJaY/J1gHXj9tt72qN6ZouLFQ= +github.com/alecthomas/chroma v0.7.1/go.mod h1:gHw09mkX1Qp80JlYbmN9L3+4R5o6DJJ3GRShh+AICNc= github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo= github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= github.com/alecthomas/kong v0.1.17-0.20190424132513-439c674f7ae0/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= @@ -46,6 +48,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= +github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= @@ -59,6 +63,8 @@ github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 h1:uHTyIjqVhYRhLbJ8nIiOJHkEZZ+5YoOsAbD3sk82NiE= github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -66,12 +72,16 @@ github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.2-0.20191216170541-340f1ebe299e h1:4WfjkTUTsO6siF8ghDQQk6t7x/FPsv3w6MXkc47do7Q= github.com/google/go-cmp v0.3.2-0.20191216170541-340f1ebe299e/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= @@ -102,6 +112,8 @@ github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -123,12 +135,16 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8= +go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g= golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200208060501-ecb85df21340 h1:KOcEaR10tFr7gdJV2GCKw8Os5yED1u1aOqHjOAb6d2Y= +golang.org/x/crypto v0.0.0-20200208060501-ecb85df21340/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -181,6 +197,9 @@ golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 h1:gSbV7h1NRL2G1xTg/owz62CST1oJBmxy4QpMMregXVQ= golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0= +golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -188,6 +207,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= From faadcc9613d9e663ef39dd9d71196e033f3f2901 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 8 Feb 2020 23:14:03 -0500 Subject: [PATCH 244/519] Simplify tests --- assert_test.go | 112 ------------------------ compress_test.go | 25 +----- conn_test.go | 165 +++++++++++++++-------------------- dial.go | 1 + go.mod | 2 +- internal/test/cmp/cmp.go | 22 +++++ internal/test/doc.go | 2 + internal/test/wstest/pipe.go | 82 +++++++++++++++++ internal/test/xrand/xrand.go | 47 ++++++++++ ws_js_test.go | 12 ++- 10 files changed, 234 insertions(+), 236 deletions(-) delete mode 100644 assert_test.go create mode 100644 internal/test/cmp/cmp.go create mode 100644 internal/test/doc.go create mode 100644 internal/test/wstest/pipe.go create mode 100644 internal/test/xrand/xrand.go diff --git a/assert_test.go b/assert_test.go deleted file mode 100644 index a51b2c3d..00000000 --- a/assert_test.go +++ /dev/null @@ -1,112 +0,0 @@ -package websocket_test - -import ( - "context" - "crypto/rand" - "fmt" - "net/http" - "strings" - "testing" - "time" - - "cdr.dev/slog" - "cdr.dev/slog/sloggers/slogtest" - "cdr.dev/slog/sloggers/slogtest/assert" - - "nhooyr.io/websocket" - "nhooyr.io/websocket/wsjson" -) - -func randBytes(t *testing.T, n int) []byte { - b := make([]byte, n) - _, err := rand.Reader.Read(b) - assert.Success(t, "readRandBytes", err) - return b -} - -func echoJSON(t *testing.T, c *websocket.Conn, n int) { - slog.Helper() - - s := randString(t, n) - go writeJSON(t, c, s) - readJSON(t, c, s) -} - -func writeJSON(t *testing.T, c *websocket.Conn, v interface{}) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) - defer cancel() - - err := wsjson.Write(ctx, c, v) - assert.Success(t, "wsjson.Write", err) -} - -func readJSON(t *testing.T, c *websocket.Conn, exp interface{}) { - slog.Helper() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) - defer cancel() - - var act interface{} - err := wsjson.Read(ctx, c, &act) - assert.Success(t, "wsjson.Read", err) - assert.Equal(t, "json", exp, act) -} - -func randString(t *testing.T, n int) string { - s := strings.ToValidUTF8(string(randBytes(t, n)), "_") - s = strings.ReplaceAll(s, "\x00", "_") - if len(s) > n { - return s[:n] - } - if len(s) < n { - // Pad with = - extra := n - len(s) - return s + strings.Repeat("=", extra) - } - return s -} - -func assertEcho(t *testing.T, ctx context.Context, c *websocket.Conn, typ websocket.MessageType, n int) { - slog.Helper() - - p := randBytes(t, n) - err := c.Write(ctx, typ, p) - assert.Success(t, "write", err) - - typ2, p2, err := c.Read(ctx) - assert.Success(t, "read", err) - - assert.Equal(t, "dataType", typ, typ2) - assert.Equal(t, "payload", p, p2) -} - -func assertSubprotocol(t *testing.T, c *websocket.Conn, exp string) { - slog.Helper() - - assert.Equal(t, "subprotocol", exp, c.Subprotocol()) -} - -func assertCloseStatus(t testing.TB, exp websocket.StatusCode, err error) { - slog.Helper() - - if websocket.CloseStatus(err) == -1 { - slogtest.Fatal(t, "expected websocket.CloseError", slogType(err), slog.Error(err)) - } - if websocket.CloseStatus(err) != exp { - slogtest.Error(t, "unexpected close status", - slog.F("exp", exp), - slog.F("act", err), - ) - } - -} - -func acceptWebSocket(t testing.TB, r *http.Request, w http.ResponseWriter, opts *websocket.AcceptOptions) *websocket.Conn { - c, err := websocket.Accept(w, r, opts) - assert.Success(t, "websocket.Accept", err) - return c -} - -func slogType(v interface{}) slog.Field { - return slog.F("type", fmt.Sprintf("%T", v)) -} diff --git a/compress_test.go b/compress_test.go index 6edfcb1a..15d334d6 100644 --- a/compress_test.go +++ b/compress_test.go @@ -1,13 +1,12 @@ package websocket import ( - "crypto/rand" - "encoding/base64" - "math/big" "strings" "testing" "cdr.dev/slog/sloggers/slogtest/assert" + + "nhooyr.io/websocket/internal/test/xrand" ) func Test_slidingWindow(t *testing.T) { @@ -16,8 +15,8 @@ func Test_slidingWindow(t *testing.T) { const testCount = 99 const maxWindow = 99999 for i := 0; i < testCount; i++ { - input := randStr(t, maxWindow) - windowLength := randInt(t, maxWindow) + input := xrand.String(maxWindow) + windowLength := xrand.Int(maxWindow) r := newSlidingWindow(windowLength) r.write([]byte(input)) @@ -27,19 +26,3 @@ func Test_slidingWindow(t *testing.T) { assert.True(t, "hasSuffix", strings.HasSuffix(input, string(r.buf))) } } - -func randStr(t *testing.T, max int) string { - n := randInt(t, max) - - b := make([]byte, n) - _, err := rand.Read(b) - assert.Success(t, "rand.Read", err) - - return base64.StdEncoding.EncodeToString(b) -} - -func randInt(t *testing.T, max int) int { - x, err := rand.Int(rand.Reader, big.NewInt(int64(max))) - assert.Success(t, "rand.Int", err) - return int(x.Int64()) -} diff --git a/conn_test.go b/conn_test.go index f1361adc..d246f719 100644 --- a/conn_test.go +++ b/conn_test.go @@ -3,59 +3,96 @@ package websocket_test import ( - "bufio" "context" - "crypto/rand" "io" - "math/big" - "net" - "net/http" - "net/http/httptest" "testing" "time" - "cdr.dev/slog/sloggers/slogtest/assert" + "golang.org/x/xerrors" "nhooyr.io/websocket" + "nhooyr.io/websocket/internal/test/cmp" + "nhooyr.io/websocket/internal/test/wstest" + "nhooyr.io/websocket/internal/test/xrand" + "nhooyr.io/websocket/wsjson" ) -func goFn(fn func()) func() { - done := make(chan struct{}) +func goFn(fn func() error) chan error { + errs := make(chan error) go func() { - defer close(done) - fn() + defer close(errs) + errs <- fn() }() - return func() { - <-done - } + return errs } func TestConn(t *testing.T) { t.Parallel() - t.Run("json", func(t *testing.T) { + t.Run("data", func(t *testing.T) { t.Parallel() - for i := 0; i < 1; i++ { + for i := 0; i < 10; i++ { t.Run("", func(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() - c1, c2 := websocketPipe(t) + copts := websocket.CompressionOptions{ + Mode: websocket.CompressionMode(xrand.Int(int(websocket.CompressionDisabled))), + Threshold: xrand.Int(9999), + } + + c1, c2, err := wstest.Pipe(&websocket.DialOptions{ + CompressionOptions: copts, + }, &websocket.AcceptOptions{ + CompressionOptions: copts, + }) + if err != nil { + t.Fatal(err) + } + defer c1.Close(websocket.StatusInternalError, "") + defer c2.Close(websocket.StatusInternalError, "") - wait := goFn(func() { + echoLoopErr := goFn(func() error { err := echoLoop(ctx, c1) - assertCloseStatus(t, websocket.StatusNormalClosure, err) + return assertCloseStatus(websocket.StatusNormalClosure, err) }) - defer wait() + defer func() { + err := <-echoLoopErr + if err != nil { + t.Errorf("echo loop error: %v", err) + } + }() defer cancel() c2.SetReadLimit(1 << 30) for i := 0; i < 10; i++ { - n := randInt(t, 131_072) - echoJSON(t, c2, n) + n := xrand.Int(131_072) + + msg := xrand.String(n) + + writeErr := goFn(func() error { + return wsjson.Write(ctx, c2, msg) + }) + + var act interface{} + err := wsjson.Read(ctx, c2, &act) + if err != nil { + t.Fatal(err) + } + + err = <-writeErr + if err != nil { + t.Fatal(err) + } + + if !cmp.Equal(msg, act) { + t.Fatalf("unexpected msg read: %v", cmp.Diff(msg, act)) + } } c2.Close(websocket.StatusNormalClosure, "") @@ -64,6 +101,16 @@ func TestConn(t *testing.T) { }) } +func assertCloseStatus(exp websocket.StatusCode, err error) error { + if websocket.CloseStatus(err) == -1 { + return xerrors.Errorf("expected websocket.CloseError: %T %v", err, err) + } + if websocket.CloseStatus(err) != exp { + return xerrors.Errorf("unexpected close status (%v):%v", exp, err) + } + return nil +} + // echoLoop echos every msg received from c until an error // occurs or the context expires. // The read limit is set to 1 << 30. @@ -98,75 +145,3 @@ func echoLoop(ctx context.Context, c *websocket.Conn) error { } } } - -func randBool(t testing.TB) bool { - return randInt(t, 2) == 1 -} - -func randInt(t testing.TB, max int) int { - x, err := rand.Int(rand.Reader, big.NewInt(int64(max))) - assert.Success(t, "rand.Int", err) - return int(x.Int64()) -} - -type testHijacker struct { - *httptest.ResponseRecorder - serverConn net.Conn - hijacked chan struct{} -} - -var _ http.Hijacker = testHijacker{} - -func (hj testHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) { - close(hj.hijacked) - return hj.serverConn, bufio.NewReadWriter(bufio.NewReader(hj.serverConn), bufio.NewWriter(hj.serverConn)), nil -} - -func websocketPipe(t *testing.T) (*websocket.Conn, *websocket.Conn) { - var serverConn *websocket.Conn - tt := testTransport{ - h: func(w http.ResponseWriter, r *http.Request) { - serverConn = acceptWebSocket(t, r, w, nil) - }, - } - - dialOpts := &websocket.DialOptions{ - HTTPClient: &http.Client{ - Transport: tt, - }, - } - - clientConn, _, err := websocket.Dial(context.Background(), "ws://example.com", dialOpts) - assert.Success(t, "websocket.Dial", err) - - if randBool(t) { - return serverConn, clientConn - } - return clientConn, serverConn -} - -type testTransport struct { - h http.HandlerFunc -} - -func (t testTransport) RoundTrip(r *http.Request) (*http.Response, error) { - clientConn, serverConn := net.Pipe() - - hj := testHijacker{ - ResponseRecorder: httptest.NewRecorder(), - serverConn: serverConn, - hijacked: make(chan struct{}), - } - - done := make(chan struct{}) - t.h.ServeHTTP(hj, r) - - select { - case <-hj.hijacked: - resp := hj.ResponseRecorder.Result() - resp.Body = clientConn - return resp, nil - case <-done: - return hj.ResponseRecorder.Result(), nil - } -} diff --git a/dial.go b/dial.go index 4557602e..a1509ab5 100644 --- a/dial.go +++ b/dial.go @@ -35,6 +35,7 @@ type DialOptions struct { // CompressionOptions controls the compression options. // See docs on the CompressionOptions type. + // TODO make * CompressionOptions CompressionOptions } diff --git a/go.mod b/go.mod index ee1708a2..fc4ebb99 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/gobwas/ws v1.0.2 github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/golang/protobuf v1.3.3 - github.com/google/go-cmp v0.4.0 // indirect + github.com/google/go-cmp v0.4.0 github.com/gorilla/websocket v1.4.1 github.com/mattn/go-isatty v0.0.12 // indirect go.opencensus.io v0.22.3 // indirect diff --git a/internal/test/cmp/cmp.go b/internal/test/cmp/cmp.go new file mode 100644 index 00000000..d0eee6d0 --- /dev/null +++ b/internal/test/cmp/cmp.go @@ -0,0 +1,22 @@ +package cmp + +import ( + "reflect" + + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" +) + +// Equal checks if v1 and v2 are equal with go-cmp. +func Equal(v1, v2 interface{}) bool { + return cmp.Equal(v1, v2, cmpopts.EquateErrors(), cmp.Exporter(func(r reflect.Type) bool { + return true + })) +} + +// Diff returns a human readable diff between v1 and v2 +func Diff(v1, v2 interface{}) string { + return cmp.Diff(v1, v2, cmpopts.EquateErrors(), cmp.Exporter(func(r reflect.Type) bool { + return true + })) +} diff --git a/internal/test/doc.go b/internal/test/doc.go new file mode 100644 index 00000000..94b2e82d --- /dev/null +++ b/internal/test/doc.go @@ -0,0 +1,2 @@ +// Package test contains subpackages only used in tests. +package test diff --git a/internal/test/wstest/pipe.go b/internal/test/wstest/pipe.go new file mode 100644 index 00000000..f3d25f55 --- /dev/null +++ b/internal/test/wstest/pipe.go @@ -0,0 +1,82 @@ +package wstest + +import ( + "bufio" + "context" + "net" + "net/http" + "net/http/httptest" + + "golang.org/x/xerrors" + + "nhooyr.io/websocket" + "nhooyr.io/websocket/internal/errd" + "nhooyr.io/websocket/internal/test/xrand" +) + +// Pipe is used to create an in memory connection +// between two websockets analogous to net.Pipe. +func Pipe(dialOpts *websocket.DialOptions, acceptOpts *websocket.AcceptOptions) (_ *websocket.Conn, _ *websocket.Conn, err error) { + defer errd.Wrap(&err, "failed to create ws pipe") + + var serverConn *websocket.Conn + var acceptErr error + tt := fakeTransport{ + h: func(w http.ResponseWriter, r *http.Request) { + serverConn, acceptErr = websocket.Accept(w, r, acceptOpts) + }, + } + + if dialOpts == nil { + dialOpts = &websocket.DialOptions{} + } + dialOpts.HTTPClient = &http.Client{ + Transport: tt, + } + + clientConn, _, err := websocket.Dial(context.Background(), "ws://example.com", dialOpts) + if err != nil { + return nil, nil, xerrors.Errorf("failed to dial with fake transport: %w", err) + } + + if serverConn == nil { + return nil, nil, xerrors.Errorf("failed to get server conn from fake transport: %w", acceptErr) + } + + if xrand.True() { + return serverConn, clientConn, nil + } + return clientConn, serverConn, nil +} + +type fakeTransport struct { + h http.HandlerFunc +} + +func (t fakeTransport) RoundTrip(r *http.Request) (*http.Response, error) { + clientConn, serverConn := net.Pipe() + + hj := testHijacker{ + ResponseRecorder: httptest.NewRecorder(), + serverConn: serverConn, + } + + t.h.ServeHTTP(hj, r) + + resp := hj.ResponseRecorder.Result() + if resp.StatusCode == http.StatusSwitchingProtocols { + resp.Body = clientConn + } + return resp, nil +} + +type testHijacker struct { + *httptest.ResponseRecorder + serverConn net.Conn +} + +var _ http.Hijacker = testHijacker{} + +func (hj testHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) { + return hj.serverConn, bufio.NewReadWriter(bufio.NewReader(hj.serverConn), bufio.NewWriter(hj.serverConn)), nil +} diff --git a/internal/test/xrand/xrand.go b/internal/test/xrand/xrand.go new file mode 100644 index 00000000..2f3ad30f --- /dev/null +++ b/internal/test/xrand/xrand.go @@ -0,0 +1,47 @@ +package xrand + +import ( + "crypto/rand" + "fmt" + "math/big" + "strings" +) + +// Bytes generates random bytes with length n. +func Bytes(n int) []byte { + b := make([]byte, n) + _, err := rand.Reader.Read(b) + if err != nil { + panic(fmt.Sprintf("failed to generate rand bytes: %v", err)) + } + return b +} + +// String generates a random string with length n. +func String(n int) string { + s := strings.ToValidUTF8(string(Bytes(n)), "_") + s = strings.ReplaceAll(s, "\x00", "_") + if len(s) > n { + return s[:n] + } + if len(s) < n { + // Pad with = + extra := n - len(s) + return s + strings.Repeat("=", extra) + } + return s +} + +// True returns a randomly generated boolean. +func True() bool { + return Int(2) == 1 +} + +// Int returns a randomly generated integer between [0, max). +func Int(max int) int { + x, err := rand.Int(rand.Reader, big.NewInt(int64(max))) + if err != nil { + panic(fmt.Sprintf("failed to get random int: %v", err)) + } + return int(x.Int64()) +} diff --git a/ws_js_test.go b/ws_js_test.go index 9f725a57..65309bff 100644 --- a/ws_js_test.go +++ b/ws_js_test.go @@ -1,4 +1,4 @@ -package websocket_test +package websocket import ( "context" @@ -6,8 +6,6 @@ import ( "os" "testing" "time" - - "nhooyr.io/websocket" ) func TestEcho(t *testing.T) { @@ -16,17 +14,17 @@ func TestEcho(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() - c, resp, err := websocket.Dial(ctx, os.Getenv("WS_ECHO_SERVER_URL"), &websocket.DialOptions{ + c, resp, err := Dial(ctx, os.Getenv("WS_ECHO_SERVER_URL"), &DialOptions{ Subprotocols: []string{"echo"}, }) assert.Success(t, err) - defer c.Close(websocket.StatusInternalError, "") + defer c.Close(StatusInternalError, "") assertSubprotocol(t, c, "echo") assert.Equalf(t, &http.Response{}, resp, "http.Response") echoJSON(t, ctx, c, 1024) - assertEcho(t, ctx, c, websocket.MessageBinary, 1024) + assertEcho(t, ctx, c, MessageBinary, 1024) - err = c.Close(websocket.StatusNormalClosure, "") + err = c.Close(StatusNormalClosure, "") assert.Success(t, err) } From 3f2589ffa18b5e61a7786ad5308c5ccc87688cef Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 8 Feb 2020 23:36:06 -0500 Subject: [PATCH 245/519] Remove quite a bit of slog --- ci/test.mk | 2 +- close_test.go | 42 ++++++++++++++++++++++++++---------- conn_test.go | 30 ++++++++++++++++++-------- frame_test.go | 30 ++++++++++++++++++++------ internal/test/wstest/pipe.go | 2 +- internal/test/xrand/xrand.go | 4 ++-- 6 files changed, 79 insertions(+), 31 deletions(-) diff --git a/ci/test.mk b/ci/test.mk index 95e049b2..786a8d77 100644 --- a/ci/test.mk +++ b/ci/test.mk @@ -14,4 +14,4 @@ coveralls: gotest gotest: go test -covermode=count -coverprofile=ci/out/coverage.prof -coverpkg=./... $${GOTESTFLAGS-} ./... sed -i '/stringer\.go/d' ci/out/coverage.prof - sed -i '/assert/d' ci/out/coverage.prof + sed -i '/nhooyr.io\/websocket\/internal\/test/d' ci/out/coverage.prof diff --git a/close_test.go b/close_test.go index 16b570d0..10a35b13 100644 --- a/close_test.go +++ b/close_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - "cdr.dev/slog/sloggers/slogtest/assert" + "nhooyr.io/websocket/internal/test/cmp" ) func TestCloseError(t *testing.T) { @@ -51,13 +51,23 @@ func TestCloseError(t *testing.T) { t.Parallel() _, err := tc.ce.bytesErr() - if tc.success { - assert.Success(t, "CloseError.bytesErr", err) - } else { - assert.Error(t, "CloseError.bytesErr", err) + if tc.success != (err == nil) { + t.Fatalf("unexpected error value (wanted err == nil == %v): %v", tc.success, err) } }) } + + t.Run("Error", func(t *testing.T) { + exp := `status = StatusInternalError and reason = "meow"` + act := CloseError{ + Code: StatusInternalError, + Reason: "meow", + }.Error() + + if (act) != exp { + t.Fatal(cmp.Diff(exp, act)) + } + }) } func Test_parseClosePayload(t *testing.T) { @@ -104,10 +114,14 @@ func Test_parseClosePayload(t *testing.T) { ce, err := parseClosePayload(tc.p) if tc.success { - assert.Success(t, "parse err", err) - assert.Equal(t, "ce", tc.ce, ce) - } else { - assert.Error(t, "parse err", err) + if err != nil { + t.Fatal(err) + } + if !cmp.Equal(tc.ce, ce) { + t.Fatalf("expected %v but got %v", tc.ce, ce) + } + } else if err == nil { + t.Errorf("expected error: %v %v", ce, err) } }) } @@ -153,7 +167,10 @@ func Test_validWireCloseCode(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - assert.Equal(t, "valid", tc.valid, validWireCloseCode(tc.code)) + act := validWireCloseCode(tc.code) + if !cmp.Equal(tc.valid, act) { + t.Fatalf("unexpected valid: %v", cmp.Diff(tc.valid, act)) + } }) } } @@ -190,7 +207,10 @@ func TestCloseStatus(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - assert.Equal(t, "closeStatus", tc.exp, CloseStatus(tc.in)) + act := CloseStatus(tc.in) + if !cmp.Equal(tc.exp, act) { + t.Fatalf("unexpected closeStatus: %v", cmp.Diff(tc.exp, act)) + } }) } } diff --git a/conn_test.go b/conn_test.go index d246f719..02606ef5 100644 --- a/conn_test.go +++ b/conn_test.go @@ -14,13 +14,17 @@ import ( "nhooyr.io/websocket/internal/test/cmp" "nhooyr.io/websocket/internal/test/wstest" "nhooyr.io/websocket/internal/test/xrand" - "nhooyr.io/websocket/wsjson" ) func goFn(fn func() error) chan error { errs := make(chan error) go func() { - defer close(errs) + defer func() { + r := recover() + if r != nil { + errs <- xerrors.Errorf("panic in gofn: %v", r) + } + }() errs <- fn() }() @@ -33,7 +37,7 @@ func TestConn(t *testing.T) { t.Run("data", func(t *testing.T) { t.Parallel() - for i := 0; i < 10; i++ { + for i := 0; i < 5; i++ { t.Run("", func(t *testing.T) { t.Parallel() @@ -41,7 +45,7 @@ func TestConn(t *testing.T) { defer cancel() copts := websocket.CompressionOptions{ - Mode: websocket.CompressionMode(xrand.Int(int(websocket.CompressionDisabled))), + Mode: websocket.CompressionMode(xrand.Int(int(websocket.CompressionDisabled) + 1)), Threshold: xrand.Int(9999), } @@ -70,17 +74,21 @@ func TestConn(t *testing.T) { c2.SetReadLimit(1 << 30) - for i := 0; i < 10; i++ { + for i := 0; i < 5; i++ { n := xrand.Int(131_072) - msg := xrand.String(n) + msg := xrand.Bytes(n) + + expType := websocket.MessageBinary + if xrand.Bool() { + expType = websocket.MessageText + } writeErr := goFn(func() error { - return wsjson.Write(ctx, c2, msg) + return c2.Write(ctx, expType, msg) }) - var act interface{} - err := wsjson.Read(ctx, c2, &act) + actType, act, err := c2.Read(ctx) if err != nil { t.Fatal(err) } @@ -90,6 +98,10 @@ func TestConn(t *testing.T) { t.Fatal(err) } + if expType != actType { + t.Fatalf("unexpected message typ (%v): %v", expType, actType) + } + if !cmp.Equal(msg, act) { t.Fatalf("unexpected msg read: %v", cmp.Diff(msg, act)) } diff --git a/frame_test.go b/frame_test.go index 323ea991..0b770a4c 100644 --- a/frame_test.go +++ b/frame_test.go @@ -13,9 +13,10 @@ import ( "time" _ "unsafe" - "cdr.dev/slog/sloggers/slogtest/assert" "github.com/gobwas/ws" _ "github.com/gorilla/websocket" + + "nhooyr.io/websocket/internal/test/cmp" ) func TestHeader(t *testing.T) { @@ -80,14 +81,22 @@ func testHeader(t *testing.T, h header) { r := bufio.NewReader(b) err := writeFrameHeader(h, w) - assert.Success(t, "writeFrameHeader", err) + if err != nil { + t.Fatal(err) + } err = w.Flush() - assert.Success(t, "flush", err) + if err != nil { + t.Fatal(err) + } h2, err := readFrameHeader(r) - assert.Success(t, "readFrameHeader", err) + if err != nil { + t.Fatal(err) + } - assert.Equal(t, "header", h, h2) + if !cmp.Equal(h, h2) { + t.Fatal(cmp.Diff(h, h2)) + } } func Test_mask(t *testing.T) { @@ -98,8 +107,15 @@ func Test_mask(t *testing.T) { p := []byte{0xa, 0xb, 0xc, 0xf2, 0xc} gotKey32 := mask(key32, p) - assert.Equal(t, "mask", []byte{0, 0, 0, 0x0d, 0x6}, p) - assert.Equal(t, "maskKey", bits.RotateLeft32(key32, -8), gotKey32) + expP := []byte{0, 0, 0, 0x0d, 0x6} + if !cmp.Equal(expP, p) { + t.Fatal(cmp.Diff(expP, p)) + } + + expKey32 := bits.RotateLeft32(key32, -8) + if !cmp.Equal(expKey32, gotKey32) { + t.Fatal(cmp.Diff(expKey32, gotKey32)) + } } func basicMask(maskKey [4]byte, pos int, b []byte) int { diff --git a/internal/test/wstest/pipe.go b/internal/test/wstest/pipe.go index f3d25f55..e958aea4 100644 --- a/internal/test/wstest/pipe.go +++ b/internal/test/wstest/pipe.go @@ -43,7 +43,7 @@ func Pipe(dialOpts *websocket.DialOptions, acceptOpts *websocket.AcceptOptions) return nil, nil, xerrors.Errorf("failed to get server conn from fake transport: %w", acceptErr) } - if xrand.True() { + if xrand.Bool() { return serverConn, clientConn, nil } return clientConn, serverConn, nil diff --git a/internal/test/xrand/xrand.go b/internal/test/xrand/xrand.go index 2f3ad30f..8de1ede8 100644 --- a/internal/test/xrand/xrand.go +++ b/internal/test/xrand/xrand.go @@ -32,8 +32,8 @@ func String(n int) string { return s } -// True returns a randomly generated boolean. -func True() bool { +// Bool returns a randomly generated boolean. +func Bool() bool { return Int(2) == 1 } From b53f306c00debd46e5ed5debd2f9594ee8889f5c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 9 Feb 2020 00:47:34 -0500 Subject: [PATCH 246/519] Get Wasm tests working --- accept.go | 7 +- accept_test.go | 55 +++++--- autobahn_test.go | 33 +++-- close.go | 193 ------------------------- close_notjs.go | 199 ++++++++++++++++++++++++++ compress.go | 155 -------------------- compress_notjs.go | 156 +++++++++++++++++++++ compress_test.go | 29 ++-- conn.go | 261 ---------------------------------- conn_notjs.go | 264 +++++++++++++++++++++++++++++++++++ conn_test.go | 131 +++++++---------- dial.go | 7 +- dial_test.go | 2 +- example_test.go | 3 +- frame.go | 2 - internal/test/cmp/cmp.go | 9 ++ internal/test/wstest/echo.go | 90 ++++++++++++ internal/test/wstest/pipe.go | 3 + internal/test/wstest/url.go | 11 ++ internal/xsync/go.go | 25 ++++ internal/xsync/go_test.go | 20 +++ internal/xsync/int64.go | 23 +++ read.go | 19 +-- ws_js.go | 30 ++-- ws_js_test.go | 40 ++++-- 25 files changed, 985 insertions(+), 782 deletions(-) create mode 100644 close_notjs.go create mode 100644 compress_notjs.go create mode 100644 conn_notjs.go create mode 100644 internal/test/wstest/echo.go create mode 100644 internal/test/wstest/url.go create mode 100644 internal/xsync/go.go create mode 100644 internal/xsync/go_test.go create mode 100644 internal/xsync/int64.go diff --git a/accept.go b/accept.go index 0394fa6d..31f104b2 100644 --- a/accept.go +++ b/accept.go @@ -39,7 +39,7 @@ type AcceptOptions struct { // CompressionOptions controls the compression options. // See docs on the CompressionOptions type. - CompressionOptions CompressionOptions + CompressionOptions *CompressionOptions } // Accept accepts a WebSocket handshake from a client and upgrades the @@ -59,6 +59,11 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con if opts == nil { opts = &AcceptOptions{} } + opts = &*opts + + if opts.CompressionOptions == nil { + opts.CompressionOptions = &CompressionOptions{} + } err = verifyClientRequest(r) if err != nil { diff --git a/accept_test.go b/accept_test.go index 3e8b1f46..18302da5 100644 --- a/accept_test.go +++ b/accept_test.go @@ -10,8 +10,9 @@ import ( "strings" "testing" - "cdr.dev/slog/sloggers/slogtest/assert" "golang.org/x/xerrors" + + "nhooyr.io/websocket/internal/test/cmp" ) func TestAccept(t *testing.T) { @@ -24,7 +25,9 @@ func TestAccept(t *testing.T) { r := httptest.NewRequest("GET", "/", nil) _, err := Accept(w, r, nil) - assert.ErrorContains(t, "Accept", err, "protocol violation") + if !cmp.ErrorContains(err, "protocol violation") { + t.Fatal(err) + } }) t.Run("badOrigin", func(t *testing.T) { @@ -39,7 +42,9 @@ func TestAccept(t *testing.T) { r.Header.Set("Origin", "harhar.com") _, err := Accept(w, r, nil) - assert.ErrorContains(t, "Accept", err, "request Origin \"harhar.com\" is not authorized for Host") + if !cmp.ErrorContains(err, `request Origin "harhar.com" is not authorized for Host`) { + t.Fatal(err) + } }) t.Run("badCompression", func(t *testing.T) { @@ -56,7 +61,9 @@ func TestAccept(t *testing.T) { r.Header.Set("Sec-WebSocket-Extensions", "permessage-deflate; harharhar") _, err := Accept(w, r, nil) - assert.ErrorContains(t, "Accept", err, "unsupported permessage-deflate parameter") + if !cmp.ErrorContains(err, `unsupported permessage-deflate parameter`) { + t.Fatal(err) + } }) t.Run("requireHttpHijacker", func(t *testing.T) { @@ -70,7 +77,9 @@ func TestAccept(t *testing.T) { r.Header.Set("Sec-WebSocket-Key", "meow123") _, err := Accept(w, r, nil) - assert.ErrorContains(t, "Accept", err, "http.ResponseWriter does not implement http.Hijacker") + if !cmp.ErrorContains(err, `http.ResponseWriter does not implement http.Hijacker`) { + t.Fatal(err) + } }) t.Run("badHijack", func(t *testing.T) { @@ -90,7 +99,9 @@ func TestAccept(t *testing.T) { r.Header.Set("Sec-WebSocket-Key", "meow123") _, err := Accept(w, r, nil) - assert.ErrorContains(t, "Accept", err, "failed to hijack connection") + if !cmp.ErrorContains(err, `failed to hijack connection`) { + t.Fatal(err) + } }) } @@ -182,10 +193,8 @@ func Test_verifyClientHandshake(t *testing.T) { } err := verifyClientRequest(r) - if tc.success { - assert.Success(t, "verifyClientRequest", err) - } else { - assert.Error(t, "verifyClientRequest", err) + if tc.success != (err == nil) { + t.Fatalf("unexpected error value: %v", err) } }) } @@ -235,7 +244,9 @@ func Test_selectSubprotocol(t *testing.T) { r.Header.Set("Sec-WebSocket-Protocol", strings.Join(tc.clientProtocols, ",")) negotiated := selectSubprotocol(r, tc.serverProtocols) - assert.Equal(t, "negotiated", tc.negotiated, negotiated) + if !cmp.Equal(tc.negotiated, negotiated) { + t.Fatalf("unexpected negotiated: %v", cmp.Diff(tc.negotiated, negotiated)) + } }) } } @@ -289,10 +300,8 @@ func Test_authenticateOrigin(t *testing.T) { r.Header.Set("Origin", tc.origin) err := authenticateOrigin(r) - if tc.success { - assert.Success(t, "authenticateOrigin", err) - } else { - assert.Error(t, "authenticateOrigin", err) + if tc.success != (err == nil) { + t.Fatalf("unexpected error value: %v", err) } }) } @@ -364,13 +373,21 @@ func Test_acceptCompression(t *testing.T) { w := httptest.NewRecorder() copts, err := acceptCompression(r, w, tc.mode) if tc.error { - assert.Error(t, "acceptCompression", err) + if err == nil { + t.Fatalf("expected error: %v", copts) + } return } - assert.Success(t, "acceptCompression", err) - assert.Equal(t, "compresssionOpts", tc.expCopts, copts) - assert.Equal(t, "respHeader", tc.respSecWebSocketExtensions, w.Header().Get("Sec-WebSocket-Extensions")) + if err != nil { + t.Fatal(err) + } + if !cmp.Equal(tc.expCopts, copts) { + t.Fatalf("unexpected compression options: %v", cmp.Diff(tc.expCopts, copts)) + } + if !cmp.Equal(tc.respSecWebSocketExtensions, w.Header().Get("Sec-WebSocket-Extensions")) { + t.Fatalf("unexpected respHeader: %v", cmp.Diff(tc.respSecWebSocketExtensions, w.Header().Get("Sec-WebSocket-Extensions"))) + } }) } } diff --git a/autobahn_test.go b/autobahn_test.go index d730cf4a..4d0bd1b5 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -15,11 +15,11 @@ import ( "testing" "time" - "cdr.dev/slog/sloggers/slogtest/assert" "golang.org/x/xerrors" "nhooyr.io/websocket" "nhooyr.io/websocket/internal/errd" + "nhooyr.io/websocket/internal/test/wstest" ) var excludedAutobahnCases = []string{ @@ -45,14 +45,20 @@ func TestAutobahn(t *testing.T) { defer cancel() wstestURL, closeFn, err := wstestClientServer(ctx) - assert.Success(t, "wstestClient", err) + if err != nil { + t.Fatal(err) + } defer closeFn() err = waitWS(ctx, wstestURL) - assert.Success(t, "waitWS", err) + if err != nil { + t.Fatal(err) + } cases, err := wstestCaseCount(ctx, wstestURL) - assert.Success(t, "wstestCaseCount", err) + if err != nil { + t.Fatal(err) + } t.Run("cases", func(t *testing.T) { for i := 1; i <= cases; i++ { @@ -62,16 +68,19 @@ func TestAutobahn(t *testing.T) { defer cancel() c, _, err := websocket.Dial(ctx, fmt.Sprintf(wstestURL+"/runCase?case=%v&agent=main", i), nil) - assert.Success(t, "autobahn dial", err) - - err = echoLoop(ctx, c) + if err != nil { + t.Fatal(err) + } + err = wstest.EchoLoop(ctx, c) t.Logf("echoLoop: %v", err) }) } }) c, _, err := websocket.Dial(ctx, fmt.Sprintf(wstestURL+"/updateReports?agent=main"), nil) - assert.Success(t, "dial", err) + if err != nil { + t.Fatal(err) + } c.Close(websocket.StatusNormalClosure, "") checkWSTestIndex(t, "./ci/out/wstestClientReports/index.json") @@ -163,14 +172,18 @@ func wstestCaseCount(ctx context.Context, url string) (cases int, err error) { func checkWSTestIndex(t *testing.T, path string) { wstestOut, err := ioutil.ReadFile(path) - assert.Success(t, "ioutil.ReadFile", err) + if err != nil { + t.Fatal(err) + } var indexJSON map[string]map[string]struct { Behavior string `json:"behavior"` BehaviorClose string `json:"behaviorClose"` } err = json.Unmarshal(wstestOut, &indexJSON) - assert.Success(t, "json.Unmarshal", err) + if err != nil { + t.Fatal(err) + } for _, tests := range indexJSON { for test, result := range tests { diff --git a/close.go b/close.go index 931160e6..20073233 100644 --- a/close.go +++ b/close.go @@ -1,17 +1,9 @@ -// +build !js - package websocket import ( - "context" - "encoding/binary" "fmt" - "log" - "time" "golang.org/x/xerrors" - - "nhooyr.io/websocket/internal/errd" ) // StatusCode represents a WebSocket status code. @@ -83,188 +75,3 @@ func CloseStatus(err error) StatusCode { } return -1 } - -// Close performs the WebSocket close handshake with the given status code and reason. -// -// It will write a WebSocket close frame with a timeout of 5s and then wait 5s for -// the peer to send a close frame. -// All data messages received from the peer during the close handshake will be discarded. -// -// The connection can only be closed once. Additional calls to Close -// are no-ops. -// -// The maximum length of reason must be 125 bytes. Avoid -// sending a dynamic reason. -// -// Close will unblock all goroutines interacting with the connection once -// complete. -func (c *Conn) Close(code StatusCode, reason string) error { - return c.closeHandshake(code, reason) -} - -func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { - defer errd.Wrap(&err, "failed to close WebSocket") - - err = c.writeClose(code, reason) - if err != nil { - return err - } - - err = c.waitCloseHandshake() - if CloseStatus(err) == -1 { - return err - } - return nil -} - -func (c *Conn) writeError(code StatusCode, err error) { - c.setCloseErr(err) - c.writeClose(code, err.Error()) - c.close(nil) -} - -func (c *Conn) writeClose(code StatusCode, reason string) error { - c.closeMu.Lock() - closing := c.wroteClose - c.wroteClose = true - c.closeMu.Unlock() - if closing { - return xerrors.New("already wrote close") - } - - ce := CloseError{ - Code: code, - Reason: reason, - } - - c.setCloseErr(xerrors.Errorf("sent close frame: %w", ce)) - - var p []byte - if ce.Code != StatusNoStatusRcvd { - p = ce.bytes() - } - - return c.writeControl(context.Background(), opClose, p) -} - -func (c *Conn) waitCloseHandshake() error { - defer c.close(nil) - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) - defer cancel() - - err := c.readMu.Lock(ctx) - if err != nil { - return err - } - defer c.readMu.Unlock() - - if c.readCloseFrameErr != nil { - return c.readCloseFrameErr - } - - for { - h, err := c.readLoop(ctx) - if err != nil { - return err - } - - for i := int64(0); i < h.payloadLength; i++ { - _, err := c.br.ReadByte() - if err != nil { - return err - } - } - } -} - -func parseClosePayload(p []byte) (CloseError, error) { - if len(p) == 0 { - return CloseError{ - Code: StatusNoStatusRcvd, - }, nil - } - - if len(p) < 2 { - return CloseError{}, xerrors.Errorf("close payload %q too small, cannot even contain the 2 byte status code", p) - } - - ce := CloseError{ - Code: StatusCode(binary.BigEndian.Uint16(p)), - Reason: string(p[2:]), - } - - if !validWireCloseCode(ce.Code) { - return CloseError{}, xerrors.Errorf("invalid status code %v", ce.Code) - } - - return ce, nil -} - -// See http://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number -// and https://tools.ietf.org/html/rfc6455#section-7.4.1 -func validWireCloseCode(code StatusCode) bool { - switch code { - case statusReserved, StatusNoStatusRcvd, StatusAbnormalClosure, StatusTLSHandshake: - return false - } - - if code >= StatusNormalClosure && code <= StatusBadGateway { - return true - } - if code >= 3000 && code <= 4999 { - return true - } - - return false -} - -func (ce CloseError) bytes() []byte { - p, err := ce.bytesErr() - if err != nil { - log.Printf("websocket: failed to marshal close frame: %+v", err) - ce = CloseError{ - Code: StatusInternalError, - } - p, _ = ce.bytesErr() - } - return p -} - -const maxCloseReason = maxControlPayload - 2 - -func (ce CloseError) bytesErr() ([]byte, error) { - if len(ce.Reason) > maxCloseReason { - return nil, xerrors.Errorf("reason string max is %v but got %q with length %v", maxCloseReason, ce.Reason, len(ce.Reason)) - } - - if !validWireCloseCode(ce.Code) { - return nil, xerrors.Errorf("status code %v cannot be set", ce.Code) - } - - buf := make([]byte, 2+len(ce.Reason)) - binary.BigEndian.PutUint16(buf, uint16(ce.Code)) - copy(buf[2:], ce.Reason) - return buf, nil -} - -func (c *Conn) setCloseErr(err error) { - c.closeMu.Lock() - c.setCloseErrLocked(err) - c.closeMu.Unlock() -} - -func (c *Conn) setCloseErrLocked(err error) { - if c.closeErr == nil { - c.closeErr = xerrors.Errorf("WebSocket closed: %w", err) - } -} - -func (c *Conn) isClosed() bool { - select { - case <-c.closed: - return true - default: - return false - } -} diff --git a/close_notjs.go b/close_notjs.go new file mode 100644 index 00000000..dd1b0e0d --- /dev/null +++ b/close_notjs.go @@ -0,0 +1,199 @@ +// +build !js + +package websocket + +import ( + "context" + "encoding/binary" + "log" + "time" + + "golang.org/x/xerrors" + + "nhooyr.io/websocket/internal/errd" +) + +// Close performs the WebSocket close handshake with the given status code and reason. +// +// It will write a WebSocket close frame with a timeout of 5s and then wait 5s for +// the peer to send a close frame. +// All data messages received from the peer during the close handshake will be discarded. +// +// The connection can only be closed once. Additional calls to Close +// are no-ops. +// +// The maximum length of reason must be 125 bytes. Avoid +// sending a dynamic reason. +// +// Close will unblock all goroutines interacting with the connection once +// complete. +func (c *Conn) Close(code StatusCode, reason string) error { + return c.closeHandshake(code, reason) +} + +func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { + defer errd.Wrap(&err, "failed to close WebSocket") + + err = c.writeClose(code, reason) + if err != nil { + return err + } + + err = c.waitCloseHandshake() + if CloseStatus(err) == -1 { + return err + } + return nil +} + +func (c *Conn) writeError(code StatusCode, err error) { + c.setCloseErr(err) + c.writeClose(code, err.Error()) + c.close(nil) +} + +func (c *Conn) writeClose(code StatusCode, reason string) error { + c.closeMu.Lock() + closing := c.wroteClose + c.wroteClose = true + c.closeMu.Unlock() + if closing { + return xerrors.New("already wrote close") + } + + ce := CloseError{ + Code: code, + Reason: reason, + } + + c.setCloseErr(xerrors.Errorf("sent close frame: %w", ce)) + + var p []byte + if ce.Code != StatusNoStatusRcvd { + p = ce.bytes() + } + + return c.writeControl(context.Background(), opClose, p) +} + +func (c *Conn) waitCloseHandshake() error { + defer c.close(nil) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + err := c.readMu.Lock(ctx) + if err != nil { + return err + } + defer c.readMu.Unlock() + + if c.readCloseFrameErr != nil { + return c.readCloseFrameErr + } + + for { + h, err := c.readLoop(ctx) + if err != nil { + return err + } + + for i := int64(0); i < h.payloadLength; i++ { + _, err := c.br.ReadByte() + if err != nil { + return err + } + } + } +} + +func parseClosePayload(p []byte) (CloseError, error) { + if len(p) == 0 { + return CloseError{ + Code: StatusNoStatusRcvd, + }, nil + } + + if len(p) < 2 { + return CloseError{}, xerrors.Errorf("close payload %q too small, cannot even contain the 2 byte status code", p) + } + + ce := CloseError{ + Code: StatusCode(binary.BigEndian.Uint16(p)), + Reason: string(p[2:]), + } + + if !validWireCloseCode(ce.Code) { + return CloseError{}, xerrors.Errorf("invalid status code %v", ce.Code) + } + + return ce, nil +} + +// See http://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number +// and https://tools.ietf.org/html/rfc6455#section-7.4.1 +func validWireCloseCode(code StatusCode) bool { + switch code { + case statusReserved, StatusNoStatusRcvd, StatusAbnormalClosure, StatusTLSHandshake: + return false + } + + if code >= StatusNormalClosure && code <= StatusBadGateway { + return true + } + if code >= 3000 && code <= 4999 { + return true + } + + return false +} + +func (ce CloseError) bytes() []byte { + p, err := ce.bytesErr() + if err != nil { + log.Printf("websocket: failed to marshal close frame: %v", err) + ce = CloseError{ + Code: StatusInternalError, + } + p, _ = ce.bytesErr() + } + return p +} + +const maxCloseReason = maxControlPayload - 2 + +func (ce CloseError) bytesErr() ([]byte, error) { + if len(ce.Reason) > maxCloseReason { + return nil, xerrors.Errorf("reason string max is %v but got %q with length %v", maxCloseReason, ce.Reason, len(ce.Reason)) + } + + if !validWireCloseCode(ce.Code) { + return nil, xerrors.Errorf("status code %v cannot be set", ce.Code) + } + + buf := make([]byte, 2+len(ce.Reason)) + binary.BigEndian.PutUint16(buf, uint16(ce.Code)) + copy(buf[2:], ce.Reason) + return buf, nil +} + +func (c *Conn) setCloseErr(err error) { + c.closeMu.Lock() + c.setCloseErrLocked(err) + c.closeMu.Unlock() +} + +func (c *Conn) setCloseErrLocked(err error) { + if c.closeErr == nil { + c.closeErr = xerrors.Errorf("WebSocket closed: %w", err) + } +} + +func (c *Conn) isClosed() bool { + select { + case <-c.closed: + return true + default: + return false + } +} diff --git a/compress.go b/compress.go index efd89b33..918b3b49 100644 --- a/compress.go +++ b/compress.go @@ -1,14 +1,5 @@ -// +build !js - package websocket -import ( - "compress/flate" - "io" - "net/http" - "sync" -) - // CompressionOptions represents the available deflate extension options. // See https://tools.ietf.org/html/rfc7692 type CompressionOptions struct { @@ -60,149 +51,3 @@ const ( // important than bandwidth. CompressionDisabled ) - -func (m CompressionMode) opts() *compressionOptions { - if m == CompressionDisabled { - return nil - } - return &compressionOptions{ - clientNoContextTakeover: m == CompressionNoContextTakeover, - serverNoContextTakeover: m == CompressionNoContextTakeover, - } -} - -type compressionOptions struct { - clientNoContextTakeover bool - serverNoContextTakeover bool -} - -func (copts *compressionOptions) setHeader(h http.Header) { - s := "permessage-deflate" - if copts.clientNoContextTakeover { - s += "; client_no_context_takeover" - } - if copts.serverNoContextTakeover { - s += "; server_no_context_takeover" - } - h.Set("Sec-WebSocket-Extensions", s) -} - -// These bytes are required to get flate.Reader to return. -// They are removed when sending to avoid the overhead as -// WebSocket framing tell's when the message has ended but then -// we need to add them back otherwise flate.Reader keeps -// trying to return more bytes. -const deflateMessageTail = "\x00\x00\xff\xff" - -func (c *Conn) writeNoContextTakeOver() bool { - return c.client && c.copts.clientNoContextTakeover || !c.client && c.copts.serverNoContextTakeover -} - -func (c *Conn) readNoContextTakeOver() bool { - return !c.client && c.copts.clientNoContextTakeover || c.client && c.copts.serverNoContextTakeover -} - -type trimLastFourBytesWriter struct { - w io.Writer - tail []byte -} - -func (tw *trimLastFourBytesWriter) reset() { - tw.tail = tw.tail[:0] -} - -func (tw *trimLastFourBytesWriter) Write(p []byte) (int, error) { - extra := len(tw.tail) + len(p) - 4 - - if extra <= 0 { - tw.tail = append(tw.tail, p...) - return len(p), nil - } - - // Now we need to write as many extra bytes as we can from the previous tail. - if extra > len(tw.tail) { - extra = len(tw.tail) - } - if extra > 0 { - _, err := tw.w.Write(tw.tail[:extra]) - if err != nil { - return 0, err - } - tw.tail = tw.tail[extra:] - } - - // If p is less than or equal to 4 bytes, - // all of it is is part of the tail. - if len(p) <= 4 { - tw.tail = append(tw.tail, p...) - return len(p), nil - } - - // Otherwise, only the last 4 bytes are. - tw.tail = append(tw.tail, p[len(p)-4:]...) - - p = p[:len(p)-4] - n, err := tw.w.Write(p) - return n + 4, err -} - -var flateReaderPool sync.Pool - -func getFlateReader(r io.Reader, dict []byte) io.Reader { - fr, ok := flateReaderPool.Get().(io.Reader) - if !ok { - return flate.NewReaderDict(r, dict) - } - fr.(flate.Resetter).Reset(r, dict) - return fr -} - -func putFlateReader(fr io.Reader) { - flateReaderPool.Put(fr) -} - -var flateWriterPool sync.Pool - -func getFlateWriter(w io.Writer) *flate.Writer { - fw, ok := flateWriterPool.Get().(*flate.Writer) - if !ok { - fw, _ = flate.NewWriter(w, flate.BestSpeed) - return fw - } - fw.Reset(w) - return fw -} - -func putFlateWriter(w *flate.Writer) { - flateWriterPool.Put(w) -} - -type slidingWindow struct { - r io.Reader - buf []byte -} - -func newSlidingWindow(n int) *slidingWindow { - return &slidingWindow{ - buf: make([]byte, 0, n), - } -} - -func (w *slidingWindow) write(p []byte) { - if len(p) >= cap(w.buf) { - w.buf = w.buf[:cap(w.buf)] - p = p[len(p)-cap(w.buf):] - copy(w.buf, p) - return - } - - left := cap(w.buf) - len(w.buf) - if left < len(p) { - // We need to shift spaceNeeded bytes from the end to make room for p at the end. - spaceNeeded := len(p) - left - copy(w.buf, w.buf[spaceNeeded:]) - w.buf = w.buf[:len(w.buf)-spaceNeeded] - } - - w.buf = append(w.buf, p...) -} diff --git a/compress_notjs.go b/compress_notjs.go new file mode 100644 index 00000000..8bc2f87b --- /dev/null +++ b/compress_notjs.go @@ -0,0 +1,156 @@ +// +build !js + +package websocket + +import ( + "compress/flate" + "io" + "net/http" + "sync" +) + +func (m CompressionMode) opts() *compressionOptions { + if m == CompressionDisabled { + return nil + } + return &compressionOptions{ + clientNoContextTakeover: m == CompressionNoContextTakeover, + serverNoContextTakeover: m == CompressionNoContextTakeover, + } +} + +type compressionOptions struct { + clientNoContextTakeover bool + serverNoContextTakeover bool +} + +func (copts *compressionOptions) setHeader(h http.Header) { + s := "permessage-deflate" + if copts.clientNoContextTakeover { + s += "; client_no_context_takeover" + } + if copts.serverNoContextTakeover { + s += "; server_no_context_takeover" + } + h.Set("Sec-WebSocket-Extensions", s) +} + +// These bytes are required to get flate.Reader to return. +// They are removed when sending to avoid the overhead as +// WebSocket framing tell's when the message has ended but then +// we need to add them back otherwise flate.Reader keeps +// trying to return more bytes. +const deflateMessageTail = "\x00\x00\xff\xff" + +func (c *Conn) writeNoContextTakeOver() bool { + return c.client && c.copts.clientNoContextTakeover || !c.client && c.copts.serverNoContextTakeover +} + +func (c *Conn) readNoContextTakeOver() bool { + return !c.client && c.copts.clientNoContextTakeover || c.client && c.copts.serverNoContextTakeover +} + +type trimLastFourBytesWriter struct { + w io.Writer + tail []byte +} + +func (tw *trimLastFourBytesWriter) reset() { + tw.tail = tw.tail[:0] +} + +func (tw *trimLastFourBytesWriter) Write(p []byte) (int, error) { + extra := len(tw.tail) + len(p) - 4 + + if extra <= 0 { + tw.tail = append(tw.tail, p...) + return len(p), nil + } + + // Now we need to write as many extra bytes as we can from the previous tail. + if extra > len(tw.tail) { + extra = len(tw.tail) + } + if extra > 0 { + _, err := tw.w.Write(tw.tail[:extra]) + if err != nil { + return 0, err + } + tw.tail = tw.tail[extra:] + } + + // If p is less than or equal to 4 bytes, + // all of it is is part of the tail. + if len(p) <= 4 { + tw.tail = append(tw.tail, p...) + return len(p), nil + } + + // Otherwise, only the last 4 bytes are. + tw.tail = append(tw.tail, p[len(p)-4:]...) + + p = p[:len(p)-4] + n, err := tw.w.Write(p) + return n + 4, err +} + +var flateReaderPool sync.Pool + +func getFlateReader(r io.Reader, dict []byte) io.Reader { + fr, ok := flateReaderPool.Get().(io.Reader) + if !ok { + return flate.NewReaderDict(r, dict) + } + fr.(flate.Resetter).Reset(r, dict) + return fr +} + +func putFlateReader(fr io.Reader) { + flateReaderPool.Put(fr) +} + +var flateWriterPool sync.Pool + +func getFlateWriter(w io.Writer) *flate.Writer { + fw, ok := flateWriterPool.Get().(*flate.Writer) + if !ok { + fw, _ = flate.NewWriter(w, flate.BestSpeed) + return fw + } + fw.Reset(w) + return fw +} + +func putFlateWriter(w *flate.Writer) { + flateWriterPool.Put(w) +} + +type slidingWindow struct { + r io.Reader + buf []byte +} + +func newSlidingWindow(n int) *slidingWindow { + return &slidingWindow{ + buf: make([]byte, 0, n), + } +} + +func (w *slidingWindow) write(p []byte) { + if len(p) >= cap(w.buf) { + w.buf = w.buf[:cap(w.buf)] + p = p[len(p)-cap(w.buf):] + copy(w.buf, p) + return + } + + left := cap(w.buf) - len(w.buf) + if left < len(p) { + // We need to shift spaceNeeded bytes from the end to make room for p at the end. + spaceNeeded := len(p) - left + copy(w.buf, w.buf[spaceNeeded:]) + w.buf = w.buf[:len(w.buf)-spaceNeeded] + } + + w.buf = append(w.buf, p...) +} diff --git a/compress_test.go b/compress_test.go index 15d334d6..51f658c8 100644 --- a/compress_test.go +++ b/compress_test.go @@ -1,11 +1,11 @@ +// +build !js + package websocket import ( "strings" "testing" - "cdr.dev/slog/sloggers/slogtest/assert" - "nhooyr.io/websocket/internal/test/xrand" ) @@ -15,14 +15,21 @@ func Test_slidingWindow(t *testing.T) { const testCount = 99 const maxWindow = 99999 for i := 0; i < testCount; i++ { - input := xrand.String(maxWindow) - windowLength := xrand.Int(maxWindow) - r := newSlidingWindow(windowLength) - r.write([]byte(input)) - - if cap(r.buf) != windowLength { - t.Fatalf("sliding window length changed somehow: %q and windowLength %d", input, windowLength) - } - assert.True(t, "hasSuffix", strings.HasSuffix(input, string(r.buf))) + t.Run("", func(t *testing.T) { + t.Parallel() + + input := xrand.String(maxWindow) + windowLength := xrand.Int(maxWindow) + r := newSlidingWindow(windowLength) + r.write([]byte(input)) + + if cap(r.buf) != windowLength { + t.Fatalf("sliding window length changed somehow: %q and windowLength %d", input, windowLength) + } + + if !strings.HasSuffix(input, string(r.buf)) { + t.Fatalf("r.buf is not a suffix of input: %q and %q", input, r.buf) + } + }) } } diff --git a/conn.go b/conn.go index 163802bb..e58a8748 100644 --- a/conn.go +++ b/conn.go @@ -2,18 +2,6 @@ package websocket -import ( - "bufio" - "context" - "io" - "runtime" - "strconv" - "sync" - "sync/atomic" - - "golang.org/x/xerrors" -) - // MessageType represents the type of a WebSocket message. // See https://tools.ietf.org/html/rfc6455#section-5.6 type MessageType int @@ -25,252 +13,3 @@ const ( // MessageBinary is for binary messages like protobufs. MessageBinary ) - -// Conn represents a WebSocket connection. -// All methods may be called concurrently except for Reader and Read. -// -// You must always read from the connection. Otherwise control -// frames will not be handled. See Reader and CloseRead. -// -// Be sure to call Close on the connection when you -// are finished with it to release associated resources. -// -// On any error from any method, the connection is closed -// with an appropriate reason. -type Conn struct { - subprotocol string - rwc io.ReadWriteCloser - client bool - copts *compressionOptions - flateThreshold int - br *bufio.Reader - bw *bufio.Writer - - readTimeout chan context.Context - writeTimeout chan context.Context - - // Read state. - readMu *mu - readControlBuf [maxControlPayload]byte - msgReader *msgReader - readCloseFrameErr error - - // Write state. - msgWriter *msgWriter - writeFrameMu *mu - writeBuf []byte - writeHeader header - - closed chan struct{} - closeMu sync.Mutex - closeErr error - wroteClose bool - - pingCounter int32 - activePingsMu sync.Mutex - activePings map[string]chan<- struct{} -} - -type connConfig struct { - subprotocol string - rwc io.ReadWriteCloser - client bool - copts *compressionOptions - flateThreshold int - - br *bufio.Reader - bw *bufio.Writer -} - -func newConn(cfg connConfig) *Conn { - c := &Conn{ - subprotocol: cfg.subprotocol, - rwc: cfg.rwc, - client: cfg.client, - copts: cfg.copts, - flateThreshold: cfg.flateThreshold, - - br: cfg.br, - bw: cfg.bw, - - readTimeout: make(chan context.Context), - writeTimeout: make(chan context.Context), - - closed: make(chan struct{}), - activePings: make(map[string]chan<- struct{}), - } - if c.flate() && c.flateThreshold == 0 { - c.flateThreshold = 256 - if c.writeNoContextTakeOver() { - c.flateThreshold = 512 - } - } - - c.readMu = newMu(c) - c.writeFrameMu = newMu(c) - - c.msgReader = newMsgReader(c) - - c.msgWriter = newMsgWriter(c) - if c.client { - c.writeBuf = extractBufioWriterBuf(c.bw, c.rwc) - } - - runtime.SetFinalizer(c, func(c *Conn) { - c.close(xerrors.New("connection garbage collected")) - }) - - go c.timeoutLoop() - - return c -} - -// Subprotocol returns the negotiated subprotocol. -// An empty string means the default protocol. -func (c *Conn) Subprotocol() string { - return c.subprotocol -} - -func (c *Conn) close(err error) { - c.closeMu.Lock() - defer c.closeMu.Unlock() - - if c.isClosed() { - return - } - close(c.closed) - runtime.SetFinalizer(c, nil) - c.setCloseErrLocked(err) - - // Have to close after c.closed is closed to ensure any goroutine that wakes up - // from the connection being closed also sees that c.closed is closed and returns - // closeErr. - c.rwc.Close() - - go func() { - if c.client { - c.writeFrameMu.Lock(context.Background()) - putBufioWriter(c.bw) - } - c.msgWriter.close() - - c.msgReader.close() - if c.client { - putBufioReader(c.br) - } - }() -} - -func (c *Conn) timeoutLoop() { - readCtx := context.Background() - writeCtx := context.Background() - - for { - select { - case <-c.closed: - return - - case writeCtx = <-c.writeTimeout: - case readCtx = <-c.readTimeout: - - case <-readCtx.Done(): - c.setCloseErr(xerrors.Errorf("read timed out: %w", readCtx.Err())) - go c.writeError(StatusPolicyViolation, xerrors.New("timed out")) - case <-writeCtx.Done(): - c.close(xerrors.Errorf("write timed out: %w", writeCtx.Err())) - return - } - } -} - -func (c *Conn) flate() bool { - return c.copts != nil -} - -// Ping sends a ping to the peer and waits for a pong. -// Use this to measure latency or ensure the peer is responsive. -// Ping must be called concurrently with Reader as it does -// not read from the connection but instead waits for a Reader call -// to read the pong. -// -// TCP Keepalives should suffice for most use cases. -func (c *Conn) Ping(ctx context.Context) error { - p := atomic.AddInt32(&c.pingCounter, 1) - - err := c.ping(ctx, strconv.Itoa(int(p))) - if err != nil { - return xerrors.Errorf("failed to ping: %w", err) - } - return nil -} - -func (c *Conn) ping(ctx context.Context, p string) error { - pong := make(chan struct{}) - - c.activePingsMu.Lock() - c.activePings[p] = pong - c.activePingsMu.Unlock() - - defer func() { - c.activePingsMu.Lock() - delete(c.activePings, p) - c.activePingsMu.Unlock() - }() - - err := c.writeControl(ctx, opPing, []byte(p)) - if err != nil { - return err - } - - select { - case <-c.closed: - return c.closeErr - case <-ctx.Done(): - err := xerrors.Errorf("failed to wait for pong: %w", ctx.Err()) - c.close(err) - return err - case <-pong: - return nil - } -} - -type mu struct { - c *Conn - ch chan struct{} -} - -func newMu(c *Conn) *mu { - return &mu{ - c: c, - ch: make(chan struct{}, 1), - } -} - -func (m *mu) Lock(ctx context.Context) error { - select { - case <-m.c.closed: - return m.c.closeErr - case <-ctx.Done(): - err := xerrors.Errorf("failed to acquire lock: %w", ctx.Err()) - m.c.close(err) - return err - case m.ch <- struct{}{}: - return nil - } -} - -func (m *mu) TryLock() bool { - select { - case m.ch <- struct{}{}: - return true - default: - return false - } -} - -func (m *mu) Unlock() { - select { - case <-m.ch: - default: - } -} diff --git a/conn_notjs.go b/conn_notjs.go new file mode 100644 index 00000000..d2fea4d4 --- /dev/null +++ b/conn_notjs.go @@ -0,0 +1,264 @@ +// +build !js + +package websocket + +import ( + "bufio" + "context" + "io" + "runtime" + "strconv" + "sync" + "sync/atomic" + + "golang.org/x/xerrors" +) + +// Conn represents a WebSocket connection. +// All methods may be called concurrently except for Reader and Read. +// +// You must always read from the connection. Otherwise control +// frames will not be handled. See Reader and CloseRead. +// +// Be sure to call Close on the connection when you +// are finished with it to release associated resources. +// +// On any error from any method, the connection is closed +// with an appropriate reason. +type Conn struct { + subprotocol string + rwc io.ReadWriteCloser + client bool + copts *compressionOptions + flateThreshold int + br *bufio.Reader + bw *bufio.Writer + + readTimeout chan context.Context + writeTimeout chan context.Context + + // Read state. + readMu *mu + readControlBuf [maxControlPayload]byte + msgReader *msgReader + readCloseFrameErr error + + // Write state. + msgWriter *msgWriter + writeFrameMu *mu + writeBuf []byte + writeHeader header + + closed chan struct{} + closeMu sync.Mutex + closeErr error + wroteClose bool + + pingCounter int32 + activePingsMu sync.Mutex + activePings map[string]chan<- struct{} +} + +type connConfig struct { + subprotocol string + rwc io.ReadWriteCloser + client bool + copts *compressionOptions + flateThreshold int + + br *bufio.Reader + bw *bufio.Writer +} + +func newConn(cfg connConfig) *Conn { + c := &Conn{ + subprotocol: cfg.subprotocol, + rwc: cfg.rwc, + client: cfg.client, + copts: cfg.copts, + flateThreshold: cfg.flateThreshold, + + br: cfg.br, + bw: cfg.bw, + + readTimeout: make(chan context.Context), + writeTimeout: make(chan context.Context), + + closed: make(chan struct{}), + activePings: make(map[string]chan<- struct{}), + } + if c.flate() && c.flateThreshold == 0 { + c.flateThreshold = 256 + if c.writeNoContextTakeOver() { + c.flateThreshold = 512 + } + } + + c.readMu = newMu(c) + c.writeFrameMu = newMu(c) + + c.msgReader = newMsgReader(c) + + c.msgWriter = newMsgWriter(c) + if c.client { + c.writeBuf = extractBufioWriterBuf(c.bw, c.rwc) + } + + runtime.SetFinalizer(c, func(c *Conn) { + c.close(xerrors.New("connection garbage collected")) + }) + + go c.timeoutLoop() + + return c +} + +// Subprotocol returns the negotiated subprotocol. +// An empty string means the default protocol. +func (c *Conn) Subprotocol() string { + return c.subprotocol +} + +func (c *Conn) close(err error) { + c.closeMu.Lock() + defer c.closeMu.Unlock() + + if c.isClosed() { + return + } + close(c.closed) + runtime.SetFinalizer(c, nil) + c.setCloseErrLocked(err) + + // Have to close after c.closed is closed to ensure any goroutine that wakes up + // from the connection being closed also sees that c.closed is closed and returns + // closeErr. + c.rwc.Close() + + go func() { + if c.client { + c.writeFrameMu.Lock(context.Background()) + putBufioWriter(c.bw) + } + c.msgWriter.close() + + c.msgReader.close() + if c.client { + putBufioReader(c.br) + } + }() +} + +func (c *Conn) timeoutLoop() { + readCtx := context.Background() + writeCtx := context.Background() + + for { + select { + case <-c.closed: + return + + case writeCtx = <-c.writeTimeout: + case readCtx = <-c.readTimeout: + + case <-readCtx.Done(): + c.setCloseErr(xerrors.Errorf("read timed out: %w", readCtx.Err())) + go c.writeError(StatusPolicyViolation, xerrors.New("timed out")) + case <-writeCtx.Done(): + c.close(xerrors.Errorf("write timed out: %w", writeCtx.Err())) + return + } + } +} + +func (c *Conn) flate() bool { + return c.copts != nil +} + +// Ping sends a ping to the peer and waits for a pong. +// Use this to measure latency or ensure the peer is responsive. +// Ping must be called concurrently with Reader as it does +// not read from the connection but instead waits for a Reader call +// to read the pong. +// +// TCP Keepalives should suffice for most use cases. +func (c *Conn) Ping(ctx context.Context) error { + p := atomic.AddInt32(&c.pingCounter, 1) + + err := c.ping(ctx, strconv.Itoa(int(p))) + if err != nil { + return xerrors.Errorf("failed to ping: %w", err) + } + return nil +} + +func (c *Conn) ping(ctx context.Context, p string) error { + pong := make(chan struct{}) + + c.activePingsMu.Lock() + c.activePings[p] = pong + c.activePingsMu.Unlock() + + defer func() { + c.activePingsMu.Lock() + delete(c.activePings, p) + c.activePingsMu.Unlock() + }() + + err := c.writeControl(ctx, opPing, []byte(p)) + if err != nil { + return err + } + + select { + case <-c.closed: + return c.closeErr + case <-ctx.Done(): + err := xerrors.Errorf("failed to wait for pong: %w", ctx.Err()) + c.close(err) + return err + case <-pong: + return nil + } +} + +type mu struct { + c *Conn + ch chan struct{} +} + +func newMu(c *Conn) *mu { + return &mu{ + c: c, + ch: make(chan struct{}, 1), + } +} + +func (m *mu) Lock(ctx context.Context) error { + select { + case <-m.c.closed: + return m.c.closeErr + case <-ctx.Done(): + err := xerrors.Errorf("failed to acquire lock: %w", ctx.Err()) + m.c.close(err) + return err + case m.ch <- struct{}{}: + return nil + } +} + +func (m *mu) TryLock() bool { + select { + case m.ch <- struct{}{}: + return true + default: + return false + } +} + +func (m *mu) Unlock() { + select { + case <-m.ch: + default: + } +} diff --git a/conn_test.go b/conn_test.go index 02606ef5..5c817a25 100644 --- a/conn_test.go +++ b/conn_test.go @@ -4,33 +4,23 @@ package websocket_test import ( "context" - "io" + "fmt" + "net/http" + "net/http/httptest" + "os" + "os/exec" + "sync" "testing" "time" "golang.org/x/xerrors" "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/test/cmp" "nhooyr.io/websocket/internal/test/wstest" "nhooyr.io/websocket/internal/test/xrand" + "nhooyr.io/websocket/internal/xsync" ) -func goFn(fn func() error) chan error { - errs := make(chan error) - go func() { - defer func() { - r := recover() - if r != nil { - errs <- xerrors.Errorf("panic in gofn: %v", r) - } - }() - errs <- fn() - }() - - return errs -} - func TestConn(t *testing.T) { t.Parallel() @@ -44,7 +34,7 @@ func TestConn(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() - copts := websocket.CompressionOptions{ + copts := &websocket.CompressionOptions{ Mode: websocket.CompressionMode(xrand.Int(int(websocket.CompressionDisabled) + 1)), Threshold: xrand.Int(9999), } @@ -60,8 +50,8 @@ func TestConn(t *testing.T) { defer c1.Close(websocket.StatusInternalError, "") defer c2.Close(websocket.StatusInternalError, "") - echoLoopErr := goFn(func() error { - err := echoLoop(ctx, c1) + echoLoopErr := xsync.Go(func() error { + err := wstest.EchoLoop(ctx, c1) return assertCloseStatus(websocket.StatusNormalClosure, err) }) defer func() { @@ -72,39 +62,13 @@ func TestConn(t *testing.T) { }() defer cancel() - c2.SetReadLimit(1 << 30) + c2.SetReadLimit(131072) for i := 0; i < 5; i++ { - n := xrand.Int(131_072) - - msg := xrand.Bytes(n) - - expType := websocket.MessageBinary - if xrand.Bool() { - expType = websocket.MessageText - } - - writeErr := goFn(func() error { - return c2.Write(ctx, expType, msg) - }) - - actType, act, err := c2.Read(ctx) - if err != nil { - t.Fatal(err) - } - - err = <-writeErr + err := wstest.Echo(ctx, c2, 131072) if err != nil { t.Fatal(err) } - - if expType != actType { - t.Fatalf("unexpected message typ (%v): %v", expType, actType) - } - - if !cmp.Equal(msg, act) { - t.Fatalf("unexpected msg read: %v", cmp.Diff(msg, act)) - } } c2.Close(websocket.StatusNormalClosure, "") @@ -113,47 +77,50 @@ func TestConn(t *testing.T) { }) } -func assertCloseStatus(exp websocket.StatusCode, err error) error { - if websocket.CloseStatus(err) == -1 { - return xerrors.Errorf("expected websocket.CloseError: %T %v", err, err) - } - if websocket.CloseStatus(err) != exp { - return xerrors.Errorf("unexpected close status (%v):%v", exp, err) - } - return nil -} - -// echoLoop echos every msg received from c until an error -// occurs or the context expires. -// The read limit is set to 1 << 30. -func echoLoop(ctx context.Context, c *websocket.Conn) error { - defer c.Close(websocket.StatusInternalError, "") - - c.SetReadLimit(1 << 30) +func TestWasm(t *testing.T) { + t.Parallel() - ctx, cancel := context.WithTimeout(ctx, time.Minute) - defer cancel() + var wg sync.WaitGroup + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + wg.Add(1) + defer wg.Done() - b := make([]byte, 32<<10) - for { - typ, r, err := c.Reader(ctx) + c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + Subprotocols: []string{"echo"}, + InsecureSkipVerify: true, + }) if err != nil { - return err + t.Error(err) + return } + defer c.Close(websocket.StatusInternalError, "") - w, err := c.Writer(ctx, typ) - if err != nil { - return err + err = wstest.EchoLoop(r.Context(), c) + if websocket.CloseStatus(err) != websocket.StatusNormalClosure { + t.Errorf("echoLoop: %v", err) } + })) + defer wg.Wait() + defer s.Close() - _, err = io.CopyBuffer(w, r, b) - if err != nil { - return err - } + ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) + defer cancel() - err = w.Close() - if err != nil { - return err - } + cmd := exec.CommandContext(ctx, "go", "test", "-exec=wasmbrowsertest", "./...") + cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm", fmt.Sprintf("WS_ECHO_SERVER_URL=%v", wstest.URL(s))) + + b, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("wasm test binary failed: %v:\n%s", err, b) } } + +func assertCloseStatus(exp websocket.StatusCode, err error) error { + if websocket.CloseStatus(err) == -1 { + return xerrors.Errorf("expected websocket.CloseError: %T %v", err, err) + } + if websocket.CloseStatus(err) != exp { + return xerrors.Errorf("unexpected close status (%v):%v", exp, err) + } + return nil +} diff --git a/dial.go b/dial.go index a1509ab5..3e2042e5 100644 --- a/dial.go +++ b/dial.go @@ -35,8 +35,7 @@ type DialOptions struct { // CompressionOptions controls the compression options. // See docs on the CompressionOptions type. - // TODO make * - CompressionOptions CompressionOptions + CompressionOptions *CompressionOptions } // Dial performs a WebSocket handshake on url. @@ -60,6 +59,7 @@ func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) ( if opts == nil { opts = &DialOptions{} } + opts = &*opts if opts.HTTPClient == nil { opts.HTTPClient = http.DefaultClient @@ -67,6 +67,9 @@ func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) ( if opts.HTTPHeader == nil { opts.HTTPHeader = http.Header{} } + if opts.CompressionOptions == nil { + opts.CompressionOptions = &CompressionOptions{} + } secWebSocketKey, err := secWebSocketKey(rand) if err != nil { diff --git a/dial_test.go b/dial_test.go index 3be52208..e38e8f17 100644 --- a/dial_test.go +++ b/dial_test.go @@ -223,7 +223,7 @@ func Test_verifyServerHandshake(t *testing.T) { } _, err = verifyServerResponse(opts, key, resp) if (err == nil) != tc.success { - t.Fatalf("unexpected error: %+v", err) + t.Fatalf("unexpected error: %v", err) } }) } diff --git a/example_test.go b/example_test.go index 1842b765..075107b0 100644 --- a/example_test.go +++ b/example_test.go @@ -74,8 +74,7 @@ func ExampleCloseStatus() { _, _, err = c.Reader(ctx) if websocket.CloseStatus(err) != websocket.StatusNormalClosure { - log.Fatalf("expected to be disconnected with StatusNormalClosure but got: %+v", err) - return + log.Fatalf("expected to be disconnected with StatusNormalClosure but got: %v", err) } } diff --git a/frame.go b/frame.go index 47ff40f7..0257835e 100644 --- a/frame.go +++ b/frame.go @@ -1,5 +1,3 @@ -// +build !js - package websocket import ( diff --git a/internal/test/cmp/cmp.go b/internal/test/cmp/cmp.go index d0eee6d0..cdbadf70 100644 --- a/internal/test/cmp/cmp.go +++ b/internal/test/cmp/cmp.go @@ -2,6 +2,7 @@ package cmp import ( "reflect" + "strings" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" @@ -20,3 +21,11 @@ func Diff(v1, v2 interface{}) string { return true })) } + +// ErrorContains returns whether err.Error() contains sub. +func ErrorContains(err error, sub string) bool { + if err == nil { + return false + } + return strings.Contains(err.Error(), sub) +} diff --git a/internal/test/wstest/echo.go b/internal/test/wstest/echo.go new file mode 100644 index 00000000..70b2ba57 --- /dev/null +++ b/internal/test/wstest/echo.go @@ -0,0 +1,90 @@ +package wstest + +import ( + "context" + "io" + "time" + + "golang.org/x/xerrors" + + "nhooyr.io/websocket" + "nhooyr.io/websocket/internal/test/cmp" + "nhooyr.io/websocket/internal/test/xrand" + "nhooyr.io/websocket/internal/xsync" +) + +// EchoLoop echos every msg received from c until an error +// occurs or the context expires. +// The read limit is set to 1 << 30. +func EchoLoop(ctx context.Context, c *websocket.Conn) error { + defer c.Close(websocket.StatusInternalError, "") + + c.SetReadLimit(1 << 30) + + ctx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + + b := make([]byte, 32<<10) + for { + typ, r, err := c.Reader(ctx) + if err != nil { + return err + } + + w, err := c.Writer(ctx, typ) + if err != nil { + return err + } + + _, err = io.CopyBuffer(w, r, b) + if err != nil { + return err + } + + err = w.Close() + if err != nil { + return err + } + } +} + +// Echo writes a message and ensures the same is sent back on c. +func Echo(ctx context.Context, c *websocket.Conn, max int) error { + expType := websocket.MessageBinary + if xrand.Bool() { + expType = websocket.MessageText + } + + msg := randMessage(expType, xrand.Int(max)) + + writeErr := xsync.Go(func() error { + return c.Write(ctx, expType, msg) + }) + + actType, act, err := c.Read(ctx) + if err != nil { + return err + } + + err = <-writeErr + if err != nil { + return err + } + + if expType != actType { + return xerrors.Errorf("unexpected message typ (%v): %v", expType, actType) + } + + if !cmp.Equal(msg, act) { + return xerrors.Errorf("unexpected msg read: %v", cmp.Diff(msg, act)) + } + + return nil +} + +func randMessage(typ websocket.MessageType, n int) []byte { + if typ == websocket.MessageBinary { + return xrand.Bytes(n) + } + return []byte(xrand.String(n)) +} diff --git a/internal/test/wstest/pipe.go b/internal/test/wstest/pipe.go index e958aea4..81705a8a 100644 --- a/internal/test/wstest/pipe.go +++ b/internal/test/wstest/pipe.go @@ -1,3 +1,5 @@ +// +build !js + package wstest import ( @@ -30,6 +32,7 @@ func Pipe(dialOpts *websocket.DialOptions, acceptOpts *websocket.AcceptOptions) if dialOpts == nil { dialOpts = &websocket.DialOptions{} } + dialOpts = &*dialOpts dialOpts.HTTPClient = &http.Client{ Transport: tt, } diff --git a/internal/test/wstest/url.go b/internal/test/wstest/url.go new file mode 100644 index 00000000..a11c61b4 --- /dev/null +++ b/internal/test/wstest/url.go @@ -0,0 +1,11 @@ +package wstest + +import ( + "net/http/httptest" + "strings" +) + +// URL returns the ws url for s. +func URL(s *httptest.Server) string { + return strings.Replace(s.URL, "http", "ws", 1) +} diff --git a/internal/xsync/go.go b/internal/xsync/go.go new file mode 100644 index 00000000..96cf8103 --- /dev/null +++ b/internal/xsync/go.go @@ -0,0 +1,25 @@ +package xsync + +import ( + "golang.org/x/xerrors" +) + +// Go allows running a function in another goroutine +// and waiting for its error. +func Go(fn func() error) chan error { + errs := make(chan error, 1) + go func() { + defer func() { + r := recover() + if r != nil { + select { + case errs <- xerrors.Errorf("panic in go fn: %v", r): + default: + } + } + }() + errs <- fn() + }() + + return errs +} diff --git a/internal/xsync/go_test.go b/internal/xsync/go_test.go new file mode 100644 index 00000000..c0613e64 --- /dev/null +++ b/internal/xsync/go_test.go @@ -0,0 +1,20 @@ +package xsync + +import ( + "testing" + + "nhooyr.io/websocket/internal/test/cmp" +) + +func TestGoRecover(t *testing.T) { + t.Parallel() + + errs := Go(func() error { + panic("anmol") + }) + + err := <-errs + if !cmp.ErrorContains(err, "anmol") { + t.Fatalf("unexpected err: %v", err) + } +} diff --git a/internal/xsync/int64.go b/internal/xsync/int64.go new file mode 100644 index 00000000..a0c40204 --- /dev/null +++ b/internal/xsync/int64.go @@ -0,0 +1,23 @@ +package xsync + +import ( + "sync/atomic" +) + +// Int64 represents an atomic int64. +type Int64 struct { + // We do not use atomic.Load/StoreInt64 since it does not + // work on 32 bit computers but we need 64 bit integers. + i atomic.Value +} + +// Load loads the int64. +func (v *Int64) Load() int64 { + i, _ := v.i.Load().(int64) + return i +} + +// Store stores the int64. +func (v *Int64) Store(i int64) { + v.i.Store(i) +} diff --git a/read.go b/read.go index b681a944..e723ef3c 100644 --- a/read.go +++ b/read.go @@ -7,12 +7,12 @@ import ( "io" "io/ioutil" "strings" - "sync/atomic" "time" "golang.org/x/xerrors" "nhooyr.io/websocket/internal/errd" + "nhooyr.io/websocket/internal/xsync" ) // Reader reads from the connection until until there is a WebSocket @@ -415,7 +415,7 @@ func (mr *msgReader) read(p []byte) (int, error) { type limitReader struct { c *Conn r io.Reader - limit atomicInt64 + limit xsync.Int64 n int64 } @@ -448,21 +448,6 @@ func (lr *limitReader) Read(p []byte) (int, error) { return n, err } -type atomicInt64 struct { - // We do not use atomic.Load/StoreInt64 since it does not - // work on 32 bit computers but we need 64 bit integers. - i atomic.Value -} - -func (v *atomicInt64) Load() int64 { - i, _ := v.i.Load().(int64) - return i -} - -func (v *atomicInt64) Store(i int64) { - v.i.Store(i) -} - type readerFunc func(p []byte) (int, error) func (f readerFunc) Read(p []byte) (int, error) { diff --git a/ws_js.go b/ws_js.go index 3ce6f34d..de76afa6 100644 --- a/ws_js.go +++ b/ws_js.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "io" + "net/http" "reflect" "runtime" "sync" @@ -13,6 +14,7 @@ import ( "nhooyr.io/websocket/internal/bpool" "nhooyr.io/websocket/internal/wsjs" + "nhooyr.io/websocket/internal/xsync" ) // MessageType represents the type of a WebSocket message. @@ -32,10 +34,10 @@ type Conn struct { ws wsjs.WebSocket // read limit for a message in bytes. - msgReadLimit atomicInt64 + msgReadLimit xsync.Int64 closingMu sync.Mutex - isReadClosed atomicInt64 + isReadClosed xsync.Int64 closeOnce sync.Once closed chan struct{} closeErrOnce sync.Once @@ -67,11 +69,8 @@ func (c *Conn) init() { c.closed = make(chan struct{}) c.readSignal = make(chan struct{}, 1) - c.msgReadLimit = &wssync.Int64{} c.msgReadLimit.Store(32768) - c.isReadClosed = &wssync.Int64{} - c.releaseOnClose = c.ws.OnClose(func(e wsjs.CloseEvent) { err := CloseError{ Code: StatusCode(e.Code), @@ -121,7 +120,7 @@ func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { return 0, nil, xerrors.Errorf("failed to read: %w", err) } if int64(len(p)) > c.msgReadLimit.Load() { - err := xerrors.Errorf("read limited at %v bytes", c.msgReadLimit) + err := xerrors.Errorf("read limited at %v bytes", c.msgReadLimit.Load()) c.Close(StatusMessageTooBig, err.Error()) return 0, nil, err } @@ -248,17 +247,17 @@ type DialOptions struct { // Dial creates a new WebSocket connection to the given url with the given options. // The passed context bounds the maximum time spent waiting for the connection to open. -// The returned *http.Response is always nil or the zero value. It's only in the signature +// The returned *http.Response is always nil or a mock. It's only in the signature // to match the core API. -func Dial(ctx context.Context, url string, opts *DialOptions) (*Conn, error) { - c, err := dial(ctx, url, opts) +func Dial(ctx context.Context, url string, opts *DialOptions) (*Conn, *http.Response, error) { + c, resp, err := dial(ctx, url, opts) if err != nil { - return nil, resp, xerrors.Errorf("failed to WebSocket dial %q: %w", url, err) + return nil, nil, xerrors.Errorf("failed to WebSocket dial %q: %w", url, err) } - return c, nil + return c, resp, nil } -func dial(ctx context.Context, url string, opts *DialOptions) (*Conn, error) { +func dial(ctx context.Context, url string, opts *DialOptions) (*Conn, *http.Response, error) { if opts == nil { opts = &DialOptions{} } @@ -284,11 +283,12 @@ func dial(ctx context.Context, url string, opts *DialOptions) (*Conn, error) { c.Close(StatusPolicyViolation, "dial timed out") return nil, nil, ctx.Err() case <-opench: + return c, &http.Response{ + StatusCode: http.StatusSwitchingProtocols, + }, nil case <-c.closed: - return c, nil, c.closeErr + return nil, nil, c.closeErr } - - return c, nil } // Reader attempts to read a message from the connection. diff --git a/ws_js_test.go b/ws_js_test.go index 65309bff..8d49af6b 100644 --- a/ws_js_test.go +++ b/ws_js_test.go @@ -1,4 +1,4 @@ -package websocket +package websocket_test import ( "context" @@ -6,25 +6,43 @@ import ( "os" "testing" "time" + + "nhooyr.io/websocket" + "nhooyr.io/websocket/internal/test/cmp" + "nhooyr.io/websocket/internal/test/wstest" ) -func TestEcho(t *testing.T) { +func TestWasm(t *testing.T) { t.Parallel() ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() - c, resp, err := Dial(ctx, os.Getenv("WS_ECHO_SERVER_URL"), &DialOptions{ + c, resp, err := websocket.Dial(ctx, os.Getenv("WS_ECHO_SERVER_URL"), &websocket.DialOptions{ Subprotocols: []string{"echo"}, }) - assert.Success(t, err) - defer c.Close(StatusInternalError, "") + if err != nil { + t.Fatal(err) + } + defer c.Close(websocket.StatusInternalError, "") + + if !cmp.Equal("echo", c.Subprotocol()) { + t.Fatalf("unexpected subprotocol: %v", cmp.Diff("echo", c.Subprotocol())) + } + if !cmp.Equal(http.StatusSwitchingProtocols, resp.StatusCode) { + t.Fatalf("unexpected status code: %v", cmp.Diff(http.StatusSwitchingProtocols, resp.StatusCode)) + } - assertSubprotocol(t, c, "echo") - assert.Equalf(t, &http.Response{}, resp, "http.Response") - echoJSON(t, ctx, c, 1024) - assertEcho(t, ctx, c, MessageBinary, 1024) + c.SetReadLimit(65536) + for i := 0; i < 10; i++ { + err = wstest.Echo(ctx, c, 65536) + if err != nil { + t.Fatal(err) + } + } - err = c.Close(StatusNormalClosure, "") - assert.Success(t, err) + err = c.Close(websocket.StatusNormalClosure, "") + if err != nil { + t.Fatal(err) + } } From 69ff675fa5b55466a0a7ad8af391cec679b21216 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 9 Feb 2020 01:27:32 -0500 Subject: [PATCH 247/519] More tests and fixes --- close_notjs.go | 21 ++-- compress_notjs.go | 11 -- conn_notjs.go | 22 ++-- conn_test.go | 141 +++++++++++++++++++++++-- dial_test.go | 18 +++- go.mod | 8 -- go.sum | 255 ---------------------------------------------- write.go | 6 ++ ws_js_test.go | 2 +- 9 files changed, 170 insertions(+), 314 deletions(-) diff --git a/close_notjs.go b/close_notjs.go index dd1b0e0d..08a1ea05 100644 --- a/close_notjs.go +++ b/close_notjs.go @@ -35,7 +35,7 @@ func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { defer errd.Wrap(&err, "failed to close WebSocket") err = c.writeClose(code, reason) - if err != nil { + if CloseStatus(err) == -1 { return err } @@ -46,12 +46,6 @@ func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { return nil } -func (c *Conn) writeError(code StatusCode, err error) { - c.setCloseErr(err) - c.writeClose(code, err.Error()) - c.close(nil) -} - func (c *Conn) writeClose(code StatusCode, reason string) error { c.closeMu.Lock() closing := c.wroteClose @@ -70,7 +64,12 @@ func (c *Conn) writeClose(code StatusCode, reason string) error { var p []byte if ce.Code != StatusNoStatusRcvd { - p = ce.bytes() + var err error + p, err = ce.bytes() + if err != nil { + log.Printf("websocket: %v", err) + return err + } } return c.writeControl(context.Background(), opClose, p) @@ -148,16 +147,16 @@ func validWireCloseCode(code StatusCode) bool { return false } -func (ce CloseError) bytes() []byte { +func (ce CloseError) bytes() ([]byte, error) { p, err := ce.bytesErr() if err != nil { - log.Printf("websocket: failed to marshal close frame: %v", err) + err = xerrors.Errorf("failed to marshal close frame: %w", err) ce = CloseError{ Code: StatusInternalError, } p, _ = ce.bytesErr() } - return p + return p, err } const maxCloseReason = maxControlPayload - 2 diff --git a/compress_notjs.go b/compress_notjs.go index 8bc2f87b..6ab6e284 100644 --- a/compress_notjs.go +++ b/compress_notjs.go @@ -10,9 +10,6 @@ import ( ) func (m CompressionMode) opts() *compressionOptions { - if m == CompressionDisabled { - return nil - } return &compressionOptions{ clientNoContextTakeover: m == CompressionNoContextTakeover, serverNoContextTakeover: m == CompressionNoContextTakeover, @@ -42,14 +39,6 @@ func (copts *compressionOptions) setHeader(h http.Header) { // trying to return more bytes. const deflateMessageTail = "\x00\x00\xff\xff" -func (c *Conn) writeNoContextTakeOver() bool { - return c.client && c.copts.clientNoContextTakeover || !c.client && c.copts.serverNoContextTakeover -} - -func (c *Conn) readNoContextTakeOver() bool { - return !c.client && c.copts.clientNoContextTakeover || c.client && c.copts.serverNoContextTakeover -} - type trimLastFourBytesWriter struct { w io.Writer tail []byte diff --git a/conn_notjs.go b/conn_notjs.go index d2fea4d4..96d17b73 100644 --- a/conn_notjs.go +++ b/conn_notjs.go @@ -87,12 +87,6 @@ func newConn(cfg connConfig) *Conn { closed: make(chan struct{}), activePings: make(map[string]chan<- struct{}), } - if c.flate() && c.flateThreshold == 0 { - c.flateThreshold = 256 - if c.writeNoContextTakeOver() { - c.flateThreshold = 512 - } - } c.readMu = newMu(c) c.writeFrameMu = newMu(c) @@ -104,6 +98,13 @@ func newConn(cfg connConfig) *Conn { c.writeBuf = extractBufioWriterBuf(c.bw, c.rwc) } + if c.flate() && c.flateThreshold == 0 { + c.flateThreshold = 256 + if !c.msgWriter.flateContextTakeover() { + c.flateThreshold = 512 + } + } + runtime.SetFinalizer(c, func(c *Conn) { c.close(xerrors.New("connection garbage collected")) }) @@ -247,15 +248,6 @@ func (m *mu) Lock(ctx context.Context) error { } } -func (m *mu) TryLock() bool { - select { - case m.ch <- struct{}{}: - return true - default: - return false - } -} - func (m *mu) Unlock() { select { case <-m.ch: diff --git a/conn_test.go b/conn_test.go index 5c817a25..098fc097 100644 --- a/conn_test.go +++ b/conn_test.go @@ -16,6 +16,7 @@ import ( "golang.org/x/xerrors" "nhooyr.io/websocket" + "nhooyr.io/websocket/internal/test/cmp" "nhooyr.io/websocket/internal/test/wstest" "nhooyr.io/websocket/internal/test/xrand" "nhooyr.io/websocket/internal/xsync" @@ -31,9 +32,6 @@ func TestConn(t *testing.T) { t.Run("", func(t *testing.T) { t.Parallel() - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) - defer cancel() - copts := &websocket.CompressionOptions{ Mode: websocket.CompressionMode(xrand.Int(int(websocket.CompressionDisabled) + 1)), Threshold: xrand.Int(9999), @@ -47,11 +45,14 @@ func TestConn(t *testing.T) { if err != nil { t.Fatal(err) } - defer c1.Close(websocket.StatusInternalError, "") defer c2.Close(websocket.StatusInternalError, "") + defer c1.Close(websocket.StatusInternalError, "") + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() echoLoopErr := xsync.Go(func() error { - err := wstest.EchoLoop(ctx, c1) + err := wstest.EchoLoop(ctx, c2) return assertCloseStatus(websocket.StatusNormalClosure, err) }) defer func() { @@ -62,19 +63,143 @@ func TestConn(t *testing.T) { }() defer cancel() - c2.SetReadLimit(131072) + c1.SetReadLimit(131072) for i := 0; i < 5; i++ { - err := wstest.Echo(ctx, c2, 131072) + err := wstest.Echo(ctx, c1, 131072) if err != nil { t.Fatal(err) } } - c2.Close(websocket.StatusNormalClosure, "") + err = c1.Close(websocket.StatusNormalClosure, "") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } }) } }) + + t.Run("badClose", func(t *testing.T) { + t.Parallel() + + c1, c2, err := wstest.Pipe(nil, nil) + if err != nil { + t.Fatal(err) + } + defer c1.Close(websocket.StatusInternalError, "") + defer c2.Close(websocket.StatusInternalError, "") + + err = c1.Close(-1, "") + if !cmp.ErrorContains(err, "failed to marshal close frame: status code StatusCode(-1) cannot be set") { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("ping", func(t *testing.T) { + t.Parallel() + + c1, c2, err := wstest.Pipe(nil, nil) + if err != nil { + t.Fatal(err) + } + defer c1.Close(websocket.StatusInternalError, "") + defer c2.Close(websocket.StatusInternalError, "") + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) + defer cancel() + + c2.CloseRead(ctx) + c1.CloseRead(ctx) + + for i := 0; i < 10; i++ { + err = c1.Ping(ctx) + if err != nil { + t.Fatal(err) + } + } + + err = c1.Close(websocket.StatusNormalClosure, "") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("badPing", func(t *testing.T) { + t.Parallel() + + c1, c2, err := wstest.Pipe(nil, nil) + if err != nil { + t.Fatal(err) + } + defer c1.Close(websocket.StatusInternalError, "") + defer c2.Close(websocket.StatusInternalError, "") + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + c2.CloseRead(ctx) + + err = c1.Ping(ctx) + if !cmp.ErrorContains(err, "failed to wait for pong") { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("concurrentWrite", func(t *testing.T) { + t.Parallel() + + c1, c2, err := wstest.Pipe(nil, nil) + if err != nil { + t.Fatal(err) + } + defer c2.Close(websocket.StatusInternalError, "") + defer c1.Close(websocket.StatusInternalError, "") + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + discardLoopErr := xsync.Go(func() error { + for { + _, _, err := c2.Read(ctx) + if websocket.CloseStatus(err) == websocket.StatusNormalClosure { + return nil + } + if err != nil { + return err + } + } + }) + defer func() { + err := <-discardLoopErr + if err != nil { + t.Errorf("discard loop error: %v", err) + } + }() + defer cancel() + + msg := xrand.Bytes(xrand.Int(9999)) + const count = 100 + errs := make(chan error, count) + + for i := 0; i < count; i++ { + go func() { + errs <- c1.Write(ctx, websocket.MessageBinary, msg) + }() + } + + for i := 0; i < count; i++ { + err := <-errs + if err != nil { + t.Fatal(err) + } + } + + err = c1.Close(websocket.StatusNormalClosure, "") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) } func TestWasm(t *testing.T) { diff --git a/dial_test.go b/dial_test.go index e38e8f17..c4657415 100644 --- a/dial_test.go +++ b/dial_test.go @@ -13,7 +13,7 @@ import ( "testing" "time" - "cdr.dev/slog/sloggers/slogtest/assert" + "nhooyr.io/websocket/internal/test/cmp" ) func TestBadDials(t *testing.T) { @@ -70,7 +70,9 @@ func TestBadDials(t *testing.T) { } _, _, err := dial(ctx, tc.url, tc.opts, tc.rand) - assert.Error(t, "dial", err) + if err == nil { + t.Fatalf("expected error") + } }) } }) @@ -88,7 +90,9 @@ func TestBadDials(t *testing.T) { }, nil }), }) - assert.ErrorContains(t, "dial", err, "failed to WebSocket dial: expected handshake response status code 101 but got 0") + if !cmp.ErrorContains(err, "failed to WebSocket dial: expected handshake response status code 101 but got 0") { + t.Fatal(err) + } }) t.Run("badBody", func(t *testing.T) { @@ -113,7 +117,9 @@ func TestBadDials(t *testing.T) { _, _, err := Dial(ctx, "ws://example.com", &DialOptions{ HTTPClient: mockHTTPClient(rt), }) - assert.ErrorContains(t, "dial", err, "response body is not a io.ReadWriteCloser") + if !cmp.ErrorContains(err, "response body is not a io.ReadWriteCloser") { + t.Fatal(err) + } }) } @@ -211,7 +217,9 @@ func Test_verifyServerHandshake(t *testing.T) { r := httptest.NewRequest("GET", "/", nil) key, err := secWebSocketKey(rand.Reader) - assert.Success(t, "secWebSocketKey", err) + if err != nil { + t.Fatal(err) + } r.Header.Set("Sec-WebSocket-Key", key) if resp.Header.Get("Sec-WebSocket-Accept") == "" { diff --git a/go.mod b/go.mod index fc4ebb99..cb372391 100644 --- a/go.mod +++ b/go.mod @@ -3,20 +3,12 @@ module nhooyr.io/websocket go 1.12 require ( - cdr.dev/slog v1.3.0 - github.com/alecthomas/chroma v0.7.1 // indirect - github.com/fatih/color v1.9.0 // indirect github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee // indirect github.com/gobwas/pool v0.2.0 // indirect github.com/gobwas/ws v1.0.2 - github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect github.com/golang/protobuf v1.3.3 github.com/google/go-cmp v0.4.0 github.com/gorilla/websocket v1.4.1 - github.com/mattn/go-isatty v0.0.12 // indirect - go.opencensus.io v0.22.3 // indirect - golang.org/x/crypto v0.0.0-20200208060501-ecb85df21340 // indirect - golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 // indirect golang.org/x/time v0.0.0-20191024005414-555d28b269f0 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 ) diff --git a/go.sum b/go.sum index 1d1dc3a6..8cbc66ce 100644 --- a/go.sum +++ b/go.sum @@ -1,271 +1,16 @@ -cdr.dev/slog v1.3.0 h1:MYN1BChIaVEGxdS7I5cpdyMC0+WfJfK8BETAfzfLUGQ= -cdr.dev/slog v1.3.0/go.mod h1:C5OL99WyuOK8YHZdYY57dAPN1jK2WJlCdq2VP6xeQns= -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.49.0 h1:CH+lkubJzcPYB1Ggupcq0+k8Ni2ILdG2lYjDIgavDBQ= -cloud.google.com/go v0.49.0/go.mod h1:hGvAdzcWNbyuxS3nWhD7H2cIJxjRRTRLQVB0bdputVY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= -github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= -github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= -github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= -github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= -github.com/alecthomas/chroma v0.7.0 h1:z+0HgTUmkpRDRz0SRSdMaqOLfJV4F+N1FPDZUZIDUzw= -github.com/alecthomas/chroma v0.7.0/go.mod h1:1U/PfCsTALWWYHDnsIQkxEBM0+6LLe0v8+RSVMOwxeY= -github.com/alecthomas/chroma v0.7.1 h1:G1i02OhUbRi2nJxcNkwJaY/J1gHXj9tt72qN6ZouLFQ= -github.com/alecthomas/chroma v0.7.1/go.mod h1:gHw09mkX1Qp80JlYbmN9L3+4R5o6DJJ3GRShh+AICNc= -github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721 h1:JHZL0hZKJ1VENNfmXvHbgYlbUOvpzYzvy2aZU5gXVeo= -github.com/alecthomas/colour v0.0.0-20160524082231-60882d9e2721/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8qTHF68zU0= -github.com/alecthomas/kong v0.1.17-0.20190424132513-439c674f7ae0/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= -github.com/alecthomas/kong v0.2.1-0.20190708041108-0548c6b1afae/go.mod h1:+inYUSluD+p4L8KdviBSgzcqEjUQOfC5fQDRFuc36lI= -github.com/alecthomas/kong-hcl v0.1.8-0.20190615233001-b21fea9723c8/go.mod h1:MRgZdU3vrFd05IQ89AxUZ0aYdF39BYoNFa324SodPCA= -github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897 h1:p9Sln00KOTlrYkxI1zYWl1QLnEqAqEARBEYa8FQnQcY= -github.com/alecthomas/repr v0.0.0-20180818092828-117648cd9897/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= -github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964 h1:y5HC9v93H5EPKqaS1UYVg1uYah5Xf51mBfIoWehClUQ= -github.com/danwakefield/fnmatch v0.0.0-20160403171240-cbb64ac3d964/go.mod h1:Xd9hchkHSWYkEqJwUGisez3G1QY8Ryz0sdWrLPMGjLk= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dlclark/regexp2 v1.1.6 h1:CqB4MjHw0MFCDj+PHHjiESmHX+N7t0tJzKvC6M97BRg= -github.com/dlclark/regexp2 v1.1.6/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/dlclark/regexp2 v1.2.0 h1:8sAhBGEM0dRWogWqWyQeIJnxjWO6oIjl8FKqREDsGfk= -github.com/dlclark/regexp2 v1.2.0/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 h1:ZgQEtGgCBiWRM39fZuwSd1LwSqqSW0hOdXCYYDX0R3I= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9 h1:uHTyIjqVhYRhLbJ8nIiOJHkEZZ+5YoOsAbD3sk82NiE= -github.com/golang/groupcache v0.0.0-20191027212112-611e8accdfc9/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e h1:1r7pUrabqp18hOBcwBwiTsbnFeTZHV9eER/QT5JVZxY= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.2-0.20191216170541-340f1ebe299e h1:4WfjkTUTsO6siF8ghDQQk6t7x/FPsv3w6MXkc47do7Q= -github.com/google/go-cmp v0.3.2-0.20191216170541-340f1ebe299e/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gorilla/csrf v1.6.0/go.mod h1:7tSf8kmjNYr7IWDCYhd3U8Ck34iQ/Yw5CJu7bAkHEGI= -github.com/gorilla/handlers v1.4.1/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= -github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= -github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.11 h1:FxPOTFNqGkuDUGi3H/qkUbQO4ZiBa2brKq5r0l8TGeM= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= -github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= -github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2 h1:75k/FF0Q2YM8QYo07VPddOLBslDt1MZOdEslOHvmzAs= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3 h1:8sGtKOrtQqkN1bp2AtX+misvLIlOmsEsNd+9NIcPEm8= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5 h1:58fnuSXlxZmFdJyvtTFVmVhcMLU6v5fEb/ok4wyqtNU= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g= -golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200208060501-ecb85df21340 h1:KOcEaR10tFr7gdJV2GCKw8Os5yED1u1aOqHjOAb6d2Y= -golang.org/x/crypto v0.0.0-20200208060501-ecb85df21340/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859 h1:R/3boaszxrf1GEUWTVDzSKVwLmSJpwZ1yqXm8j0v2QI= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181128092732-4ed8d59d0b35/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191210023423-ac6580df4449 h1:gSbV7h1NRL2G1xTg/owz62CST1oJBmxy4QpMMregXVQ= -golang.org/x/sys v0.0.0-20191210023423-ac6580df4449/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5 h1:LfCXLvNmTYH9kEmVgqbnsWfruoXZIrh4YBgqVHtDvw0= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2 h1:EtTFh6h4SAKemS+CURDMTDIANuduG5zKEXShyy18bGA= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7 h1:9zdDQZ7Thm29KFXgAX/+yaf3eVbP7djjWp/dXAppNCc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1 h1:aQktFqmDE2yjveXJlVIfslDFmFnUXSqG0i6KRcJAeMc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1 h1:j6XxA85m/6txkUCHvzlV5f+HBNl/1r5cZ2A/3IEFOO8= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1 h1:wdKvqQk7IttEw92GoRyKG2IDrUIpgpj6H6m81yfeMW0= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/write.go b/write.go index 70656b9f..612e52cb 100644 --- a/write.go +++ b/write.go @@ -335,3 +335,9 @@ func extractBufioWriterBuf(bw *bufio.Writer, w io.Writer) []byte { return writeBuf } + +func (c *Conn) writeError(code StatusCode, err error) { + c.setCloseErr(err) + c.writeClose(code, err.Error()) + c.close(nil) +} diff --git a/ws_js_test.go b/ws_js_test.go index 8d49af6b..bda9c0a5 100644 --- a/ws_js_test.go +++ b/ws_js_test.go @@ -15,7 +15,7 @@ import ( func TestWasm(t *testing.T) { t.Parallel() - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) defer cancel() c, resp, err := websocket.Dial(ctx, os.Getenv("WS_ECHO_SERVER_URL"), &websocket.DialOptions{ From 085e6717e9e4982bfc3f235e2627f7aed7d69d04 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 9 Feb 2020 01:45:02 -0500 Subject: [PATCH 248/519] Get coverage to 85% --- conn_test.go | 117 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/conn_test.go b/conn_test.go index 098fc097..db5ec84d 100644 --- a/conn_test.go +++ b/conn_test.go @@ -5,6 +5,8 @@ package websocket_test import ( "context" "fmt" + "io" + "io/ioutil" "net/http" "net/http/httptest" "os" @@ -200,6 +202,121 @@ func TestConn(t *testing.T) { t.Fatalf("unexpected error: %v", err) } }) + + t.Run("concurrentWriteError", func(t *testing.T) { + t.Parallel() + + c1, c2, err := wstest.Pipe(nil, nil) + if err != nil { + t.Fatal(err) + } + defer c2.Close(websocket.StatusInternalError, "") + defer c1.Close(websocket.StatusInternalError, "") + + _, err = c1.Writer(context.Background(), websocket.MessageText) + if err != nil { + t.Fatal(err) + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100) + defer cancel() + + err = c1.Write(ctx, websocket.MessageText, []byte("x")) + if !xerrors.Is(err, context.DeadlineExceeded) { + t.Fatal(err) + } + }) + + t.Run("netConn", func(t *testing.T) { + t.Parallel() + + c1, c2, err := wstest.Pipe(nil, nil) + if err != nil { + t.Fatal(err) + } + defer c2.Close(websocket.StatusInternalError, "") + defer c1.Close(websocket.StatusInternalError, "") + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + n1 := websocket.NetConn(ctx, c1, websocket.MessageBinary) + n2 := websocket.NetConn(ctx, c2, websocket.MessageBinary) + + // Does not give any confidence but at least ensures no crashes. + d, _ := ctx.Deadline() + n1.SetDeadline(d) + n1.SetDeadline(time.Time{}) + + if n1.RemoteAddr() != n1.LocalAddr() { + t.Fatal() + } + if n1.RemoteAddr().String() != "websocket/unknown-addr" || n1.RemoteAddr().Network() != "websocket" { + t.Fatal(n1.RemoteAddr()) + } + + errs := xsync.Go(func() error { + _, err := n2.Write([]byte("hello")) + if err != nil { + return err + } + return n2.Close() + }) + + b, err := ioutil.ReadAll(n1) + if err != nil { + t.Fatal(err) + } + + _, err = n1.Read(nil) + if err != io.EOF { + t.Fatalf("expected EOF: %v", err) + } + + err = <-errs + if err != nil { + t.Fatal(err) + } + + if !cmp.Equal([]byte("hello"), b) { + t.Fatalf("unexpected msg: %v", cmp.Diff([]byte("hello"), b)) + } + }) + + t.Run("netConn", func(t *testing.T) { + t.Parallel() + + c1, c2, err := wstest.Pipe(nil, nil) + if err != nil { + t.Fatal(err) + } + defer c2.Close(websocket.StatusInternalError, "") + defer c1.Close(websocket.StatusInternalError, "") + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + n1 := websocket.NetConn(ctx, c1, websocket.MessageBinary) + n2 := websocket.NetConn(ctx, c2, websocket.MessageText) + + errs := xsync.Go(func() error { + _, err := n2.Write([]byte("hello")) + if err != nil { + return err + } + return nil + }) + + _, err = ioutil.ReadAll(n1) + if !cmp.ErrorContains(err, `unexpected frame type read (expected MessageBinary): MessageText`) { + t.Fatal(err) + } + + err = <-errs + if err != nil { + t.Fatal(err) + } + }) } func TestWasm(t *testing.T) { From 51769b30952a33d6362a4efbccaf30ba64ea3a3b Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 9 Feb 2020 01:54:31 -0500 Subject: [PATCH 249/519] Add wspb test --- conn_test.go | 105 +++++++++++++++++++++++++++++++++++++++++++++++++- ws_js_test.go | 2 +- 2 files changed, 104 insertions(+), 3 deletions(-) diff --git a/conn_test.go b/conn_test.go index db5ec84d..a0edd8df 100644 --- a/conn_test.go +++ b/conn_test.go @@ -15,6 +15,9 @@ import ( "testing" "time" + "github.com/golang/protobuf/proto" + "github.com/golang/protobuf/ptypes" + "github.com/golang/protobuf/ptypes/duration" "golang.org/x/xerrors" "nhooyr.io/websocket" @@ -22,12 +25,14 @@ import ( "nhooyr.io/websocket/internal/test/wstest" "nhooyr.io/websocket/internal/test/xrand" "nhooyr.io/websocket/internal/xsync" + "nhooyr.io/websocket/wsjson" + "nhooyr.io/websocket/wspb" ) func TestConn(t *testing.T) { t.Parallel() - t.Run("data", func(t *testing.T) { + t.Run("fuzzData", func(t *testing.T) { t.Parallel() for i := 0; i < 5; i++ { @@ -317,6 +322,102 @@ func TestConn(t *testing.T) { t.Fatal(err) } }) + + t.Run("wsjson", func(t *testing.T) { + t.Parallel() + + c1, c2, err := wstest.Pipe(nil, nil) + if err != nil { + t.Fatal(err) + } + defer c2.Close(websocket.StatusInternalError, "") + defer c1.Close(websocket.StatusInternalError, "") + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + echoLoopErr := xsync.Go(func() error { + err := wstest.EchoLoop(ctx, c2) + return assertCloseStatus(websocket.StatusNormalClosure, err) + }) + defer func() { + err := <-echoLoopErr + if err != nil { + t.Errorf("echo loop error: %v", err) + } + }() + defer cancel() + + c1.SetReadLimit(131072) + + exp := xrand.String(xrand.Int(131072)) + err = wsjson.Write(ctx, c1, exp) + if err != nil { + t.Fatal(err) + } + + var act interface{} + err = wsjson.Read(ctx, c1, &act) + if err != nil { + t.Fatal(err) + } + if exp != act { + t.Fatal(cmp.Diff(exp, act)) + } + + err = c1.Close(websocket.StatusNormalClosure, "") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) + + t.Run("wspb", func(t *testing.T) { + t.Parallel() + + c1, c2, err := wstest.Pipe(nil, nil) + if err != nil { + t.Fatal(err) + } + defer c2.Close(websocket.StatusInternalError, "") + defer c1.Close(websocket.StatusInternalError, "") + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + echoLoopErr := xsync.Go(func() error { + err := wstest.EchoLoop(ctx, c2) + return assertCloseStatus(websocket.StatusNormalClosure, err) + }) + defer func() { + err := <-echoLoopErr + if err != nil { + t.Errorf("echo loop error: %v", err) + } + }() + defer cancel() + + c1.SetReadLimit(131072) + + exp := ptypes.DurationProto(100) + err = wspb.Write(ctx, c1, exp) + if err != nil { + t.Fatal(err) + } + + act := &duration.Duration{} + err = wspb.Read(ctx, c1, act) + if err != nil { + t.Fatal(err) + } + if !proto.Equal(exp, act) { + t.Fatal(cmp.Diff(exp, act)) + } + + err = c1.Close(websocket.StatusNormalClosure, "") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + }) } func TestWasm(t *testing.T) { @@ -345,7 +446,7 @@ func TestWasm(t *testing.T) { defer wg.Wait() defer s.Close() - ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() cmd := exec.CommandContext(ctx, "go", "test", "-exec=wasmbrowsertest", "./...") diff --git a/ws_js_test.go b/ws_js_test.go index bda9c0a5..8671dd21 100644 --- a/ws_js_test.go +++ b/ws_js_test.go @@ -15,7 +15,7 @@ import ( func TestWasm(t *testing.T) { t.Parallel() - ctx, cancel := context.WithTimeout(context.Background(), time.Second*20) + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() c, resp, err := websocket.Dial(ctx, os.Getenv("WS_ECHO_SERVER_URL"), &websocket.DialOptions{ From 670be052707b9505a51f0530535e50df06114b11 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 9 Feb 2020 02:03:16 -0500 Subject: [PATCH 250/519] Merge in handshake improvements from master --- accept.go | 25 +++++++++++++++---------- accept_test.go | 2 +- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/accept.go b/accept.go index 31f104b2..cc9babb0 100644 --- a/accept.go +++ b/accept.go @@ -65,9 +65,9 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con opts.CompressionOptions = &CompressionOptions{} } - err = verifyClientRequest(r) + errCode, err := verifyClientRequest(w, r) if err != nil { - http.Error(w, err.Error(), http.StatusBadRequest) + http.Error(w, err.Error(), errCode) return nil, err } @@ -127,32 +127,37 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con }), nil } -func verifyClientRequest(r *http.Request) error { +func verifyClientRequest(w http.ResponseWriter, r *http.Request) (errCode int, _ error) { if !r.ProtoAtLeast(1, 1) { - return xerrors.Errorf("WebSocket protocol violation: handshake request must be at least HTTP/1.1: %q", r.Proto) + return http.StatusUpgradeRequired, xerrors.Errorf("WebSocket protocol violation: handshake request must be at least HTTP/1.1: %q", r.Proto) } if !headerContainsToken(r.Header, "Connection", "Upgrade") { - return xerrors.Errorf("WebSocket protocol violation: Connection header %q does not contain Upgrade", r.Header.Get("Connection")) + w.Header().Set("Connection", "Upgrade") + w.Header().Set("Upgrade", "websocket") + return http.StatusUpgradeRequired, xerrors.Errorf("WebSocket protocol violation: Connection header %q does not contain Upgrade", r.Header.Get("Connection")) } if !headerContainsToken(r.Header, "Upgrade", "websocket") { - return xerrors.Errorf("WebSocket protocol violation: Upgrade header %q does not contain websocket", r.Header.Get("Upgrade")) + w.Header().Set("Connection", "Upgrade") + w.Header().Set("Upgrade", "websocket") + return http.StatusUpgradeRequired, xerrors.Errorf("WebSocket protocol violation: Upgrade header %q does not contain websocket", r.Header.Get("Upgrade")) } if r.Method != "GET" { - return xerrors.Errorf("WebSocket protocol violation: handshake request method is not GET but %q", r.Method) + return http.StatusMethodNotAllowed, xerrors.Errorf("WebSocket protocol violation: handshake request method is not GET but %q", r.Method) } if r.Header.Get("Sec-WebSocket-Version") != "13" { - return xerrors.Errorf("unsupported WebSocket protocol version (only 13 is supported): %q", r.Header.Get("Sec-WebSocket-Version")) + w.Header().Set("Sec-WebSocket-Version", "13") + return http.StatusBadRequest, xerrors.Errorf("unsupported WebSocket protocol version (only 13 is supported): %q", r.Header.Get("Sec-WebSocket-Version")) } if r.Header.Get("Sec-WebSocket-Key") == "" { - return xerrors.New("WebSocket protocol violation: missing Sec-WebSocket-Key") + return http.StatusBadRequest, xerrors.New("WebSocket protocol violation: missing Sec-WebSocket-Key") } - return nil + return 0, nil } func authenticateOrigin(r *http.Request) error { diff --git a/accept_test.go b/accept_test.go index 18302da5..354e95ec 100644 --- a/accept_test.go +++ b/accept_test.go @@ -192,7 +192,7 @@ func Test_verifyClientHandshake(t *testing.T) { r.Header.Set(k, v) } - err := verifyClientRequest(r) + _, err := verifyClientRequest(httptest.NewRecorder(), r) if tc.success != (err == nil) { t.Fatalf("unexpected error value: %v", err) } From 3a526d8c78ea452584284b9b6da08e1679da2867 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 9 Feb 2020 02:31:59 -0500 Subject: [PATCH 251/519] Fix bug in closeHandshake --- close_notjs.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/close_notjs.go b/close_notjs.go index 08a1ea05..160a1237 100644 --- a/close_notjs.go +++ b/close_notjs.go @@ -35,7 +35,7 @@ func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { defer errd.Wrap(&err, "failed to close WebSocket") err = c.writeClose(code, reason) - if CloseStatus(err) == -1 { + if err != nil && CloseStatus(err) == -1 { return err } @@ -63,16 +63,19 @@ func (c *Conn) writeClose(code StatusCode, reason string) error { c.setCloseErr(xerrors.Errorf("sent close frame: %w", ce)) var p []byte + var err error if ce.Code != StatusNoStatusRcvd { - var err error p, err = ce.bytes() if err != nil { log.Printf("websocket: %v", err) - return err } } - return c.writeControl(context.Background(), opClose, p) + werr := c.writeControl(context.Background(), opClose, p) + if err != nil { + return err + } + return werr } func (c *Conn) waitCloseHandshake() error { From 999b812944250843206e83e2da9c8cb02d48f6f8 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 9 Feb 2020 02:35:11 -0500 Subject: [PATCH 252/519] Fix race in msgReader --- read.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/read.go b/read.go index e723ef3c..0c3610d3 100644 --- a/read.go +++ b/read.go @@ -352,6 +352,8 @@ func (mr *msgReader) Read(p []byte) (n int, err error) { } if xerrors.Is(err, io.EOF) { err = io.EOF + + mr.returnFlateReader() } }() @@ -373,11 +375,7 @@ func (mr *msgReader) read(p []byte) (int, error) { if mr.payloadLength == 0 { if mr.fin { if mr.flate { - n, err := mr.flateTail.Read(p) - if xerrors.Is(err, io.EOF) { - mr.returnFlateReader() - } - return n, err + return mr.flateTail.Read(p) } return 0, io.EOF } From 4b84d25251ad9be731c0452f93cad4e48a893d6b Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 9 Feb 2020 02:46:49 -0500 Subject: [PATCH 253/519] Fix a race with c.closed --- conn_notjs.go | 2 +- conn_test.go | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/conn_notjs.go b/conn_notjs.go index 96d17b73..4d8762bf 100644 --- a/conn_notjs.go +++ b/conn_notjs.go @@ -127,9 +127,9 @@ func (c *Conn) close(err error) { if c.isClosed() { return } + c.setCloseErrLocked(err) close(c.closed) runtime.SetFinalizer(c, nil) - c.setCloseErrLocked(err) // Have to close after c.closed is closed to ensure any goroutine that wakes up // from the connection being closed also sees that c.closed is closed and returns diff --git a/conn_test.go b/conn_test.go index a0edd8df..8c00522e 100644 --- a/conn_test.go +++ b/conn_test.go @@ -55,7 +55,7 @@ func TestConn(t *testing.T) { defer c2.Close(websocket.StatusInternalError, "") defer c1.Close(websocket.StatusInternalError, "") - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) defer cancel() echoLoopErr := xsync.Go(func() error { @@ -142,7 +142,7 @@ func TestConn(t *testing.T) { defer c1.Close(websocket.StatusInternalError, "") defer c2.Close(websocket.StatusInternalError, "") - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() c2.CloseRead(ctx) @@ -242,7 +242,7 @@ func TestConn(t *testing.T) { defer c2.Close(websocket.StatusInternalError, "") defer c1.Close(websocket.StatusInternalError, "") - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() n1 := websocket.NetConn(ctx, c1, websocket.MessageBinary) @@ -298,7 +298,7 @@ func TestConn(t *testing.T) { defer c2.Close(websocket.StatusInternalError, "") defer c1.Close(websocket.StatusInternalError, "") - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() n1 := websocket.NetConn(ctx, c1, websocket.MessageBinary) @@ -333,7 +333,7 @@ func TestConn(t *testing.T) { defer c2.Close(websocket.StatusInternalError, "") defer c1.Close(websocket.StatusInternalError, "") - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() echoLoopErr := xsync.Go(func() error { @@ -381,7 +381,7 @@ func TestConn(t *testing.T) { defer c2.Close(websocket.StatusInternalError, "") defer c1.Close(websocket.StatusInternalError, "") - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() echoLoopErr := xsync.Go(func() error { From 85f249d11aff22c45781c237ec783a328e199755 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 9 Feb 2020 02:51:03 -0500 Subject: [PATCH 254/519] Up timeouts --- conn_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/conn_test.go b/conn_test.go index 8c00522e..5662344f 100644 --- a/conn_test.go +++ b/conn_test.go @@ -55,7 +55,7 @@ func TestConn(t *testing.T) { defer c2.Close(websocket.StatusInternalError, "") defer c1.Close(websocket.StatusInternalError, "") - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() echoLoopErr := xsync.Go(func() error { @@ -163,7 +163,7 @@ func TestConn(t *testing.T) { defer c2.Close(websocket.StatusInternalError, "") defer c1.Close(websocket.StatusInternalError, "") - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() discardLoopErr := xsync.Go(func() error { @@ -288,7 +288,7 @@ func TestConn(t *testing.T) { } }) - t.Run("netConn", func(t *testing.T) { + t.Run("netConn/BadMsg", func(t *testing.T) { t.Parallel() c1, c2, err := wstest.Pipe(nil, nil) @@ -333,7 +333,7 @@ func TestConn(t *testing.T) { defer c2.Close(websocket.StatusInternalError, "") defer c1.Close(websocket.StatusInternalError, "") - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() echoLoopErr := xsync.Go(func() error { @@ -381,7 +381,7 @@ func TestConn(t *testing.T) { defer c2.Close(websocket.StatusInternalError, "") defer c1.Close(websocket.StatusInternalError, "") - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) defer cancel() echoLoopErr := xsync.Go(func() error { From 6b38ebbb43156c7dd421e7fd8c3a96cf8c0d5e5f Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 9 Feb 2020 03:11:45 -0500 Subject: [PATCH 255/519] Test fixes --- conn_test.go | 25 +++++++++++++++---------- internal/test/wstest/echo.go | 3 ++- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/conn_test.go b/conn_test.go index 5662344f..6a5e6809 100644 --- a/conn_test.go +++ b/conn_test.go @@ -142,7 +142,7 @@ func TestConn(t *testing.T) { defer c1.Close(websocket.StatusInternalError, "") defer c2.Close(websocket.StatusInternalError, "") - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) defer cancel() c2.CloseRead(ctx) @@ -163,7 +163,7 @@ func TestConn(t *testing.T) { defer c2.Close(websocket.StatusInternalError, "") defer c1.Close(websocket.StatusInternalError, "") - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) defer cancel() discardLoopErr := xsync.Go(func() error { @@ -242,7 +242,7 @@ func TestConn(t *testing.T) { defer c2.Close(websocket.StatusInternalError, "") defer c1.Close(websocket.StatusInternalError, "") - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) defer cancel() n1 := websocket.NetConn(ctx, c1, websocket.MessageBinary) @@ -298,7 +298,7 @@ func TestConn(t *testing.T) { defer c2.Close(websocket.StatusInternalError, "") defer c1.Close(websocket.StatusInternalError, "") - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) defer cancel() n1 := websocket.NetConn(ctx, c1, websocket.MessageBinary) @@ -333,7 +333,7 @@ func TestConn(t *testing.T) { defer c2.Close(websocket.StatusInternalError, "") defer c1.Close(websocket.StatusInternalError, "") - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) defer cancel() echoLoopErr := xsync.Go(func() error { @@ -351,10 +351,10 @@ func TestConn(t *testing.T) { c1.SetReadLimit(131072) exp := xrand.String(xrand.Int(131072)) - err = wsjson.Write(ctx, c1, exp) - if err != nil { - t.Fatal(err) - } + + werr := xsync.Go(func() error { + return wsjson.Write(ctx, c1, exp) + }) var act interface{} err = wsjson.Read(ctx, c1, &act) @@ -365,6 +365,11 @@ func TestConn(t *testing.T) { t.Fatal(cmp.Diff(exp, act)) } + err = <-werr + if err != nil { + t.Fatal(err) + } + err = c1.Close(websocket.StatusNormalClosure, "") if err != nil { t.Fatalf("unexpected error: %v", err) @@ -381,7 +386,7 @@ func TestConn(t *testing.T) { defer c2.Close(websocket.StatusInternalError, "") defer c1.Close(websocket.StatusInternalError, "") - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) defer cancel() echoLoopErr := xsync.Go(func() error { diff --git a/internal/test/wstest/echo.go b/internal/test/wstest/echo.go index 70b2ba57..714767fc 100644 --- a/internal/test/wstest/echo.go +++ b/internal/test/wstest/echo.go @@ -1,6 +1,7 @@ package wstest import ( + "bytes" "context" "io" "time" @@ -75,7 +76,7 @@ func Echo(ctx context.Context, c *websocket.Conn, max int) error { return xerrors.Errorf("unexpected message typ (%v): %v", expType, actType) } - if !cmp.Equal(msg, act) { + if !bytes.Equal(msg, act) { return xerrors.Errorf("unexpected msg read: %v", cmp.Diff(msg, act)) } From 6770421bab627ff98b2ddb30a85e3a99a5cef9f3 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 11 Feb 2020 23:13:23 -0500 Subject: [PATCH 256/519] Fix goroutine leak from deadlock when closing --- conn_test.go | 4 +--- read.go | 6 ++++-- write.go | 22 ++++++++++++++++++---- 3 files changed, 23 insertions(+), 9 deletions(-) diff --git a/conn_test.go b/conn_test.go index 6a5e6809..5abc9f46 100644 --- a/conn_test.go +++ b/conn_test.go @@ -348,7 +348,7 @@ func TestConn(t *testing.T) { }() defer cancel() - c1.SetReadLimit(131072) + c1.SetReadLimit(1 << 30) exp := xrand.String(xrand.Int(131072)) @@ -401,8 +401,6 @@ func TestConn(t *testing.T) { }() defer cancel() - c1.SetReadLimit(131072) - exp := ptypes.DurationProto(100) err = wspb.Write(ctx, c1, exp) if err != nil { diff --git a/read.go b/read.go index 0c3610d3..cb1fa229 100644 --- a/read.go +++ b/read.go @@ -69,7 +69,9 @@ func (c *Conn) CloseRead(ctx context.Context) context.Context { // // When the limit is hit, the connection will be closed with StatusMessageTooBig. func (c *Conn) SetReadLimit(n int64) { - c.msgReader.limitReader.limit.Store(n) + // We add read one more byte than the limit in case + // there is a fin frame that needs to be read. + c.msgReader.limitReader.limit.Store(n + 1) } const defaultReadLimit = 32768 @@ -80,7 +82,7 @@ func newMsgReader(c *Conn) *msgReader { fin: true, } - mr.limitReader = newLimitReader(c, readerFunc(mr.read), defaultReadLimit) + mr.limitReader = newLimitReader(c, readerFunc(mr.read), defaultReadLimit+1) return mr } diff --git a/write.go b/write.go index 612e52cb..245827a2 100644 --- a/write.go +++ b/write.go @@ -50,7 +50,8 @@ func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { type msgWriter struct { c *Conn - mu *mu + mu *mu + activeMu *mu ctx context.Context opcode opcode @@ -63,8 +64,9 @@ type msgWriter struct { func newMsgWriter(c *Conn) *msgWriter { mw := &msgWriter{ - c: c, - mu: newMu(c), + c: c, + mu: newMu(c), + activeMu: newMu(c), } return mw } @@ -147,6 +149,12 @@ func (mw *msgWriter) returnFlateWriter() { func (mw *msgWriter) Write(p []byte) (_ int, err error) { defer errd.Wrap(&err, "failed to write") + err = mw.activeMu.Lock(mw.ctx) + if err != nil { + return 0, err + } + defer mw.activeMu.Unlock() + if mw.closed { return 0, xerrors.New("cannot use closed writer") } @@ -173,6 +181,12 @@ func (mw *msgWriter) write(p []byte) (int, error) { func (mw *msgWriter) Close() (err error) { defer errd.Wrap(&err, "failed to close writer") + err = mw.activeMu.Lock(mw.ctx) + if err != nil { + return err + } + defer mw.activeMu.Unlock() + if mw.closed { return xerrors.New("cannot use closed writer") } @@ -201,7 +215,7 @@ func (mw *msgWriter) Close() (err error) { } func (mw *msgWriter) close() { - mw.mu.Lock(context.Background()) + mw.activeMu.Lock(context.Background()) mw.returnFlateWriter() } From c7523658b7ecdfccd4408f90cd87b981a72c5dc4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 11 Feb 2020 23:36:32 -0500 Subject: [PATCH 257/519] Make flateThreshold work Was noop before. --- write.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/write.go b/write.go index 245827a2..28f139af 100644 --- a/write.go +++ b/write.go @@ -106,7 +106,7 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error return 0, err } - if !c.flate() || len(p) < c.flateThreshold { + if !c.flate() { defer c.msgWriter.mu.Unlock() return c.writeFrame(ctx, true, false, c.msgWriter.opcode, p) } @@ -159,9 +159,16 @@ func (mw *msgWriter) Write(p []byte) (_ int, err error) { return 0, xerrors.New("cannot use closed writer") } - // TODO Write to buffer to detect whether to enable flate or not for this message. - if mw.c.flate() { - mw.ensureFlate() + if mw.opcode != opContinuation { + // First frame needs to be written. + if len(p) >= mw.c.flateThreshold { + // Only enables flate if the length crosses the + // threshold on the first write. + mw.ensureFlate() + } + } + + if mw.flate { return mw.flateWriter.Write(p) } From 0ea94666828da2849ce04a4cf7b5d5d8c9398fc2 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 11 Feb 2020 23:49:39 -0500 Subject: [PATCH 258/519] Cleanup writeMu and flateThreshold --- read.go | 2 +- write.go | 35 ++++++++++++++--------------------- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/read.go b/read.go index cb1fa229..42d46b85 100644 --- a/read.go +++ b/read.go @@ -120,7 +120,7 @@ func (mr *msgReader) flateContextTakeover() bool { } func (c *Conn) readRSV1Illegal(h header) bool { - // If compression is enabled, rsv1 is always illegal. + // If compression is disabled, rsv1 is always illegal. if !c.flate() { return true } diff --git a/write.go b/write.go index 28f139af..9d4b670f 100644 --- a/write.go +++ b/write.go @@ -9,6 +9,7 @@ import ( "crypto/rand" "encoding/binary" "io" + "sync" "time" "golang.org/x/xerrors" @@ -50,8 +51,8 @@ func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { type msgWriter struct { c *Conn - mu *mu - activeMu *mu + mu *mu + writeMu sync.Mutex ctx context.Context opcode opcode @@ -64,9 +65,8 @@ type msgWriter struct { func newMsgWriter(c *Conn) *msgWriter { mw := &msgWriter{ - c: c, - mu: newMu(c), - activeMu: newMu(c), + c: c, + mu: newMu(c), } return mw } @@ -149,21 +149,17 @@ func (mw *msgWriter) returnFlateWriter() { func (mw *msgWriter) Write(p []byte) (_ int, err error) { defer errd.Wrap(&err, "failed to write") - err = mw.activeMu.Lock(mw.ctx) - if err != nil { - return 0, err - } - defer mw.activeMu.Unlock() + mw.writeMu.Lock() + defer mw.writeMu.Unlock() if mw.closed { return 0, xerrors.New("cannot use closed writer") } - if mw.opcode != opContinuation { - // First frame needs to be written. - if len(p) >= mw.c.flateThreshold { - // Only enables flate if the length crosses the - // threshold on the first write. + if mw.c.flate() { + // Only enables flate if the length crosses the + // threshold on the first frame + if mw.opcode != opContinuation && len(p) >= mw.c.flateThreshold { mw.ensureFlate() } } @@ -188,11 +184,8 @@ func (mw *msgWriter) write(p []byte) (int, error) { func (mw *msgWriter) Close() (err error) { defer errd.Wrap(&err, "failed to close writer") - err = mw.activeMu.Lock(mw.ctx) - if err != nil { - return err - } - defer mw.activeMu.Unlock() + mw.writeMu.Lock() + defer mw.writeMu.Unlock() if mw.closed { return xerrors.New("cannot use closed writer") @@ -222,7 +215,7 @@ func (mw *msgWriter) Close() (err error) { } func (mw *msgWriter) close() { - mw.activeMu.Lock(context.Background()) + mw.writeMu.Lock() mw.returnFlateWriter() } From b33d48cb5b86653743c1605512f3049fb8c41958 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 12 Feb 2020 00:15:02 -0500 Subject: [PATCH 259/519] Minor cleanup --- ci/test.mk | 2 +- conn.go | 2 -- read.go | 2 +- ws_js.go | 12 ------------ 4 files changed, 2 insertions(+), 16 deletions(-) diff --git a/ci/test.mk b/ci/test.mk index 786a8d77..3fc34bbf 100644 --- a/ci/test.mk +++ b/ci/test.mk @@ -12,6 +12,6 @@ coveralls: gotest goveralls -coverprofile=ci/out/coverage.prof gotest: - go test -covermode=count -coverprofile=ci/out/coverage.prof -coverpkg=./... $${GOTESTFLAGS-} ./... + go test -timeout=30m -covermode=count -coverprofile=ci/out/coverage.prof -coverpkg=./... $${GOTESTFLAGS-} ./... sed -i '/stringer\.go/d' ci/out/coverage.prof sed -i '/nhooyr.io\/websocket\/internal\/test/d' ci/out/coverage.prof diff --git a/conn.go b/conn.go index e58a8748..a41808be 100644 --- a/conn.go +++ b/conn.go @@ -1,5 +1,3 @@ -// +build !js - package websocket // MessageType represents the type of a WebSocket message. diff --git a/read.go b/read.go index 42d46b85..a9c291d1 100644 --- a/read.go +++ b/read.go @@ -120,7 +120,7 @@ func (mr *msgReader) flateContextTakeover() bool { } func (c *Conn) readRSV1Illegal(h header) bool { - // If compression is disabled, rsv1 is always illegal. + // If compression is disabled, rsv1 is illegal. if !c.flate() { return true } diff --git a/ws_js.go b/ws_js.go index de76afa6..05c4c062 100644 --- a/ws_js.go +++ b/ws_js.go @@ -17,18 +17,6 @@ import ( "nhooyr.io/websocket/internal/xsync" ) -// MessageType represents the type of a WebSocket message. -// See https://tools.ietf.org/html/rfc6455#section-5.6 -type MessageType int - -// MessageType constants. -const ( - // MessageText is for UTF-8 encoded text messages like JSON. - MessageText MessageType = iota + 1 - // MessageBinary is for binary messages like protobufs. - MessageBinary -) - // Conn provides a wrapper around the browser WebSocket API. type Conn struct { ws wsjs.WebSocket From 9c5bfabce2d2b82e1b998842012502afabc32511 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 12 Feb 2020 20:29:43 -0500 Subject: [PATCH 260/519] Simplifications of conn_test.go --- conn_test.go | 443 +++++++++++++++++---------------------- internal/test/cmp/cmp.go | 3 +- 2 files changed, 194 insertions(+), 252 deletions(-) diff --git a/conn_test.go b/conn_test.go index 5abc9f46..b2a35af8 100644 --- a/conn_test.go +++ b/conn_test.go @@ -15,7 +15,6 @@ import ( "testing" "time" - "github.com/golang/protobuf/proto" "github.com/golang/protobuf/ptypes" "github.com/golang/protobuf/ptypes/duration" "golang.org/x/xerrors" @@ -37,153 +36,86 @@ func TestConn(t *testing.T) { for i := 0; i < 5; i++ { t.Run("", func(t *testing.T) { - t.Parallel() + tt := newTest(t) + defer tt.done() - copts := &websocket.CompressionOptions{ + dialCopts := &websocket.CompressionOptions{ Mode: websocket.CompressionMode(xrand.Int(int(websocket.CompressionDisabled) + 1)), Threshold: xrand.Int(9999), } - c1, c2, err := wstest.Pipe(&websocket.DialOptions{ - CompressionOptions: copts, - }, &websocket.AcceptOptions{ - CompressionOptions: copts, - }) - if err != nil { - t.Fatal(err) + acceptCopts := &websocket.CompressionOptions{ + Mode: websocket.CompressionMode(xrand.Int(int(websocket.CompressionDisabled) + 1)), + Threshold: xrand.Int(9999), } - defer c2.Close(websocket.StatusInternalError, "") - defer c1.Close(websocket.StatusInternalError, "") - - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) - defer cancel() - echoLoopErr := xsync.Go(func() error { - err := wstest.EchoLoop(ctx, c2) - return assertCloseStatus(websocket.StatusNormalClosure, err) + c1, c2 := tt.pipe(&websocket.DialOptions{ + CompressionOptions: dialCopts, + }, &websocket.AcceptOptions{ + CompressionOptions: acceptCopts, }) - defer func() { - err := <-echoLoopErr - if err != nil { - t.Errorf("echo loop error: %v", err) - } - }() - defer cancel() + + tt.goEchoLoop(c2) c1.SetReadLimit(131072) for i := 0; i < 5; i++ { - err := wstest.Echo(ctx, c1, 131072) - if err != nil { - t.Fatal(err) - } + err := wstest.Echo(tt.ctx, c1, 131072) + tt.success(err) } - err = c1.Close(websocket.StatusNormalClosure, "") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } + err := c1.Close(websocket.StatusNormalClosure, "") + tt.success(err) }) } }) t.Run("badClose", func(t *testing.T) { - t.Parallel() + tt := newTest(t) + defer tt.done() - c1, c2, err := wstest.Pipe(nil, nil) - if err != nil { - t.Fatal(err) - } - defer c1.Close(websocket.StatusInternalError, "") - defer c2.Close(websocket.StatusInternalError, "") + c1, _ := tt.pipe(nil, nil) - err = c1.Close(-1, "") - if !cmp.ErrorContains(err, "failed to marshal close frame: status code StatusCode(-1) cannot be set") { - t.Fatalf("unexpected error: %v", err) - } + err := c1.Close(-1, "") + tt.errContains(err, "failed to marshal close frame: status code StatusCode(-1) cannot be set") }) t.Run("ping", func(t *testing.T) { - t.Parallel() + tt := newTest(t) + defer tt.done() - c1, c2, err := wstest.Pipe(nil, nil) - if err != nil { - t.Fatal(err) - } - defer c1.Close(websocket.StatusInternalError, "") - defer c2.Close(websocket.StatusInternalError, "") + c1, c2 := tt.pipe(nil, nil) - ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) - defer cancel() - - c2.CloseRead(ctx) - c1.CloseRead(ctx) + c1.CloseRead(tt.ctx) + c2.CloseRead(tt.ctx) for i := 0; i < 10; i++ { - err = c1.Ping(ctx) - if err != nil { - t.Fatal(err) - } + err := c1.Ping(tt.ctx) + tt.success(err) } - err = c1.Close(websocket.StatusNormalClosure, "") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } + err := c1.Close(websocket.StatusNormalClosure, "") + tt.success(err) }) t.Run("badPing", func(t *testing.T) { - t.Parallel() - - c1, c2, err := wstest.Pipe(nil, nil) - if err != nil { - t.Fatal(err) - } - defer c1.Close(websocket.StatusInternalError, "") - defer c2.Close(websocket.StatusInternalError, "") + tt := newTest(t) + defer tt.done() - ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) - defer cancel() + c1, c2 := tt.pipe(nil, nil) - c2.CloseRead(ctx) + c2.CloseRead(tt.ctx) - err = c1.Ping(ctx) - if !cmp.ErrorContains(err, "failed to wait for pong") { - t.Fatalf("unexpected error: %v", err) - } + err := c1.Ping(tt.ctx) + tt.errContains(err, "failed to wait for pong") }) t.Run("concurrentWrite", func(t *testing.T) { - t.Parallel() + tt := newTest(t) + defer tt.done() - c1, c2, err := wstest.Pipe(nil, nil) - if err != nil { - t.Fatal(err) - } - defer c2.Close(websocket.StatusInternalError, "") - defer c1.Close(websocket.StatusInternalError, "") - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) - defer cancel() - - discardLoopErr := xsync.Go(func() error { - for { - _, _, err := c2.Read(ctx) - if websocket.CloseStatus(err) == websocket.StatusNormalClosure { - return nil - } - if err != nil { - return err - } - } - }) - defer func() { - err := <-discardLoopErr - if err != nil { - t.Errorf("discard loop error: %v", err) - } - }() - defer cancel() + c1, c2 := tt.pipe(nil, nil) + tt.goDiscardLoop(c2) msg := xrand.Bytes(xrand.Int(9999)) const count = 100 @@ -191,74 +123,52 @@ func TestConn(t *testing.T) { for i := 0; i < count; i++ { go func() { - errs <- c1.Write(ctx, websocket.MessageBinary, msg) + errs <- c1.Write(tt.ctx, websocket.MessageBinary, msg) }() } for i := 0; i < count; i++ { err := <-errs - if err != nil { - t.Fatal(err) - } + tt.success(err) } - err = c1.Close(websocket.StatusNormalClosure, "") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } + err := c1.Close(websocket.StatusNormalClosure, "") + tt.success(err) }) t.Run("concurrentWriteError", func(t *testing.T) { - t.Parallel() + tt := newTest(t) + defer tt.done() - c1, c2, err := wstest.Pipe(nil, nil) - if err != nil { - t.Fatal(err) - } - defer c2.Close(websocket.StatusInternalError, "") - defer c1.Close(websocket.StatusInternalError, "") + c1, _ := tt.pipe(nil, nil) - _, err = c1.Writer(context.Background(), websocket.MessageText) - if err != nil { - t.Fatal(err) - } + _, err := c1.Writer(tt.ctx, websocket.MessageText) + tt.success(err) ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100) defer cancel() err = c1.Write(ctx, websocket.MessageText, []byte("x")) - if !xerrors.Is(err, context.DeadlineExceeded) { - t.Fatal(err) - } + tt.eq(context.DeadlineExceeded, err) }) t.Run("netConn", func(t *testing.T) { - t.Parallel() + tt := newTest(t) + defer tt.done() - c1, c2, err := wstest.Pipe(nil, nil) - if err != nil { - t.Fatal(err) - } - defer c2.Close(websocket.StatusInternalError, "") - defer c1.Close(websocket.StatusInternalError, "") - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) - defer cancel() + c1, c2 := tt.pipe(nil, nil) - n1 := websocket.NetConn(ctx, c1, websocket.MessageBinary) - n2 := websocket.NetConn(ctx, c2, websocket.MessageBinary) + n1 := websocket.NetConn(tt.ctx, c1, websocket.MessageBinary) + n2 := websocket.NetConn(tt.ctx, c2, websocket.MessageBinary) // Does not give any confidence but at least ensures no crashes. - d, _ := ctx.Deadline() + d, _ := tt.ctx.Deadline() n1.SetDeadline(d) n1.SetDeadline(time.Time{}) - if n1.RemoteAddr() != n1.LocalAddr() { - t.Fatal() - } - if n1.RemoteAddr().String() != "websocket/unknown-addr" || n1.RemoteAddr().Network() != "websocket" { - t.Fatal(n1.RemoteAddr()) - } + tt.eq(n1.RemoteAddr(), n1.LocalAddr()) + tt.eq("websocket/unknown-addr", n1.RemoteAddr().String()) + tt.eq("websocket", n1.RemoteAddr().Network()) errs := xsync.Go(func() error { _, err := n2.Write([]byte("hello")) @@ -269,40 +179,25 @@ func TestConn(t *testing.T) { }) b, err := ioutil.ReadAll(n1) - if err != nil { - t.Fatal(err) - } + tt.success(err) _, err = n1.Read(nil) - if err != io.EOF { - t.Fatalf("expected EOF: %v", err) - } + tt.eq(err, io.EOF) err = <-errs - if err != nil { - t.Fatal(err) - } + tt.success(err) - if !cmp.Equal([]byte("hello"), b) { - t.Fatalf("unexpected msg: %v", cmp.Diff([]byte("hello"), b)) - } + tt.eq([]byte("hello"), b) }) t.Run("netConn/BadMsg", func(t *testing.T) { - t.Parallel() + tt := newTest(t) + defer tt.done() - c1, c2, err := wstest.Pipe(nil, nil) - if err != nil { - t.Fatal(err) - } - defer c2.Close(websocket.StatusInternalError, "") - defer c1.Close(websocket.StatusInternalError, "") - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) - defer cancel() + c1, c2 := tt.pipe(nil, nil) - n1 := websocket.NetConn(ctx, c1, websocket.MessageBinary) - n2 := websocket.NetConn(ctx, c2, websocket.MessageText) + n1 := websocket.NetConn(tt.ctx, c1, websocket.MessageBinary) + n2 := websocket.NetConn(tt.ctx, c2, websocket.MessageText) errs := xsync.Go(func() error { _, err := n2.Write([]byte("hello")) @@ -312,114 +207,60 @@ func TestConn(t *testing.T) { return nil }) - _, err = ioutil.ReadAll(n1) - if !cmp.ErrorContains(err, `unexpected frame type read (expected MessageBinary): MessageText`) { - t.Fatal(err) - } + _, err := ioutil.ReadAll(n1) + tt.errContains(err, `unexpected frame type read (expected MessageBinary): MessageText`) err = <-errs - if err != nil { - t.Fatal(err) - } + tt.success(err) }) t.Run("wsjson", func(t *testing.T) { - t.Parallel() - - c1, c2, err := wstest.Pipe(nil, nil) - if err != nil { - t.Fatal(err) - } - defer c2.Close(websocket.StatusInternalError, "") - defer c1.Close(websocket.StatusInternalError, "") + tt := newTest(t) + defer tt.done() - ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) - defer cancel() + c1, c2 := tt.pipe(nil, nil) - echoLoopErr := xsync.Go(func() error { - err := wstest.EchoLoop(ctx, c2) - return assertCloseStatus(websocket.StatusNormalClosure, err) - }) - defer func() { - err := <-echoLoopErr - if err != nil { - t.Errorf("echo loop error: %v", err) - } - }() - defer cancel() + tt.goEchoLoop(c2) c1.SetReadLimit(1 << 30) exp := xrand.String(xrand.Int(131072)) werr := xsync.Go(func() error { - return wsjson.Write(ctx, c1, exp) + return wsjson.Write(tt.ctx, c1, exp) }) var act interface{} - err = wsjson.Read(ctx, c1, &act) - if err != nil { - t.Fatal(err) - } - if exp != act { - t.Fatal(cmp.Diff(exp, act)) - } + err := wsjson.Read(tt.ctx, c1, &act) + tt.success(err) + tt.eq(exp, act) err = <-werr - if err != nil { - t.Fatal(err) - } + tt.success(err) err = c1.Close(websocket.StatusNormalClosure, "") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } + tt.success(err) }) t.Run("wspb", func(t *testing.T) { - t.Parallel() + tt := newTest(t) + defer tt.done() - c1, c2, err := wstest.Pipe(nil, nil) - if err != nil { - t.Fatal(err) - } - defer c2.Close(websocket.StatusInternalError, "") - defer c1.Close(websocket.StatusInternalError, "") - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*15) - defer cancel() + c1, c2 := tt.pipe(nil, nil) - echoLoopErr := xsync.Go(func() error { - err := wstest.EchoLoop(ctx, c2) - return assertCloseStatus(websocket.StatusNormalClosure, err) - }) - defer func() { - err := <-echoLoopErr - if err != nil { - t.Errorf("echo loop error: %v", err) - } - }() - defer cancel() + tt.goEchoLoop(c2) exp := ptypes.DurationProto(100) - err = wspb.Write(ctx, c1, exp) - if err != nil { - t.Fatal(err) - } + err := wspb.Write(tt.ctx, c1, exp) + tt.success(err) act := &duration.Duration{} - err = wspb.Read(ctx, c1, act) - if err != nil { - t.Fatal(err) - } - if !proto.Equal(exp, act) { - t.Fatal(cmp.Diff(exp, act)) - } + err = wspb.Read(tt.ctx, c1, act) + tt.success(err) + tt.eq(exp, act) err = c1.Close(websocket.StatusNormalClosure, "") - if err != nil { - t.Fatalf("unexpected error: %v", err) - } + tt.success(err) }) } @@ -443,7 +284,7 @@ func TestWasm(t *testing.T) { err = wstest.EchoLoop(r.Context(), c) if websocket.CloseStatus(err) != websocket.StatusNormalClosure { - t.Errorf("echoLoop: %v", err) + t.Errorf("echoLoop failed: %v", err) } })) defer wg.Wait() @@ -470,3 +311,103 @@ func assertCloseStatus(exp websocket.StatusCode, err error) error { } return nil } + +type test struct { + t *testing.T + ctx context.Context + + doneFuncs []func() +} + +func newTest(t *testing.T) *test { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + tt := &test{t: t, ctx: ctx} + tt.appendDone(cancel) + return tt +} + +func (tt *test) appendDone(f func()) { + tt.doneFuncs = append(tt.doneFuncs, f) +} + +func (tt *test) done() { + for i := len(tt.doneFuncs) - 1; i >= 0; i-- { + tt.doneFuncs[i]() + } +} + +func (tt *test) goEchoLoop(c *websocket.Conn) { + ctx, cancel := context.WithCancel(tt.ctx) + + echoLoopErr := xsync.Go(func() error { + err := wstest.EchoLoop(ctx, c) + return assertCloseStatus(websocket.StatusNormalClosure, err) + }) + tt.appendDone(func() { + cancel() + err := <-echoLoopErr + if err != nil { + tt.t.Errorf("echo loop error: %v", err) + } + }) +} + +func (tt *test) goDiscardLoop(c *websocket.Conn) { + ctx, cancel := context.WithCancel(tt.ctx) + + discardLoopErr := xsync.Go(func() error { + for { + _, _, err := c.Read(ctx) + if websocket.CloseStatus(err) == websocket.StatusNormalClosure { + return nil + } + if err != nil { + return err + } + } + }) + tt.appendDone(func() { + cancel() + err := <-discardLoopErr + if err != nil { + tt.t.Errorf("discard loop error: %v", err) + } + }) +} + +func (tt *test) pipe(dialOpts *websocket.DialOptions, acceptOpts *websocket.AcceptOptions) (c1, c2 *websocket.Conn) { + tt.t.Helper() + + c1, c2, err := wstest.Pipe(dialOpts, acceptOpts) + if err != nil { + tt.t.Fatal(err) + } + tt.appendDone(func() { + c2.Close(websocket.StatusInternalError, "") + c1.Close(websocket.StatusInternalError, "") + }) + return c1, c2 +} + +func (tt *test) success(err error) { + tt.t.Helper() + if err != nil { + tt.t.Fatal(err) + } +} + +func (tt *test) errContains(err error, sub string) { + tt.t.Helper() + if !cmp.ErrorContains(err, sub) { + tt.t.Fatalf("error does not contain %q: %v", sub, err) + } +} + +func (tt *test) eq(exp, act interface{}) { + tt.t.Helper() + if !cmp.Equal(exp, act) { + tt.t.Fatalf(cmp.Diff(exp, act)) + } +} diff --git a/internal/test/cmp/cmp.go b/internal/test/cmp/cmp.go index cdbadf70..6f3dd706 100644 --- a/internal/test/cmp/cmp.go +++ b/internal/test/cmp/cmp.go @@ -4,6 +4,7 @@ import ( "reflect" "strings" + "github.com/golang/protobuf/proto" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" ) @@ -12,7 +13,7 @@ import ( func Equal(v1, v2 interface{}) bool { return cmp.Equal(v1, v2, cmpopts.EquateErrors(), cmp.Exporter(func(r reflect.Type) bool { return true - })) + }), cmp.Comparer(proto.Equal)) } // Diff returns a human readable diff between v1 and v2 From 3673c2cf26752428863df479227e1a00b8948ea6 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 13 Feb 2020 01:05:09 -0500 Subject: [PATCH 261/519] Use basic test assertions --- accept_js.go | 19 ++++ accept_test.go | 54 ++++------ autobahn_test.go | 31 ++---- close_test.go | 33 +++--- compress_test.go | 6 +- conn_test.go | 189 +++++++++++++-------------------- dial_test.go | 24 ++--- frame_test.go | 27 ++--- internal/test/assert/assert.go | 46 ++++++++ internal/test/cmp/cmp.go | 18 +--- internal/xsync/go_test.go | 6 +- ws_js_test.go | 22 ++-- 12 files changed, 206 insertions(+), 269 deletions(-) create mode 100644 accept_js.go create mode 100644 internal/test/assert/assert.go diff --git a/accept_js.go b/accept_js.go new file mode 100644 index 00000000..efc92817 --- /dev/null +++ b/accept_js.go @@ -0,0 +1,19 @@ +package websocket + +import ( + "net/http" + + "golang.org/x/xerrors" +) + +// AcceptOptions represents Accept's options. +type AcceptOptions struct { + Subprotocols []string + InsecureSkipVerify bool + CompressionOptions *CompressionOptions +} + +// Accept is stubbed out for Wasm. +func Accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, error) { + return nil, xerrors.New("unimplemented") +} diff --git a/accept_test.go b/accept_test.go index 354e95ec..53338e17 100644 --- a/accept_test.go +++ b/accept_test.go @@ -12,7 +12,7 @@ import ( "golang.org/x/xerrors" - "nhooyr.io/websocket/internal/test/cmp" + "nhooyr.io/websocket/internal/test/assert" ) func TestAccept(t *testing.T) { @@ -25,9 +25,7 @@ func TestAccept(t *testing.T) { r := httptest.NewRequest("GET", "/", nil) _, err := Accept(w, r, nil) - if !cmp.ErrorContains(err, "protocol violation") { - t.Fatal(err) - } + assert.Contains(t, err, "protocol violation") }) t.Run("badOrigin", func(t *testing.T) { @@ -42,9 +40,7 @@ func TestAccept(t *testing.T) { r.Header.Set("Origin", "harhar.com") _, err := Accept(w, r, nil) - if !cmp.ErrorContains(err, `request Origin "harhar.com" is not authorized for Host`) { - t.Fatal(err) - } + assert.Contains(t, err, `request Origin "harhar.com" is not authorized for Host`) }) t.Run("badCompression", func(t *testing.T) { @@ -61,9 +57,7 @@ func TestAccept(t *testing.T) { r.Header.Set("Sec-WebSocket-Extensions", "permessage-deflate; harharhar") _, err := Accept(w, r, nil) - if !cmp.ErrorContains(err, `unsupported permessage-deflate parameter`) { - t.Fatal(err) - } + assert.Contains(t, err, `unsupported permessage-deflate parameter`) }) t.Run("requireHttpHijacker", func(t *testing.T) { @@ -77,9 +71,7 @@ func TestAccept(t *testing.T) { r.Header.Set("Sec-WebSocket-Key", "meow123") _, err := Accept(w, r, nil) - if !cmp.ErrorContains(err, `http.ResponseWriter does not implement http.Hijacker`) { - t.Fatal(err) - } + assert.Contains(t, err, `http.ResponseWriter does not implement http.Hijacker`) }) t.Run("badHijack", func(t *testing.T) { @@ -99,9 +91,7 @@ func TestAccept(t *testing.T) { r.Header.Set("Sec-WebSocket-Key", "meow123") _, err := Accept(w, r, nil) - if !cmp.ErrorContains(err, `failed to hijack connection`) { - t.Fatal(err) - } + assert.Contains(t, err, `failed to hijack connection`) }) } @@ -193,8 +183,10 @@ func Test_verifyClientHandshake(t *testing.T) { } _, err := verifyClientRequest(httptest.NewRecorder(), r) - if tc.success != (err == nil) { - t.Fatalf("unexpected error value: %v", err) + if tc.success { + assert.Success(t, err) + } else { + assert.Error(t, err) } }) } @@ -244,9 +236,7 @@ func Test_selectSubprotocol(t *testing.T) { r.Header.Set("Sec-WebSocket-Protocol", strings.Join(tc.clientProtocols, ",")) negotiated := selectSubprotocol(r, tc.serverProtocols) - if !cmp.Equal(tc.negotiated, negotiated) { - t.Fatalf("unexpected negotiated: %v", cmp.Diff(tc.negotiated, negotiated)) - } + assert.Equal(t, "negotiated", tc.negotiated, negotiated) }) } } @@ -300,8 +290,10 @@ func Test_authenticateOrigin(t *testing.T) { r.Header.Set("Origin", tc.origin) err := authenticateOrigin(r) - if tc.success != (err == nil) { - t.Fatalf("unexpected error value: %v", err) + if tc.success { + assert.Success(t, err) + } else { + assert.Error(t, err) } }) } @@ -373,21 +365,13 @@ func Test_acceptCompression(t *testing.T) { w := httptest.NewRecorder() copts, err := acceptCompression(r, w, tc.mode) if tc.error { - if err == nil { - t.Fatalf("expected error: %v", copts) - } + assert.Error(t, err) return } - if err != nil { - t.Fatal(err) - } - if !cmp.Equal(tc.expCopts, copts) { - t.Fatalf("unexpected compression options: %v", cmp.Diff(tc.expCopts, copts)) - } - if !cmp.Equal(tc.respSecWebSocketExtensions, w.Header().Get("Sec-WebSocket-Extensions")) { - t.Fatalf("unexpected respHeader: %v", cmp.Diff(tc.respSecWebSocketExtensions, w.Header().Get("Sec-WebSocket-Extensions"))) - } + assert.Success(t, err) + assert.Equal(t, "compression options", tc.expCopts, copts) + assert.Equal(t, "Sec-WebSocket-Extensions", tc.respSecWebSocketExtensions, w.Header().Get("Sec-WebSocket-Extensions")) }) } } diff --git a/autobahn_test.go b/autobahn_test.go index 4d0bd1b5..0763bc97 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -19,6 +19,7 @@ import ( "nhooyr.io/websocket" "nhooyr.io/websocket/internal/errd" + "nhooyr.io/websocket/internal/test/assert" "nhooyr.io/websocket/internal/test/wstest" ) @@ -45,32 +46,26 @@ func TestAutobahn(t *testing.T) { defer cancel() wstestURL, closeFn, err := wstestClientServer(ctx) - if err != nil { - t.Fatal(err) - } + assert.Success(t, err) defer closeFn() err = waitWS(ctx, wstestURL) - if err != nil { - t.Fatal(err) - } + assert.Success(t, err) cases, err := wstestCaseCount(ctx, wstestURL) - if err != nil { - t.Fatal(err) - } + assert.Success(t, err) t.Run("cases", func(t *testing.T) { for i := 1; i <= cases; i++ { i := i t.Run("", func(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) defer cancel() c, _, err := websocket.Dial(ctx, fmt.Sprintf(wstestURL+"/runCase?case=%v&agent=main", i), nil) - if err != nil { - t.Fatal(err) - } + assert.Success(t, err) err = wstest.EchoLoop(ctx, c) t.Logf("echoLoop: %v", err) }) @@ -78,9 +73,7 @@ func TestAutobahn(t *testing.T) { }) c, _, err := websocket.Dial(ctx, fmt.Sprintf(wstestURL+"/updateReports?agent=main"), nil) - if err != nil { - t.Fatal(err) - } + assert.Success(t, err) c.Close(websocket.StatusNormalClosure, "") checkWSTestIndex(t, "./ci/out/wstestClientReports/index.json") @@ -172,18 +165,14 @@ func wstestCaseCount(ctx context.Context, url string) (cases int, err error) { func checkWSTestIndex(t *testing.T, path string) { wstestOut, err := ioutil.ReadFile(path) - if err != nil { - t.Fatal(err) - } + assert.Success(t, err) var indexJSON map[string]map[string]struct { Behavior string `json:"behavior"` BehaviorClose string `json:"behaviorClose"` } err = json.Unmarshal(wstestOut, &indexJSON) - if err != nil { - t.Fatal(err) - } + assert.Success(t, err) for _, tests := range indexJSON { for test, result := range tests { diff --git a/close_test.go b/close_test.go index 10a35b13..00a48d9e 100644 --- a/close_test.go +++ b/close_test.go @@ -8,7 +8,7 @@ import ( "strings" "testing" - "nhooyr.io/websocket/internal/test/cmp" + "nhooyr.io/websocket/internal/test/assert" ) func TestCloseError(t *testing.T) { @@ -51,8 +51,10 @@ func TestCloseError(t *testing.T) { t.Parallel() _, err := tc.ce.bytesErr() - if tc.success != (err == nil) { - t.Fatalf("unexpected error value (wanted err == nil == %v): %v", tc.success, err) + if tc.success { + assert.Success(t, err) + } else { + assert.Error(t, err) } }) } @@ -63,10 +65,7 @@ func TestCloseError(t *testing.T) { Code: StatusInternalError, Reason: "meow", }.Error() - - if (act) != exp { - t.Fatal(cmp.Diff(exp, act)) - } + assert.Equal(t, "CloseError.Error()", exp, act) }) } @@ -114,14 +113,10 @@ func Test_parseClosePayload(t *testing.T) { ce, err := parseClosePayload(tc.p) if tc.success { - if err != nil { - t.Fatal(err) - } - if !cmp.Equal(tc.ce, ce) { - t.Fatalf("expected %v but got %v", tc.ce, ce) - } - } else if err == nil { - t.Errorf("expected error: %v %v", ce, err) + assert.Success(t, err) + assert.Equal(t, "close payload", tc.ce, ce) + } else { + assert.Error(t, err) } }) } @@ -168,9 +163,7 @@ func Test_validWireCloseCode(t *testing.T) { t.Parallel() act := validWireCloseCode(tc.code) - if !cmp.Equal(tc.valid, act) { - t.Fatalf("unexpected valid: %v", cmp.Diff(tc.valid, act)) - } + assert.Equal(t, "wire close code", tc.valid, act) }) } } @@ -208,9 +201,7 @@ func TestCloseStatus(t *testing.T) { t.Parallel() act := CloseStatus(tc.in) - if !cmp.Equal(tc.exp, act) { - t.Fatalf("unexpected closeStatus: %v", cmp.Diff(tc.exp, act)) - } + assert.Equal(t, "close status", tc.exp, act) }) } } diff --git a/compress_test.go b/compress_test.go index 51f658c8..364d542d 100644 --- a/compress_test.go +++ b/compress_test.go @@ -6,6 +6,7 @@ import ( "strings" "testing" + "nhooyr.io/websocket/internal/test/assert" "nhooyr.io/websocket/internal/test/xrand" ) @@ -23,10 +24,7 @@ func Test_slidingWindow(t *testing.T) { r := newSlidingWindow(windowLength) r.write([]byte(input)) - if cap(r.buf) != windowLength { - t.Fatalf("sliding window length changed somehow: %q and windowLength %d", input, windowLength) - } - + assert.Equal(t, "window length", windowLength, cap(r.buf)) if !strings.HasSuffix(input, string(r.buf)) { t.Fatalf("r.buf is not a suffix of input: %q and %q", input, r.buf) } diff --git a/conn_test.go b/conn_test.go index b2a35af8..f9b52f22 100644 --- a/conn_test.go +++ b/conn_test.go @@ -20,7 +20,7 @@ import ( "golang.org/x/xerrors" "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/test/cmp" + "nhooyr.io/websocket/internal/test/assert" "nhooyr.io/websocket/internal/test/wstest" "nhooyr.io/websocket/internal/test/xrand" "nhooyr.io/websocket/internal/xsync" @@ -34,26 +34,21 @@ func TestConn(t *testing.T) { t.Run("fuzzData", func(t *testing.T) { t.Parallel() + copts := func() *websocket.CompressionOptions { + return &websocket.CompressionOptions{ + Mode: websocket.CompressionMode(xrand.Int(int(websocket.CompressionDisabled) + 1)), + Threshold: xrand.Int(9999), + } + } + for i := 0; i < 5; i++ { t.Run("", func(t *testing.T) { - tt := newTest(t) - defer tt.done() - - dialCopts := &websocket.CompressionOptions{ - Mode: websocket.CompressionMode(xrand.Int(int(websocket.CompressionDisabled) + 1)), - Threshold: xrand.Int(9999), - } - - acceptCopts := &websocket.CompressionOptions{ - Mode: websocket.CompressionMode(xrand.Int(int(websocket.CompressionDisabled) + 1)), - Threshold: xrand.Int(9999), - } - - c1, c2 := tt.pipe(&websocket.DialOptions{ - CompressionOptions: dialCopts, + tt, c1, c2 := newConnTest(t, &websocket.DialOptions{ + CompressionOptions: copts(), }, &websocket.AcceptOptions{ - CompressionOptions: acceptCopts, + CompressionOptions: copts(), }) + defer tt.done() tt.goEchoLoop(c2) @@ -61,60 +56,53 @@ func TestConn(t *testing.T) { for i := 0; i < 5; i++ { err := wstest.Echo(tt.ctx, c1, 131072) - tt.success(err) + assert.Success(t, err) } err := c1.Close(websocket.StatusNormalClosure, "") - tt.success(err) + assert.Success(t, err) }) } }) t.Run("badClose", func(t *testing.T) { - tt := newTest(t) + tt, c1, _ := newConnTest(t, nil, nil) defer tt.done() - c1, _ := tt.pipe(nil, nil) - err := c1.Close(-1, "") - tt.errContains(err, "failed to marshal close frame: status code StatusCode(-1) cannot be set") + assert.Contains(t, err, "failed to marshal close frame: status code StatusCode(-1) cannot be set") }) t.Run("ping", func(t *testing.T) { - tt := newTest(t) + tt, c1, c2 := newConnTest(t, nil, nil) defer tt.done() - c1, c2 := tt.pipe(nil, nil) - c1.CloseRead(tt.ctx) c2.CloseRead(tt.ctx) for i := 0; i < 10; i++ { err := c1.Ping(tt.ctx) - tt.success(err) + assert.Success(t, err) } err := c1.Close(websocket.StatusNormalClosure, "") - tt.success(err) + assert.Success(t, err) }) t.Run("badPing", func(t *testing.T) { - tt := newTest(t) + tt, c1, c2 := newConnTest(t, nil, nil) defer tt.done() - c1, c2 := tt.pipe(nil, nil) - c2.CloseRead(tt.ctx) err := c1.Ping(tt.ctx) - tt.errContains(err, "failed to wait for pong") + assert.Contains(t, err, "failed to wait for pong") }) t.Run("concurrentWrite", func(t *testing.T) { - tt := newTest(t) + tt, c1, c2 := newConnTest(t, nil, nil) defer tt.done() - c1, c2 := tt.pipe(nil, nil) tt.goDiscardLoop(c2) msg := xrand.Bytes(xrand.Int(9999)) @@ -129,35 +117,31 @@ func TestConn(t *testing.T) { for i := 0; i < count; i++ { err := <-errs - tt.success(err) + assert.Success(t, err) } err := c1.Close(websocket.StatusNormalClosure, "") - tt.success(err) + assert.Success(t, err) }) t.Run("concurrentWriteError", func(t *testing.T) { - tt := newTest(t) + tt, c1, _ := newConnTest(t, nil, nil) defer tt.done() - c1, _ := tt.pipe(nil, nil) - _, err := c1.Writer(tt.ctx, websocket.MessageText) - tt.success(err) + assert.Success(t, err) ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond*100) defer cancel() err = c1.Write(ctx, websocket.MessageText, []byte("x")) - tt.eq(context.DeadlineExceeded, err) + assert.Equal(t, "write error", context.DeadlineExceeded, err) }) t.Run("netConn", func(t *testing.T) { - tt := newTest(t) + tt, c1, c2 := newConnTest(t, nil, nil) defer tt.done() - c1, c2 := tt.pipe(nil, nil) - n1 := websocket.NetConn(tt.ctx, c1, websocket.MessageBinary) n2 := websocket.NetConn(tt.ctx, c2, websocket.MessageBinary) @@ -166,9 +150,9 @@ func TestConn(t *testing.T) { n1.SetDeadline(d) n1.SetDeadline(time.Time{}) - tt.eq(n1.RemoteAddr(), n1.LocalAddr()) - tt.eq("websocket/unknown-addr", n1.RemoteAddr().String()) - tt.eq("websocket", n1.RemoteAddr().Network()) + assert.Equal(t, "remote addr", n1.RemoteAddr(), n1.LocalAddr()) + assert.Equal(t, "remote addr string", "websocket/unknown-addr", n1.RemoteAddr().String()) + assert.Equal(t, "remote addr network", "websocket", n1.RemoteAddr().Network()) errs := xsync.Go(func() error { _, err := n2.Write([]byte("hello")) @@ -179,23 +163,21 @@ func TestConn(t *testing.T) { }) b, err := ioutil.ReadAll(n1) - tt.success(err) + assert.Success(t, err) _, err = n1.Read(nil) - tt.eq(err, io.EOF) + assert.Equal(t, "read error", err, io.EOF) err = <-errs - tt.success(err) + assert.Success(t, err) - tt.eq([]byte("hello"), b) + assert.Equal(t, "read msg", []byte("hello"), b) }) t.Run("netConn/BadMsg", func(t *testing.T) { - tt := newTest(t) + tt, c1, c2 := newConnTest(t, nil, nil) defer tt.done() - c1, c2 := tt.pipe(nil, nil) - n1 := websocket.NetConn(tt.ctx, c1, websocket.MessageBinary) n2 := websocket.NetConn(tt.ctx, c2, websocket.MessageText) @@ -208,18 +190,16 @@ func TestConn(t *testing.T) { }) _, err := ioutil.ReadAll(n1) - tt.errContains(err, `unexpected frame type read (expected MessageBinary): MessageText`) + assert.Contains(t, err, `unexpected frame type read (expected MessageBinary): MessageText`) err = <-errs - tt.success(err) + assert.Success(t, err) }) t.Run("wsjson", func(t *testing.T) { - tt := newTest(t) + tt, c1, c2 := newConnTest(t, nil, nil) defer tt.done() - c1, c2 := tt.pipe(nil, nil) - tt.goEchoLoop(c2) c1.SetReadLimit(1 << 30) @@ -232,35 +212,33 @@ func TestConn(t *testing.T) { var act interface{} err := wsjson.Read(tt.ctx, c1, &act) - tt.success(err) - tt.eq(exp, act) + assert.Success(t, err) + assert.Equal(t, "read msg", exp, act) err = <-werr - tt.success(err) + assert.Success(t, err) err = c1.Close(websocket.StatusNormalClosure, "") - tt.success(err) + assert.Success(t, err) }) t.Run("wspb", func(t *testing.T) { - tt := newTest(t) + tt, c1, c2 := newConnTest(t, nil, nil) defer tt.done() - c1, c2 := tt.pipe(nil, nil) - tt.goEchoLoop(c2) exp := ptypes.DurationProto(100) err := wspb.Write(tt.ctx, c1, exp) - tt.success(err) + assert.Success(t, err) act := &duration.Duration{} err = wspb.Read(tt.ctx, c1, act) - tt.success(err) - tt.eq(exp, act) + assert.Success(t, err) + assert.Equal(t, "read msg", exp, act) err = c1.Close(websocket.StatusNormalClosure, "") - tt.success(err) + assert.Success(t, err) }) } @@ -277,14 +255,17 @@ func TestWasm(t *testing.T) { InsecureSkipVerify: true, }) if err != nil { - t.Error(err) + t.Errorf("echo server failed: %v", err) return } defer c.Close(websocket.StatusInternalError, "") err = wstest.EchoLoop(r.Context(), c) - if websocket.CloseStatus(err) != websocket.StatusNormalClosure { - t.Errorf("echoLoop failed: %v", err) + + err = assertCloseStatus(websocket.StatusNormalClosure, err) + if err != nil { + t.Errorf("echo server failed: %v", err) + return } })) defer wg.Wait() @@ -307,38 +288,47 @@ func assertCloseStatus(exp websocket.StatusCode, err error) error { return xerrors.Errorf("expected websocket.CloseError: %T %v", err, err) } if websocket.CloseStatus(err) != exp { - return xerrors.Errorf("unexpected close status (%v):%v", exp, err) + return xerrors.Errorf("expected close status %v but got ", exp, err) } return nil } -type test struct { +type connTest struct { t *testing.T ctx context.Context doneFuncs []func() } -func newTest(t *testing.T) *test { +func newConnTest(t *testing.T, dialOpts *websocket.DialOptions, acceptOpts *websocket.AcceptOptions) (tt *connTest, c1, c2 *websocket.Conn) { t.Parallel() + t.Helper() ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) - tt := &test{t: t, ctx: ctx} + tt = &connTest{t: t, ctx: ctx} tt.appendDone(cancel) - return tt + + c1, c2, err := wstest.Pipe(dialOpts, acceptOpts) + assert.Success(tt.t, err) + tt.appendDone(func() { + c2.Close(websocket.StatusInternalError, "") + c1.Close(websocket.StatusInternalError, "") + }) + + return tt, c1, c2 } -func (tt *test) appendDone(f func()) { +func (tt *connTest) appendDone(f func()) { tt.doneFuncs = append(tt.doneFuncs, f) } -func (tt *test) done() { +func (tt *connTest) done() { for i := len(tt.doneFuncs) - 1; i >= 0; i-- { tt.doneFuncs[i]() } } -func (tt *test) goEchoLoop(c *websocket.Conn) { +func (tt *connTest) goEchoLoop(c *websocket.Conn) { ctx, cancel := context.WithCancel(tt.ctx) echoLoopErr := xsync.Go(func() error { @@ -354,7 +344,7 @@ func (tt *test) goEchoLoop(c *websocket.Conn) { }) } -func (tt *test) goDiscardLoop(c *websocket.Conn) { +func (tt *connTest) goDiscardLoop(c *websocket.Conn) { ctx, cancel := context.WithCancel(tt.ctx) discardLoopErr := xsync.Go(func() error { @@ -376,38 +366,3 @@ func (tt *test) goDiscardLoop(c *websocket.Conn) { } }) } - -func (tt *test) pipe(dialOpts *websocket.DialOptions, acceptOpts *websocket.AcceptOptions) (c1, c2 *websocket.Conn) { - tt.t.Helper() - - c1, c2, err := wstest.Pipe(dialOpts, acceptOpts) - if err != nil { - tt.t.Fatal(err) - } - tt.appendDone(func() { - c2.Close(websocket.StatusInternalError, "") - c1.Close(websocket.StatusInternalError, "") - }) - return c1, c2 -} - -func (tt *test) success(err error) { - tt.t.Helper() - if err != nil { - tt.t.Fatal(err) - } -} - -func (tt *test) errContains(err error, sub string) { - tt.t.Helper() - if !cmp.ErrorContains(err, sub) { - tt.t.Fatalf("error does not contain %q: %v", sub, err) - } -} - -func (tt *test) eq(exp, act interface{}) { - tt.t.Helper() - if !cmp.Equal(exp, act) { - tt.t.Fatalf(cmp.Diff(exp, act)) - } -} diff --git a/dial_test.go b/dial_test.go index c4657415..06084cc5 100644 --- a/dial_test.go +++ b/dial_test.go @@ -13,7 +13,7 @@ import ( "testing" "time" - "nhooyr.io/websocket/internal/test/cmp" + "nhooyr.io/websocket/internal/test/assert" ) func TestBadDials(t *testing.T) { @@ -70,9 +70,7 @@ func TestBadDials(t *testing.T) { } _, _, err := dial(ctx, tc.url, tc.opts, tc.rand) - if err == nil { - t.Fatalf("expected error") - } + assert.Error(t, err) }) } }) @@ -90,9 +88,7 @@ func TestBadDials(t *testing.T) { }, nil }), }) - if !cmp.ErrorContains(err, "failed to WebSocket dial: expected handshake response status code 101 but got 0") { - t.Fatal(err) - } + assert.Contains(t, err, "failed to WebSocket dial: expected handshake response status code 101 but got 0") }) t.Run("badBody", func(t *testing.T) { @@ -117,9 +113,7 @@ func TestBadDials(t *testing.T) { _, _, err := Dial(ctx, "ws://example.com", &DialOptions{ HTTPClient: mockHTTPClient(rt), }) - if !cmp.ErrorContains(err, "response body is not a io.ReadWriteCloser") { - t.Fatal(err) - } + assert.Contains(t, err, "response body is not a io.ReadWriteCloser") }) } @@ -217,9 +211,7 @@ func Test_verifyServerHandshake(t *testing.T) { r := httptest.NewRequest("GET", "/", nil) key, err := secWebSocketKey(rand.Reader) - if err != nil { - t.Fatal(err) - } + assert.Success(t, err) r.Header.Set("Sec-WebSocket-Key", key) if resp.Header.Get("Sec-WebSocket-Accept") == "" { @@ -230,8 +222,10 @@ func Test_verifyServerHandshake(t *testing.T) { Subprotocols: strings.Split(r.Header.Get("Sec-WebSocket-Protocol"), ","), } _, err = verifyServerResponse(opts, key, resp) - if (err == nil) != tc.success { - t.Fatalf("unexpected error: %v", err) + if tc.success { + assert.Success(t, err) + } else { + assert.Error(t, err) } }) } diff --git a/frame_test.go b/frame_test.go index 0b770a4c..8745da0b 100644 --- a/frame_test.go +++ b/frame_test.go @@ -16,7 +16,7 @@ import ( "github.com/gobwas/ws" _ "github.com/gorilla/websocket" - "nhooyr.io/websocket/internal/test/cmp" + "nhooyr.io/websocket/internal/test/assert" ) func TestHeader(t *testing.T) { @@ -81,22 +81,15 @@ func testHeader(t *testing.T, h header) { r := bufio.NewReader(b) err := writeFrameHeader(h, w) - if err != nil { - t.Fatal(err) - } + assert.Success(t, err) + err = w.Flush() - if err != nil { - t.Fatal(err) - } + assert.Success(t, err) h2, err := readFrameHeader(r) - if err != nil { - t.Fatal(err) - } + assert.Success(t, err) - if !cmp.Equal(h, h2) { - t.Fatal(cmp.Diff(h, h2)) - } + assert.Equal(t, "read header", h, h2) } func Test_mask(t *testing.T) { @@ -108,14 +101,10 @@ func Test_mask(t *testing.T) { gotKey32 := mask(key32, p) expP := []byte{0, 0, 0, 0x0d, 0x6} - if !cmp.Equal(expP, p) { - t.Fatal(cmp.Diff(expP, p)) - } + assert.Equal(t, "p", expP, p) expKey32 := bits.RotateLeft32(key32, -8) - if !cmp.Equal(expKey32, gotKey32) { - t.Fatal(cmp.Diff(expKey32, gotKey32)) - } + assert.Equal(t, "key32", expKey32, gotKey32) } func basicMask(maskKey [4]byte, pos int, b []byte) int { diff --git a/internal/test/assert/assert.go b/internal/test/assert/assert.go new file mode 100644 index 00000000..2bc01dba --- /dev/null +++ b/internal/test/assert/assert.go @@ -0,0 +1,46 @@ +package assert + +import ( + "fmt" + "strings" + "testing" + + "nhooyr.io/websocket/internal/test/cmp" +) + +// Equal asserts exp == act. +func Equal(t testing.TB, name string, exp, act interface{}) { + t.Helper() + + if diff := cmp.Diff(exp, act); diff != "" { + t.Fatalf("unexpected %v: %v", name, diff) + } +} + +// Success asserts err == nil. +func Success(t testing.TB, err error) { + t.Helper() + + if err != nil { + t.Fatal(err) + } +} + +// Error asserts err != nil. +func Error(t testing.TB, err error) { + t.Helper() + + if err == nil { + t.Fatal("expected error") + } +} + +// Contains asserts the fmt.Sprint(v) contains sub. +func Contains(t testing.TB, v interface{}, sub string) { + t.Helper() + + vstr := fmt.Sprint(v) + if !strings.Contains(vstr, sub) { + t.Fatalf("expected %q to contain %q", vstr, sub) + } +} diff --git a/internal/test/cmp/cmp.go b/internal/test/cmp/cmp.go index 6f3dd706..eadcb5d9 100644 --- a/internal/test/cmp/cmp.go +++ b/internal/test/cmp/cmp.go @@ -2,31 +2,15 @@ package cmp import ( "reflect" - "strings" "github.com/golang/protobuf/proto" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" ) -// Equal checks if v1 and v2 are equal with go-cmp. -func Equal(v1, v2 interface{}) bool { - return cmp.Equal(v1, v2, cmpopts.EquateErrors(), cmp.Exporter(func(r reflect.Type) bool { - return true - }), cmp.Comparer(proto.Equal)) -} - // Diff returns a human readable diff between v1 and v2 func Diff(v1, v2 interface{}) string { return cmp.Diff(v1, v2, cmpopts.EquateErrors(), cmp.Exporter(func(r reflect.Type) bool { return true - })) -} - -// ErrorContains returns whether err.Error() contains sub. -func ErrorContains(err error, sub string) bool { - if err == nil { - return false - } - return strings.Contains(err.Error(), sub) + }), cmp.Comparer(proto.Equal)) } diff --git a/internal/xsync/go_test.go b/internal/xsync/go_test.go index c0613e64..dabea8a5 100644 --- a/internal/xsync/go_test.go +++ b/internal/xsync/go_test.go @@ -3,7 +3,7 @@ package xsync import ( "testing" - "nhooyr.io/websocket/internal/test/cmp" + "nhooyr.io/websocket/internal/test/assert" ) func TestGoRecover(t *testing.T) { @@ -14,7 +14,5 @@ func TestGoRecover(t *testing.T) { }) err := <-errs - if !cmp.ErrorContains(err, "anmol") { - t.Fatalf("unexpected err: %v", err) - } + assert.Contains(t, err, "anmol") } diff --git a/ws_js_test.go b/ws_js_test.go index 8671dd21..e6be6181 100644 --- a/ws_js_test.go +++ b/ws_js_test.go @@ -8,7 +8,7 @@ import ( "time" "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/test/cmp" + "nhooyr.io/websocket/internal/test/assert" "nhooyr.io/websocket/internal/test/wstest" ) @@ -21,28 +21,18 @@ func TestWasm(t *testing.T) { c, resp, err := websocket.Dial(ctx, os.Getenv("WS_ECHO_SERVER_URL"), &websocket.DialOptions{ Subprotocols: []string{"echo"}, }) - if err != nil { - t.Fatal(err) - } + assert.Success(t, err) defer c.Close(websocket.StatusInternalError, "") - if !cmp.Equal("echo", c.Subprotocol()) { - t.Fatalf("unexpected subprotocol: %v", cmp.Diff("echo", c.Subprotocol())) - } - if !cmp.Equal(http.StatusSwitchingProtocols, resp.StatusCode) { - t.Fatalf("unexpected status code: %v", cmp.Diff(http.StatusSwitchingProtocols, resp.StatusCode)) - } + assert.Equal(t, "subprotocol", "echo", c.Subprotocol()) + assert.Equal(t, "response code", http.StatusSwitchingProtocols, resp.StatusCode) c.SetReadLimit(65536) for i := 0; i < 10; i++ { err = wstest.Echo(ctx, c, 65536) - if err != nil { - t.Fatal(err) - } + assert.Success(t, err) } err = c.Close(websocket.StatusNormalClosure, "") - if err != nil { - t.Fatal(err) - } + assert.Success(t, err) } From c5b0a009c19d7240f17ae3857741e8904deadeb6 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 13 Feb 2020 01:34:32 -0500 Subject: [PATCH 262/519] Fix badPing test duration --- conn_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/conn_test.go b/conn_test.go index f9b52f22..e1e6c35c 100644 --- a/conn_test.go +++ b/conn_test.go @@ -95,7 +95,10 @@ func TestConn(t *testing.T) { c2.CloseRead(tt.ctx) - err := c1.Ping(tt.ctx) + ctx, cancel := context.WithTimeout(tt.ctx, time.Millisecond*100) + defer cancel() + + err := c1.Ping(ctx) assert.Contains(t, err, "failed to wait for pong") }) From 1c7c14ea4a79ee48a621e419aacff447dec3c8bf Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 13 Feb 2020 01:46:17 -0500 Subject: [PATCH 263/519] Pool sliding windows --- compress_notjs.go | 17 ++++++++++++++++- read.go | 4 ++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/compress_notjs.go b/compress_notjs.go index 6ab6e284..3f0d8b9f 100644 --- a/compress_notjs.go +++ b/compress_notjs.go @@ -115,16 +115,31 @@ func putFlateWriter(w *flate.Writer) { } type slidingWindow struct { - r io.Reader buf []byte } +var swPool = map[int]*sync.Pool{} + func newSlidingWindow(n int) *slidingWindow { + p, ok := swPool[n] + if !ok { + p = &sync.Pool{} + swPool[n] = p + } + sw, ok := p.Get().(*slidingWindow) + if ok { + return sw + } return &slidingWindow{ buf: make([]byte, 0, n), } } +func returnSlidingWindow(sw *slidingWindow) { + sw.buf = sw.buf[:0] + swPool[cap(sw.buf)].Put(sw) +} + func (w *slidingWindow) write(p []byte) { if len(p) >= cap(w.buf) { w.buf = w.buf[:cap(w.buf)] diff --git a/read.go b/read.go index a9c291d1..49c03b40 100644 --- a/read.go +++ b/read.go @@ -110,6 +110,10 @@ func (mr *msgReader) returnFlateReader() { func (mr *msgReader) close() { mr.c.readMu.Lock(context.Background()) mr.returnFlateReader() + + if mr.dict != nil { + returnSlidingWindow(mr.dict) + } } func (mr *msgReader) flateContextTakeover() bool { From 503b4696fcbad5c2c18e364fcc31540a7c5e43e9 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 13 Feb 2020 01:57:19 -0500 Subject: [PATCH 264/519] Simplify sliding window API --- compress_notjs.go | 43 +++++++++++++++++++++++++------------------ compress_test.go | 11 ++++++----- conn_test.go | 7 +++---- read.go | 16 +++++----------- 4 files changed, 39 insertions(+), 38 deletions(-) diff --git a/compress_notjs.go b/compress_notjs.go index 3f0d8b9f..20761362 100644 --- a/compress_notjs.go +++ b/compress_notjs.go @@ -120,41 +120,48 @@ type slidingWindow struct { var swPool = map[int]*sync.Pool{} -func newSlidingWindow(n int) *slidingWindow { +func (sw *slidingWindow) init(n int) { + if sw.buf != nil { + return + } + p, ok := swPool[n] if !ok { p = &sync.Pool{} swPool[n] = p } - sw, ok := p.Get().(*slidingWindow) + buf, ok := p.Get().([]byte) if ok { - return sw - } - return &slidingWindow{ - buf: make([]byte, 0, n), + sw.buf = buf[:0] + } else { + sw.buf = make([]byte, 0, n) } } -func returnSlidingWindow(sw *slidingWindow) { - sw.buf = sw.buf[:0] - swPool[cap(sw.buf)].Put(sw) +func (sw *slidingWindow) close() { + if sw.buf == nil { + return + } + + swPool[cap(sw.buf)].Put(sw.buf) + sw.buf = nil } -func (w *slidingWindow) write(p []byte) { - if len(p) >= cap(w.buf) { - w.buf = w.buf[:cap(w.buf)] - p = p[len(p)-cap(w.buf):] - copy(w.buf, p) +func (sw *slidingWindow) write(p []byte) { + if len(p) >= cap(sw.buf) { + sw.buf = sw.buf[:cap(sw.buf)] + p = p[len(p)-cap(sw.buf):] + copy(sw.buf, p) return } - left := cap(w.buf) - len(w.buf) + left := cap(sw.buf) - len(sw.buf) if left < len(p) { // We need to shift spaceNeeded bytes from the end to make room for p at the end. spaceNeeded := len(p) - left - copy(w.buf, w.buf[spaceNeeded:]) - w.buf = w.buf[:len(w.buf)-spaceNeeded] + copy(sw.buf, sw.buf[spaceNeeded:]) + sw.buf = sw.buf[:len(sw.buf)-spaceNeeded] } - w.buf = append(w.buf, p...) + sw.buf = append(sw.buf, p...) } diff --git a/compress_test.go b/compress_test.go index 364d542d..2c4c896c 100644 --- a/compress_test.go +++ b/compress_test.go @@ -21,12 +21,13 @@ func Test_slidingWindow(t *testing.T) { input := xrand.String(maxWindow) windowLength := xrand.Int(maxWindow) - r := newSlidingWindow(windowLength) - r.write([]byte(input)) + var sw slidingWindow + sw.init(windowLength) + sw.write([]byte(input)) - assert.Equal(t, "window length", windowLength, cap(r.buf)) - if !strings.HasSuffix(input, string(r.buf)) { - t.Fatalf("r.buf is not a suffix of input: %q and %q", input, r.buf) + assert.Equal(t, "window length", windowLength, cap(sw.buf)) + if !strings.HasSuffix(input, string(sw.buf)) { + t.Fatalf("r.buf is not a suffix of input: %q and %q", input, sw.buf) } }) } diff --git a/conn_test.go b/conn_test.go index e1e6c35c..25b0809d 100644 --- a/conn_test.go +++ b/conn_test.go @@ -351,13 +351,12 @@ func (tt *connTest) goDiscardLoop(c *websocket.Conn) { ctx, cancel := context.WithCancel(tt.ctx) discardLoopErr := xsync.Go(func() error { + defer c.Close(websocket.StatusInternalError, "") + for { _, _, err := c.Read(ctx) - if websocket.CloseStatus(err) == websocket.StatusNormalClosure { - return nil - } if err != nil { - return err + return assertCloseStatus(websocket.StatusNormalClosure, err) } } }) diff --git a/read.go b/read.go index 49c03b40..dd73ac92 100644 --- a/read.go +++ b/read.go @@ -87,15 +87,11 @@ func newMsgReader(c *Conn) *msgReader { } func (mr *msgReader) resetFlate() { - if mr.flateContextTakeover() && mr.dict == nil { - mr.dict = newSlidingWindow(32768) - } - if mr.flateContextTakeover() { - mr.flateReader = getFlateReader(readerFunc(mr.read), mr.dict.buf) - } else { - mr.flateReader = getFlateReader(readerFunc(mr.read), nil) + mr.dict.init(32768) } + + mr.flateReader = getFlateReader(readerFunc(mr.read), mr.dict.buf) mr.limitReader.r = mr.flateReader mr.flateTail.Reset(deflateMessageTail) } @@ -111,9 +107,7 @@ func (mr *msgReader) close() { mr.c.readMu.Lock(context.Background()) mr.returnFlateReader() - if mr.dict != nil { - returnSlidingWindow(mr.dict) - } + mr.dict.close() } func (mr *msgReader) flateContextTakeover() bool { @@ -325,7 +319,7 @@ type msgReader struct { flateReader io.Reader flateTail strings.Reader limitReader *limitReader - dict *slidingWindow + dict slidingWindow fin bool payloadLength int64 From dff4af3cbf8ae30e4961ff2a6b32e46344c1b424 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 13 Feb 2020 02:54:18 -0500 Subject: [PATCH 265/519] Add conn benchmark --- .gitignore | 1 + autobahn_test.go | 2 - compress_notjs.go | 7 +++ conn_notjs.go | 1 + conn_test.go | 124 ++++++++++++++++++++++++++++++++++++++----- frame.go | 13 +++-- frame_test.go | 3 +- internal/xsync/go.go | 2 +- read.go | 6 ++- 9 files changed, 132 insertions(+), 27 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..6961e5c8 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +websocket.test diff --git a/autobahn_test.go b/autobahn_test.go index 0763bc97..fb24a06b 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -59,8 +59,6 @@ func TestAutobahn(t *testing.T) { for i := 1; i <= cases; i++ { i := i t.Run("", func(t *testing.T) { - t.Parallel() - ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) defer cancel() diff --git a/compress_notjs.go b/compress_notjs.go index 20761362..7c6b2fc0 100644 --- a/compress_notjs.go +++ b/compress_notjs.go @@ -118,6 +118,7 @@ type slidingWindow struct { buf []byte } +var swPoolMu sync.Mutex var swPool = map[int]*sync.Pool{} func (sw *slidingWindow) init(n int) { @@ -125,6 +126,9 @@ func (sw *slidingWindow) init(n int) { return } + swPoolMu.Lock() + defer swPoolMu.Unlock() + p, ok := swPool[n] if !ok { p = &sync.Pool{} @@ -143,6 +147,9 @@ func (sw *slidingWindow) close() { return } + swPoolMu.Lock() + defer swPoolMu.Unlock() + swPool[cap(sw.buf)].Put(sw.buf) sw.buf = nil } diff --git a/conn_notjs.go b/conn_notjs.go index 4d8762bf..178fcad0 100644 --- a/conn_notjs.go +++ b/conn_notjs.go @@ -39,6 +39,7 @@ type Conn struct { // Read state. readMu *mu + readHeader header readControlBuf [maxControlPayload]byte msgReader *msgReader readCloseFrameErr error diff --git a/conn_test.go b/conn_test.go index 25b0809d..265156e9 100644 --- a/conn_test.go +++ b/conn_test.go @@ -3,7 +3,9 @@ package websocket_test import ( + "bytes" "context" + "crypto/rand" "fmt" "io" "io/ioutil" @@ -48,7 +50,7 @@ func TestConn(t *testing.T) { }, &websocket.AcceptOptions{ CompressionOptions: copts(), }) - defer tt.done() + defer tt.cleanup() tt.goEchoLoop(c2) @@ -67,7 +69,7 @@ func TestConn(t *testing.T) { t.Run("badClose", func(t *testing.T) { tt, c1, _ := newConnTest(t, nil, nil) - defer tt.done() + defer tt.cleanup() err := c1.Close(-1, "") assert.Contains(t, err, "failed to marshal close frame: status code StatusCode(-1) cannot be set") @@ -75,7 +77,7 @@ func TestConn(t *testing.T) { t.Run("ping", func(t *testing.T) { tt, c1, c2 := newConnTest(t, nil, nil) - defer tt.done() + defer tt.cleanup() c1.CloseRead(tt.ctx) c2.CloseRead(tt.ctx) @@ -91,7 +93,7 @@ func TestConn(t *testing.T) { t.Run("badPing", func(t *testing.T) { tt, c1, c2 := newConnTest(t, nil, nil) - defer tt.done() + defer tt.cleanup() c2.CloseRead(tt.ctx) @@ -104,7 +106,7 @@ func TestConn(t *testing.T) { t.Run("concurrentWrite", func(t *testing.T) { tt, c1, c2 := newConnTest(t, nil, nil) - defer tt.done() + defer tt.cleanup() tt.goDiscardLoop(c2) @@ -129,7 +131,7 @@ func TestConn(t *testing.T) { t.Run("concurrentWriteError", func(t *testing.T) { tt, c1, _ := newConnTest(t, nil, nil) - defer tt.done() + defer tt.cleanup() _, err := c1.Writer(tt.ctx, websocket.MessageText) assert.Success(t, err) @@ -143,7 +145,7 @@ func TestConn(t *testing.T) { t.Run("netConn", func(t *testing.T) { tt, c1, c2 := newConnTest(t, nil, nil) - defer tt.done() + defer tt.cleanup() n1 := websocket.NetConn(tt.ctx, c1, websocket.MessageBinary) n2 := websocket.NetConn(tt.ctx, c2, websocket.MessageBinary) @@ -179,7 +181,7 @@ func TestConn(t *testing.T) { t.Run("netConn/BadMsg", func(t *testing.T) { tt, c1, c2 := newConnTest(t, nil, nil) - defer tt.done() + defer tt.cleanup() n1 := websocket.NetConn(tt.ctx, c1, websocket.MessageBinary) n2 := websocket.NetConn(tt.ctx, c2, websocket.MessageText) @@ -201,7 +203,7 @@ func TestConn(t *testing.T) { t.Run("wsjson", func(t *testing.T) { tt, c1, c2 := newConnTest(t, nil, nil) - defer tt.done() + defer tt.cleanup() tt.goEchoLoop(c2) @@ -227,7 +229,7 @@ func TestConn(t *testing.T) { t.Run("wspb", func(t *testing.T) { tt, c1, c2 := newConnTest(t, nil, nil) - defer tt.done() + defer tt.cleanup() tt.goEchoLoop(c2) @@ -297,14 +299,16 @@ func assertCloseStatus(exp websocket.StatusCode, err error) error { } type connTest struct { - t *testing.T + t testing.TB ctx context.Context doneFuncs []func() } -func newConnTest(t *testing.T, dialOpts *websocket.DialOptions, acceptOpts *websocket.AcceptOptions) (tt *connTest, c1, c2 *websocket.Conn) { - t.Parallel() +func newConnTest(t testing.TB, dialOpts *websocket.DialOptions, acceptOpts *websocket.AcceptOptions) (tt *connTest, c1, c2 *websocket.Conn) { + if t, ok := t.(*testing.T); ok { + t.Parallel() + } t.Helper() ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) @@ -325,7 +329,7 @@ func (tt *connTest) appendDone(f func()) { tt.doneFuncs = append(tt.doneFuncs, f) } -func (tt *connTest) done() { +func (tt *connTest) cleanup() { for i := len(tt.doneFuncs) - 1; i >= 0; i-- { tt.doneFuncs[i]() } @@ -368,3 +372,95 @@ func (tt *connTest) goDiscardLoop(c *websocket.Conn) { } }) } + +func BenchmarkConn(b *testing.B) { + var benchCases = []struct { + name string + mode websocket.CompressionMode + }{ + { + name: "compressionDisabled", + mode: websocket.CompressionDisabled, + }, + { + name: "compression", + mode: websocket.CompressionContextTakeover, + }, + { + name: "noContextCompression", + mode: websocket.CompressionNoContextTakeover, + }, + } + for _, bc := range benchCases { + b.Run(bc.name, func(b *testing.B) { + bb, c1, c2 := newConnTest(b, &websocket.DialOptions{ + CompressionOptions: &websocket.CompressionOptions{Mode: bc.mode}, + }, nil) + defer bb.cleanup() + + bb.goEchoLoop(c2) + + const n = 32768 + writeBuf := make([]byte, n) + readBuf := make([]byte, n) + writes := make(chan websocket.MessageType) + defer close(writes) + werrs := make(chan error) + + go func() { + for typ := range writes { + werrs <- c1.Write(bb.ctx, typ, writeBuf) + } + }() + b.SetBytes(n) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _, err := rand.Reader.Read(writeBuf) + if err != nil { + b.Fatal(err) + } + + expType := websocket.MessageBinary + if writeBuf[0]%2 == 1 { + expType = websocket.MessageText + } + writes <- expType + + typ, r, err := c1.Reader(bb.ctx) + if err != nil { + b.Fatal(err) + } + if expType != typ { + assert.Equal(b, "data type", expType, typ) + } + + _, err = io.ReadFull(r, readBuf) + if err != nil { + b.Fatal(err) + } + + n2, err := r.Read(readBuf) + if err != io.EOF { + assert.Equal(b, "read err", io.EOF, err) + } + if n2 != 0 { + assert.Equal(b, "n2", 0, n2) + } + + if !bytes.Equal(writeBuf, readBuf) { + assert.Equal(b, "msg", writeBuf, readBuf) + } + + err = <-werrs + if err != nil { + b.Fatal(err) + } + } + b.StopTimer() + + err := c1.Close(websocket.StatusNormalClosure, "") + assert.Success(b, err) + }) + } +} diff --git a/frame.go b/frame.go index 0257835e..491ae75c 100644 --- a/frame.go +++ b/frame.go @@ -46,15 +46,14 @@ type header struct { // readFrameHeader reads a header from the reader. // See https://tools.ietf.org/html/rfc6455#section-5.2. -func readFrameHeader(r *bufio.Reader) (_ header, err error) { +func readFrameHeader(h *header, r *bufio.Reader) (err error) { defer errd.Wrap(&err, "failed to read frame header") b, err := r.ReadByte() if err != nil { - return header{}, err + return err } - var h header h.fin = b&(1<<7) != 0 h.rsv1 = b&(1<<6) != 0 h.rsv2 = b&(1<<5) != 0 @@ -64,7 +63,7 @@ func readFrameHeader(r *bufio.Reader) (_ header, err error) { b, err = r.ReadByte() if err != nil { - return header{}, err + return err } h.masked = b&(1<<7) != 0 @@ -81,17 +80,17 @@ func readFrameHeader(r *bufio.Reader) (_ header, err error) { err = binary.Read(r, binary.BigEndian, &h.payloadLength) } if err != nil { - return header{}, err + return err } if h.masked { err = binary.Read(r, binary.LittleEndian, &h.maskKey) if err != nil { - return header{}, err + return err } } - return h, nil + return nil } // maxControlPayload is the maximum length of a control frame payload. diff --git a/frame_test.go b/frame_test.go index 8745da0b..38f1599a 100644 --- a/frame_test.go +++ b/frame_test.go @@ -86,7 +86,8 @@ func testHeader(t *testing.T, h header) { err = w.Flush() assert.Success(t, err) - h2, err := readFrameHeader(r) + var h2 header + err = readFrameHeader(&h2, r) assert.Success(t, err) assert.Equal(t, "read header", h, h2) diff --git a/internal/xsync/go.go b/internal/xsync/go.go index 96cf8103..d88ac622 100644 --- a/internal/xsync/go.go +++ b/internal/xsync/go.go @@ -6,7 +6,7 @@ import ( // Go allows running a function in another goroutine // and waiting for its error. -func Go(fn func() error) chan error { +func Go(fn func() error) <- chan error { errs := make(chan error, 1) go func() { defer func() { diff --git a/read.go b/read.go index dd73ac92..bf7fa6d9 100644 --- a/read.go +++ b/read.go @@ -173,7 +173,7 @@ func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { case c.readTimeout <- ctx: } - h, err := readFrameHeader(c.br) + err := readFrameHeader(&c.readHeader, c.br) if err != nil { select { case <-c.closed: @@ -192,7 +192,7 @@ func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { case c.readTimeout <- context.Background(): } - return h, nil + return c.readHeader, nil } func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { @@ -390,6 +390,8 @@ func (mr *msgReader) read(p []byte) (int, error) { return 0, err } mr.setFrame(h) + + return mr.read(p) } if int64(len(p)) > mr.payloadLength { From 2377cca1760dfd3ee74cd945b775aef44b98ebb9 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 15 Feb 2020 15:54:25 -0500 Subject: [PATCH 266/519] Switch to klauspost/compress --- compress_notjs.go | 44 +++++++++++++----- conn_notjs.go | 17 +++---- conn_test.go | 49 +++++++++----------- frame.go | 41 ++++++++++------ frame_test.go | 5 +- go.mod | 1 + go.sum | 2 + internal/xsync/go.go | 2 +- read.go | 93 +++++++++++++++++++++---------------- write.go | 108 ++++++++++++++++++++++++++----------------- 10 files changed, 215 insertions(+), 147 deletions(-) diff --git a/compress_notjs.go b/compress_notjs.go index 7c6b2fc0..a61b7ba4 100644 --- a/compress_notjs.go +++ b/compress_notjs.go @@ -3,10 +3,11 @@ package websocket import ( - "compress/flate" "io" "net/http" "sync" + + "github.com/klauspost/compress/flate" ) func (m CompressionMode) opts() *compressionOptions { @@ -45,10 +46,16 @@ type trimLastFourBytesWriter struct { } func (tw *trimLastFourBytesWriter) reset() { - tw.tail = tw.tail[:0] + if tw != nil && tw.tail != nil { + tw.tail = tw.tail[:0] + } } func (tw *trimLastFourBytesWriter) Write(p []byte) (int, error) { + if tw.tail == nil { + tw.tail = make([]byte, 0, 4) + } + extra := len(tw.tail) + len(p) - 4 if extra <= 0 { @@ -65,7 +72,10 @@ func (tw *trimLastFourBytesWriter) Write(p []byte) (int, error) { if err != nil { return 0, err } - tw.tail = tw.tail[extra:] + + // Shift remaining bytes in tail over. + n := copy(tw.tail, tw.tail[extra:]) + tw.tail = tw.tail[:n] } // If p is less than or equal to 4 bytes, @@ -118,22 +128,32 @@ type slidingWindow struct { buf []byte } -var swPoolMu sync.Mutex +var swPoolMu sync.RWMutex var swPool = map[int]*sync.Pool{} -func (sw *slidingWindow) init(n int) { - if sw.buf != nil { - return +func slidingWindowPool(n int) *sync.Pool { + swPoolMu.RLock() + p, ok := swPool[n] + swPoolMu.RUnlock() + if ok { + return p } + p = &sync.Pool{} + swPoolMu.Lock() - defer swPoolMu.Unlock() + swPool[n] = p + swPoolMu.Unlock() - p, ok := swPool[n] - if !ok { - p = &sync.Pool{} - swPool[n] = p + return p +} + +func (sw *slidingWindow) init(n int) { + if sw.buf != nil { + return } + + p := slidingWindowPool(n) buf, ok := p.Get().([]byte) if ok { sw.buf = buf[:0] diff --git a/conn_notjs.go b/conn_notjs.go index 178fcad0..e6ff7df3 100644 --- a/conn_notjs.go +++ b/conn_notjs.go @@ -39,16 +39,17 @@ type Conn struct { // Read state. readMu *mu - readHeader header + readHeaderBuf [8]byte readControlBuf [maxControlPayload]byte msgReader *msgReader readCloseFrameErr error // Write state. - msgWriter *msgWriter - writeFrameMu *mu - writeBuf []byte - writeHeader header + msgWriterState *msgWriterState + writeFrameMu *mu + writeBuf []byte + writeHeaderBuf [8]byte + writeHeader header closed chan struct{} closeMu sync.Mutex @@ -94,14 +95,14 @@ func newConn(cfg connConfig) *Conn { c.msgReader = newMsgReader(c) - c.msgWriter = newMsgWriter(c) + c.msgWriterState = newMsgWriterState(c) if c.client { c.writeBuf = extractBufioWriterBuf(c.bw, c.rwc) } if c.flate() && c.flateThreshold == 0 { c.flateThreshold = 256 - if !c.msgWriter.flateContextTakeover() { + if !c.msgWriterState.flateContextTakeover() { c.flateThreshold = 512 } } @@ -142,7 +143,7 @@ func (c *Conn) close(err error) { c.writeFrameMu.Lock(context.Background()) putBufioWriter(c.bw) } - c.msgWriter.close() + c.msgWriterState.close() c.msgReader.close() if c.client { diff --git a/conn_test.go b/conn_test.go index 265156e9..398ffd51 100644 --- a/conn_test.go +++ b/conn_test.go @@ -5,7 +5,6 @@ package websocket_test import ( "bytes" "context" - "crypto/rand" "fmt" "io" "io/ioutil" @@ -13,6 +12,7 @@ import ( "net/http/httptest" "os" "os/exec" + "strings" "sync" "testing" "time" @@ -379,15 +379,15 @@ func BenchmarkConn(b *testing.B) { mode websocket.CompressionMode }{ { - name: "compressionDisabled", + name: "disabledCompress", mode: websocket.CompressionDisabled, }, { - name: "compression", + name: "compress", mode: websocket.CompressionContextTakeover, }, { - name: "noContextCompression", + name: "compressNoContext", mode: websocket.CompressionNoContextTakeover, }, } @@ -395,44 +395,36 @@ func BenchmarkConn(b *testing.B) { b.Run(bc.name, func(b *testing.B) { bb, c1, c2 := newConnTest(b, &websocket.DialOptions{ CompressionOptions: &websocket.CompressionOptions{Mode: bc.mode}, - }, nil) + }, &websocket.AcceptOptions{ + CompressionOptions: &websocket.CompressionOptions{Mode: bc.mode}, + }) defer bb.cleanup() bb.goEchoLoop(c2) - const n = 32768 - writeBuf := make([]byte, n) - readBuf := make([]byte, n) - writes := make(chan websocket.MessageType) + msg := []byte(strings.Repeat("1234", 128)) + readBuf := make([]byte, len(msg)) + writes := make(chan struct{}) defer close(writes) werrs := make(chan error) go func() { - for typ := range writes { - werrs <- c1.Write(bb.ctx, typ, writeBuf) + for range writes { + werrs <- c1.Write(bb.ctx, websocket.MessageText, msg) } }() - b.SetBytes(n) + b.SetBytes(int64(len(msg))) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - _, err := rand.Reader.Read(writeBuf) - if err != nil { - b.Fatal(err) - } - - expType := websocket.MessageBinary - if writeBuf[0]%2 == 1 { - expType = websocket.MessageText - } - writes <- expType + writes <- struct{}{} typ, r, err := c1.Reader(bb.ctx) if err != nil { b.Fatal(err) } - if expType != typ { - assert.Equal(b, "data type", expType, typ) + if websocket.MessageText != typ { + assert.Equal(b, "data type", websocket.MessageText, typ) } _, err = io.ReadFull(r, readBuf) @@ -448,8 +440,8 @@ func BenchmarkConn(b *testing.B) { assert.Equal(b, "n2", 0, n2) } - if !bytes.Equal(writeBuf, readBuf) { - assert.Equal(b, "msg", writeBuf, readBuf) + if !bytes.Equal(msg, readBuf) { + assert.Equal(b, "msg", msg, readBuf) } err = <-werrs @@ -464,3 +456,8 @@ func BenchmarkConn(b *testing.B) { }) } } + +func TestCompression(t *testing.T) { + t.Parallel() + +} diff --git a/frame.go b/frame.go index 491ae75c..4acaecf4 100644 --- a/frame.go +++ b/frame.go @@ -3,9 +3,12 @@ package websocket import ( "bufio" "encoding/binary" + "io" "math" "math/bits" + "golang.org/x/xerrors" + "nhooyr.io/websocket/internal/errd" ) @@ -46,12 +49,12 @@ type header struct { // readFrameHeader reads a header from the reader. // See https://tools.ietf.org/html/rfc6455#section-5.2. -func readFrameHeader(h *header, r *bufio.Reader) (err error) { +func readFrameHeader(r *bufio.Reader, readBuf []byte) (h header, err error) { defer errd.Wrap(&err, "failed to read frame header") b, err := r.ReadByte() if err != nil { - return err + return header{}, err } h.fin = b&(1<<7) != 0 @@ -63,7 +66,7 @@ func readFrameHeader(h *header, r *bufio.Reader) (err error) { b, err = r.ReadByte() if err != nil { - return err + return header{}, err } h.masked = b&(1<<7) != 0 @@ -73,24 +76,29 @@ func readFrameHeader(h *header, r *bufio.Reader) (err error) { case payloadLength < 126: h.payloadLength = int64(payloadLength) case payloadLength == 126: - var pl uint16 - err = binary.Read(r, binary.BigEndian, &pl) - h.payloadLength = int64(pl) + _, err = io.ReadFull(r, readBuf[:2]) + h.payloadLength = int64(binary.BigEndian.Uint16(readBuf)) case payloadLength == 127: - err = binary.Read(r, binary.BigEndian, &h.payloadLength) + _, err = io.ReadFull(r, readBuf) + h.payloadLength = int64(binary.BigEndian.Uint64(readBuf)) } if err != nil { - return err + return header{}, err + } + + if h.payloadLength < 0 { + return header{}, xerrors.Errorf("received negative payload length: %v", h.payloadLength) } if h.masked { - err = binary.Read(r, binary.LittleEndian, &h.maskKey) + _, err = io.ReadFull(r, readBuf[:4]) if err != nil { - return err + return header{}, err } + h.maskKey = binary.LittleEndian.Uint32(readBuf) } - return nil + return h, nil } // maxControlPayload is the maximum length of a control frame payload. @@ -99,7 +107,7 @@ const maxControlPayload = 125 // writeFrameHeader writes the bytes of the header to w. // See https://tools.ietf.org/html/rfc6455#section-5.2 -func writeFrameHeader(h header, w *bufio.Writer) (err error) { +func writeFrameHeader(h header, w *bufio.Writer, buf []byte) (err error) { defer errd.Wrap(&err, "failed to write frame header") var b byte @@ -143,16 +151,19 @@ func writeFrameHeader(h header, w *bufio.Writer) (err error) { switch { case h.payloadLength > math.MaxUint16: - err = binary.Write(w, binary.BigEndian, h.payloadLength) + binary.BigEndian.PutUint64(buf, uint64(h.payloadLength)) + _, err = w.Write(buf) case h.payloadLength > 125: - err = binary.Write(w, binary.BigEndian, uint16(h.payloadLength)) + binary.BigEndian.PutUint16(buf, uint16(h.payloadLength)) + _, err = w.Write(buf[:2]) } if err != nil { return err } if h.masked { - err = binary.Write(w, binary.LittleEndian, h.maskKey) + binary.LittleEndian.PutUint32(buf, h.maskKey) + _, err = w.Write(buf[:4]) if err != nil { return err } diff --git a/frame_test.go b/frame_test.go index 38f1599a..76826248 100644 --- a/frame_test.go +++ b/frame_test.go @@ -80,14 +80,13 @@ func testHeader(t *testing.T, h header) { w := bufio.NewWriter(b) r := bufio.NewReader(b) - err := writeFrameHeader(h, w) + err := writeFrameHeader(h, w, make([]byte, 8)) assert.Success(t, err) err = w.Flush() assert.Success(t, err) - var h2 header - err = readFrameHeader(&h2, r) + h2, err := readFrameHeader(r, make([]byte, 8)) assert.Success(t, err) assert.Equal(t, "read header", h, h2) diff --git a/go.mod b/go.mod index cb372391..a10c7b1e 100644 --- a/go.mod +++ b/go.mod @@ -9,6 +9,7 @@ require ( github.com/golang/protobuf v1.3.3 github.com/google/go-cmp v0.4.0 github.com/gorilla/websocket v1.4.1 + github.com/klauspost/compress v1.10.0 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 ) diff --git a/go.sum b/go.sum index 8cbc66ce..e4bbd62d 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/klauspost/compress v1.10.0 h1:92XGj1AcYzA6UrVdd4qIIBrT8OroryvRvdmg/IfmC7Y= +github.com/klauspost/compress v1.10.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= diff --git a/internal/xsync/go.go b/internal/xsync/go.go index d88ac622..712739aa 100644 --- a/internal/xsync/go.go +++ b/internal/xsync/go.go @@ -6,7 +6,7 @@ import ( // Go allows running a function in another goroutine // and waiting for its error. -func Go(fn func() error) <- chan error { +func Go(fn func() error) <-chan error { errs := make(chan error, 1) go func() { defer func() { diff --git a/read.go b/read.go index bf7fa6d9..bbad30d1 100644 --- a/read.go +++ b/read.go @@ -3,6 +3,7 @@ package websocket import ( + "bufio" "context" "io" "io/ioutil" @@ -81,8 +82,9 @@ func newMsgReader(c *Conn) *msgReader { c: c, fin: true, } + mr.readFunc = mr.read - mr.limitReader = newLimitReader(c, readerFunc(mr.read), defaultReadLimit+1) + mr.limitReader = newLimitReader(c, mr.readFunc, defaultReadLimit+1) return mr } @@ -90,13 +92,16 @@ func (mr *msgReader) resetFlate() { if mr.flateContextTakeover() { mr.dict.init(32768) } + if mr.flateBufio == nil { + mr.flateBufio = getBufioReader(mr.readFunc) + } - mr.flateReader = getFlateReader(readerFunc(mr.read), mr.dict.buf) + mr.flateReader = getFlateReader(mr.flateBufio, mr.dict.buf) mr.limitReader.r = mr.flateReader mr.flateTail.Reset(deflateMessageTail) } -func (mr *msgReader) returnFlateReader() { +func (mr *msgReader) putFlateReader() { if mr.flateReader != nil { putFlateReader(mr.flateReader) mr.flateReader = nil @@ -105,9 +110,11 @@ func (mr *msgReader) returnFlateReader() { func (mr *msgReader) close() { mr.c.readMu.Lock(context.Background()) - mr.returnFlateReader() - + mr.putFlateReader() mr.dict.close() + if mr.flateBufio != nil { + putBufioReader(mr.flateBufio) + } } func (mr *msgReader) flateContextTakeover() bool { @@ -173,7 +180,7 @@ func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { case c.readTimeout <- ctx: } - err := readFrameHeader(&c.readHeader, c.br) + h, err := readFrameHeader(c.br, c.readHeaderBuf[:]) if err != nil { select { case <-c.closed: @@ -192,7 +199,7 @@ func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { case c.readTimeout <- context.Background(): } - return c.readHeader, nil + return h, nil } func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { @@ -317,6 +324,7 @@ type msgReader struct { ctx context.Context flate bool flateReader io.Reader + flateBufio *bufio.Reader flateTail strings.Reader limitReader *limitReader dict slidingWindow @@ -324,12 +332,15 @@ type msgReader struct { fin bool payloadLength int64 maskKey uint32 + + // readerFunc(mr.Read) to avoid continuous allocations. + readFunc readerFunc } func (mr *msgReader) reset(ctx context.Context, h header) { mr.ctx = ctx mr.flate = h.rsv1 - mr.limitReader.reset(readerFunc(mr.read)) + mr.limitReader.reset(mr.readFunc) if mr.flate { mr.resetFlate() @@ -346,15 +357,15 @@ func (mr *msgReader) setFrame(h header) { func (mr *msgReader) Read(p []byte) (n int, err error) { defer func() { - errd.Wrap(&err, "failed to read") if xerrors.Is(err, io.ErrUnexpectedEOF) && mr.fin && mr.flate { err = io.EOF } if xerrors.Is(err, io.EOF) { err = io.EOF - - mr.returnFlateReader() + mr.putFlateReader() + return } + errd.Wrap(&err, "failed to read") }() err = mr.c.readMu.Lock(mr.ctx) @@ -372,44 +383,46 @@ func (mr *msgReader) Read(p []byte) (n int, err error) { } func (mr *msgReader) read(p []byte) (int, error) { - if mr.payloadLength == 0 { - if mr.fin { - if mr.flate { - return mr.flateTail.Read(p) + for { + if mr.payloadLength == 0 { + if mr.fin { + if mr.flate { + return mr.flateTail.Read(p) + } + return 0, io.EOF } - return 0, io.EOF - } - h, err := mr.c.readLoop(mr.ctx) - if err != nil { - return 0, err - } - if h.opcode != opContinuation { - err := xerrors.New("received new data message without finishing the previous message") - mr.c.writeError(StatusProtocolError, err) - return 0, err + h, err := mr.c.readLoop(mr.ctx) + if err != nil { + return 0, err + } + if h.opcode != opContinuation { + err := xerrors.New("received new data message without finishing the previous message") + mr.c.writeError(StatusProtocolError, err) + return 0, err + } + mr.setFrame(h) + + continue } - mr.setFrame(h) - return mr.read(p) - } + if int64(len(p)) > mr.payloadLength { + p = p[:mr.payloadLength] + } - if int64(len(p)) > mr.payloadLength { - p = p[:mr.payloadLength] - } + n, err := mr.c.readFramePayload(mr.ctx, p) + if err != nil { + return n, err + } - n, err := mr.c.readFramePayload(mr.ctx, p) - if err != nil { - return n, err - } + mr.payloadLength -= int64(n) - mr.payloadLength -= int64(n) + if !mr.c.client { + mr.maskKey = mask(mr.maskKey, p) + } - if !mr.c.client { - mr.maskKey = mask(mr.maskKey, p) + return n, nil } - - return n, nil } type limitReader struct { diff --git a/write.go b/write.go index 9d4b670f..ec3b7d05 100644 --- a/write.go +++ b/write.go @@ -4,7 +4,6 @@ package websocket import ( "bufio" - "compress/flate" "context" "crypto/rand" "encoding/binary" @@ -12,6 +11,8 @@ import ( "sync" "time" + "github.com/klauspost/compress/flate" + kflate "github.com/klauspost/compress/flate" "golang.org/x/xerrors" "nhooyr.io/websocket/internal/errd" @@ -24,8 +25,6 @@ import ( // // Only one writer can be open at a time, multiple calls will block until the previous writer // is closed. -// -// Never close the returned writer twice. func (c *Conn) Writer(ctx context.Context, typ MessageType) (io.WriteCloser, error) { w, err := c.writer(ctx, typ) if err != nil { @@ -49,6 +48,26 @@ func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { } type msgWriter struct { + mw *msgWriterState + closed bool +} + +func (mw *msgWriter) Write(p []byte) (int, error) { + if mw.closed { + return 0, xerrors.New("cannot use closed writer") + } + return mw.mw.Write(p) +} + +func (mw *msgWriter) Close() error { + if mw.closed { + return xerrors.New("cannot use closed writer") + } + mw.closed = true + return mw.mw.Close() +} + +type msgWriterState struct { c *Conn mu *mu @@ -56,36 +75,42 @@ type msgWriter struct { ctx context.Context opcode opcode - closed bool flate bool trimWriter *trimLastFourBytesWriter flateWriter *flate.Writer + dict slidingWindow } -func newMsgWriter(c *Conn) *msgWriter { - mw := &msgWriter{ +func newMsgWriterState(c *Conn) *msgWriterState { + mw := &msgWriterState{ c: c, mu: newMu(c), } return mw } -func (mw *msgWriter) ensureFlate() { +const stateless = true + +func (mw *msgWriterState) ensureFlate() { if mw.trimWriter == nil { mw.trimWriter = &trimLastFourBytesWriter{ w: writerFunc(mw.write), } } - if mw.flateWriter == nil { - mw.flateWriter = getFlateWriter(mw.trimWriter) + if stateless { + mw.dict.init(8192) + } else { + if mw.flateWriter == nil { + mw.flateWriter = getFlateWriter(mw.trimWriter) + } } mw.flate = true } -func (mw *msgWriter) flateContextTakeover() bool { +func (mw *msgWriterState) flateContextTakeover() bool { if mw.c.client { return !mw.c.copts.clientNoContextTakeover } @@ -93,11 +118,14 @@ func (mw *msgWriter) flateContextTakeover() bool { } func (c *Conn) writer(ctx context.Context, typ MessageType) (io.WriteCloser, error) { - err := c.msgWriter.reset(ctx, typ) + err := c.msgWriterState.reset(ctx, typ) if err != nil { return nil, err } - return c.msgWriter, nil + return &msgWriter{ + mw: c.msgWriterState, + closed: false, + }, nil } func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error) { @@ -107,8 +135,8 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error } if !c.flate() { - defer c.msgWriter.mu.Unlock() - return c.writeFrame(ctx, true, false, c.msgWriter.opcode, p) + defer c.msgWriterState.mu.Unlock() + return c.writeFrame(ctx, true, false, c.msgWriterState.opcode, p) } n, err := mw.Write(p) @@ -120,25 +148,22 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error return n, err } -func (mw *msgWriter) reset(ctx context.Context, typ MessageType) error { +func (mw *msgWriterState) reset(ctx context.Context, typ MessageType) error { err := mw.mu.Lock(ctx) if err != nil { return err } - mw.closed = false mw.ctx = ctx mw.opcode = opcode(typ) mw.flate = false - if mw.trimWriter != nil { - mw.trimWriter.reset() - } + mw.trimWriter.reset() return nil } -func (mw *msgWriter) returnFlateWriter() { +func (mw *msgWriterState) putFlateWriter() { if mw.flateWriter != nil { putFlateWriter(mw.flateWriter) mw.flateWriter = nil @@ -146,16 +171,12 @@ func (mw *msgWriter) returnFlateWriter() { } // Write writes the given bytes to the WebSocket connection. -func (mw *msgWriter) Write(p []byte) (_ int, err error) { +func (mw *msgWriterState) Write(p []byte) (_ int, err error) { defer errd.Wrap(&err, "failed to write") mw.writeMu.Lock() defer mw.writeMu.Unlock() - if mw.closed { - return 0, xerrors.New("cannot use closed writer") - } - if mw.c.flate() { // Only enables flate if the length crosses the // threshold on the first frame @@ -165,13 +186,21 @@ func (mw *msgWriter) Write(p []byte) (_ int, err error) { } if mw.flate { + if stateless { + err = kflate.StatelessDeflate(mw.trimWriter, p, false, mw.dict.buf) + if err != nil { + return 0, err + } + mw.dict.write(p) + return len(p), nil + } return mw.flateWriter.Write(p) } return mw.write(p) } -func (mw *msgWriter) write(p []byte) (int, error) { +func (mw *msgWriterState) write(p []byte) (int, error) { n, err := mw.c.writeFrame(mw.ctx, false, mw.flate, mw.opcode, p) if err != nil { return n, xerrors.Errorf("failed to write data frame: %w", err) @@ -181,42 +210,36 @@ func (mw *msgWriter) write(p []byte) (int, error) { } // Close flushes the frame to the connection. -func (mw *msgWriter) Close() (err error) { +func (mw *msgWriterState) Close() (err error) { defer errd.Wrap(&err, "failed to close writer") mw.writeMu.Lock() defer mw.writeMu.Unlock() - if mw.closed { - return xerrors.New("cannot use closed writer") - } - - if mw.flate { + if mw.flate && !stateless { err = mw.flateWriter.Flush() if err != nil { - return xerrors.Errorf("failed to flush flate writer: %w", err) + return xerrors.Errorf("failed to flush flate: %w", err) } } - // We set closed after flushing the flate writer to ensure Write - // can succeed. - mw.closed = true - _, err = mw.c.writeFrame(mw.ctx, true, mw.flate, mw.opcode, nil) if err != nil { return xerrors.Errorf("failed to write fin frame: %w", err) } if mw.flate && !mw.flateContextTakeover() { - mw.returnFlateWriter() + mw.dict.close() + mw.putFlateWriter() } mw.mu.Unlock() return nil } -func (mw *msgWriter) close() { +func (mw *msgWriterState) close() { mw.writeMu.Lock() - mw.returnFlateWriter() + mw.putFlateWriter() + mw.dict.close() } func (c *Conn) writeControl(ctx context.Context, opcode opcode, p []byte) error { @@ -250,10 +273,11 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco if c.client { c.writeHeader.masked = true - err = binary.Read(rand.Reader, binary.LittleEndian, &c.writeHeader.maskKey) + _, err = io.ReadFull(rand.Reader, c.writeHeaderBuf[:4]) if err != nil { return 0, xerrors.Errorf("failed to generate masking key: %w", err) } + c.writeHeader.maskKey = binary.LittleEndian.Uint32(c.writeHeaderBuf[:]) } c.writeHeader.rsv1 = false @@ -261,7 +285,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco c.writeHeader.rsv1 = true } - err = writeFrameHeader(c.writeHeader, c.bw) + err = writeFrameHeader(c.writeHeader, c.bw, c.writeHeaderBuf[:]) if err != nil { return 0, err } From d57b25304679bcde2d1fa519f9bf569917a40762 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 15 Feb 2020 21:15:43 -0500 Subject: [PATCH 267/519] Report how efficient compression is in BenchmarkConn --- ci/test.mk | 2 +- compress_notjs.go | 16 ----------- conn_test.go | 6 +++++ export_test.go | 22 +++++++++++++++ internal/test/assert/assert.go | 6 ++--- write.go | 49 +++++++--------------------------- 6 files changed, 41 insertions(+), 60 deletions(-) create mode 100644 export_test.go diff --git a/ci/test.mk b/ci/test.mk index 3fc34bbf..3d1f0ed1 100644 --- a/ci/test.mk +++ b/ci/test.mk @@ -1,4 +1,4 @@ -test: gotest ci/out/coverage.html +test: ci/out/coverage.html ifdef CI test: coveralls endif diff --git a/compress_notjs.go b/compress_notjs.go index a61b7ba4..a6911056 100644 --- a/compress_notjs.go +++ b/compress_notjs.go @@ -108,22 +108,6 @@ func putFlateReader(fr io.Reader) { flateReaderPool.Put(fr) } -var flateWriterPool sync.Pool - -func getFlateWriter(w io.Writer) *flate.Writer { - fw, ok := flateWriterPool.Get().(*flate.Writer) - if !ok { - fw, _ = flate.NewWriter(w, flate.BestSpeed) - return fw - } - fw.Reset(w) - return fw -} - -func putFlateWriter(w *flate.Writer) { - flateWriterPool.Put(w) -} - type slidingWindow struct { buf []byte } diff --git a/conn_test.go b/conn_test.go index 398ffd51..3b7fcdb5 100644 --- a/conn_test.go +++ b/conn_test.go @@ -402,6 +402,9 @@ func BenchmarkConn(b *testing.B) { bb.goEchoLoop(c2) + bytesWritten := c1.RecordBytesWritten() + bytesRead := c1.RecordBytesRead() + msg := []byte(strings.Repeat("1234", 128)) readBuf := make([]byte, len(msg)) writes := make(chan struct{}) @@ -451,6 +454,9 @@ func BenchmarkConn(b *testing.B) { } b.StopTimer() + b.ReportMetric(float64(*bytesWritten/b.N), "written/op") + b.ReportMetric(float64(*bytesRead/b.N), "read/op") + err := c1.Close(websocket.StatusNormalClosure, "") assert.Success(b, err) }) diff --git a/export_test.go b/export_test.go new file mode 100644 index 00000000..88b82c9f --- /dev/null +++ b/export_test.go @@ -0,0 +1,22 @@ +// +build !js + +package websocket + +func (c *Conn) RecordBytesWritten() *int { + var bytesWritten int + c.bw.Reset(writerFunc(func(p []byte) (int, error) { + bytesWritten += len(p) + return c.rwc.Write(p) + })) + return &bytesWritten +} + +func (c *Conn) RecordBytesRead() *int { + var bytesRead int + c.br.Reset(readerFunc(func(p []byte) (int, error) { + n, err := c.rwc.Read(p) + bytesRead += n + return n, err + })) + return &bytesRead +} diff --git a/internal/test/assert/assert.go b/internal/test/assert/assert.go index 2bc01dba..602b887e 100644 --- a/internal/test/assert/assert.go +++ b/internal/test/assert/assert.go @@ -39,8 +39,8 @@ func Error(t testing.TB, err error) { func Contains(t testing.TB, v interface{}, sub string) { t.Helper() - vstr := fmt.Sprint(v) - if !strings.Contains(vstr, sub) { - t.Fatalf("expected %q to contain %q", vstr, sub) + s := fmt.Sprint(v) + if !strings.Contains(s, sub) { + t.Fatalf("expected %q to contain %q", s, sub) } } diff --git a/write.go b/write.go index ec3b7d05..b560b44c 100644 --- a/write.go +++ b/write.go @@ -12,7 +12,6 @@ import ( "time" "github.com/klauspost/compress/flate" - kflate "github.com/klauspost/compress/flate" "golang.org/x/xerrors" "nhooyr.io/websocket/internal/errd" @@ -77,9 +76,8 @@ type msgWriterState struct { opcode opcode flate bool - trimWriter *trimLastFourBytesWriter - flateWriter *flate.Writer - dict slidingWindow + trimWriter *trimLastFourBytesWriter + dict slidingWindow } func newMsgWriterState(c *Conn) *msgWriterState { @@ -90,8 +88,6 @@ func newMsgWriterState(c *Conn) *msgWriterState { return mw } -const stateless = true - func (mw *msgWriterState) ensureFlate() { if mw.trimWriter == nil { mw.trimWriter = &trimLastFourBytesWriter{ @@ -99,14 +95,7 @@ func (mw *msgWriterState) ensureFlate() { } } - if stateless { - mw.dict.init(8192) - } else { - if mw.flateWriter == nil { - mw.flateWriter = getFlateWriter(mw.trimWriter) - } - } - + mw.dict.init(8192) mw.flate = true } @@ -163,13 +152,6 @@ func (mw *msgWriterState) reset(ctx context.Context, typ MessageType) error { return nil } -func (mw *msgWriterState) putFlateWriter() { - if mw.flateWriter != nil { - putFlateWriter(mw.flateWriter) - mw.flateWriter = nil - } -} - // Write writes the given bytes to the WebSocket connection. func (mw *msgWriterState) Write(p []byte) (_ int, err error) { defer errd.Wrap(&err, "failed to write") @@ -186,15 +168,12 @@ func (mw *msgWriterState) Write(p []byte) (_ int, err error) { } if mw.flate { - if stateless { - err = kflate.StatelessDeflate(mw.trimWriter, p, false, mw.dict.buf) - if err != nil { - return 0, err - } - mw.dict.write(p) - return len(p), nil + err = flate.StatelessDeflate(mw.trimWriter, p, false, mw.dict.buf) + if err != nil { + return 0, err } - return mw.flateWriter.Write(p) + mw.dict.write(p) + return len(p), nil } return mw.write(p) @@ -216,13 +195,6 @@ func (mw *msgWriterState) Close() (err error) { mw.writeMu.Lock() defer mw.writeMu.Unlock() - if mw.flate && !stateless { - err = mw.flateWriter.Flush() - if err != nil { - return xerrors.Errorf("failed to flush flate: %w", err) - } - } - _, err = mw.c.writeFrame(mw.ctx, true, mw.flate, mw.opcode, nil) if err != nil { return xerrors.Errorf("failed to write fin frame: %w", err) @@ -230,7 +202,6 @@ func (mw *msgWriterState) Close() (err error) { if mw.flate && !mw.flateContextTakeover() { mw.dict.close() - mw.putFlateWriter() } mw.mu.Unlock() return nil @@ -238,7 +209,6 @@ func (mw *msgWriterState) Close() (err error) { func (mw *msgWriterState) close() { mw.writeMu.Lock() - mw.putFlateWriter() mw.dict.close() } @@ -311,14 +281,13 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco return n, nil } -func (c *Conn) writeFramePayload(p []byte) (_ int, err error) { +func (c *Conn) writeFramePayload(p []byte) (n int, err error) { defer errd.Wrap(&err, "failed to write frame payload") if !c.writeHeader.masked { return c.bw.Write(p) } - var n int maskKey := c.writeHeader.maskKey for len(p) > 0 { // If the buffer is full, we need to flush. From 1bc100d26f19edced3ad5c6d2853c1241211a766 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 15 Feb 2020 21:54:57 -0500 Subject: [PATCH 268/519] Update docs and random little issues --- .github/workflows/ci.yml | 6 +++--- README.md | 8 +++++--- accept.go | 22 +++++++++++++--------- accept_js.go | 7 ++++--- close_notjs.go | 6 ++++-- compress.go | 17 +---------------- conn_notjs.go | 2 +- conn_test.go | 17 ++++++++--------- dial.go | 23 ++++++++++++++--------- doc.go | 5 +++-- ws_js.go | 5 +++++ 11 files changed, 61 insertions(+), 57 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 074e5246..4534425f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,7 @@ jobs: key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - - name: make fmt + - name: Run make fmt uses: ./ci/image with: args: make fmt @@ -27,7 +27,7 @@ jobs: key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - - name: make lint + - name: Run make lint uses: ./ci/image with: args: make lint @@ -42,7 +42,7 @@ jobs: key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - - name: make test + - name: Run make test uses: ./ci/image with: args: make test diff --git a/README.md b/README.md index 2569383a..631a14c9 100644 --- a/README.md +++ b/README.md @@ -17,8 +17,8 @@ go get nhooyr.io/websocket - Minimal and idiomatic API - First class [context.Context](https://blog.golang.org/context) support -- Thorough tests, fully passes the [autobahn-testsuite](https://github.com/crossbario/autobahn-testsuite) -- [Zero dependencies](https://godoc.org/nhooyr.io/websocket?imports) +- Thorough tests, fully passes the WebSocket [autobahn-testsuite](https://github.com/crossbario/autobahn-testsuite) +- [Minimal dependencies](https://godoc.org/nhooyr.io/websocket?imports) - JSON and protobuf helpers in the [wsjson](https://godoc.org/nhooyr.io/websocket/wsjson) and [wspb](https://godoc.org/nhooyr.io/websocket/wspb) subpackages - Zero alloc reads and writes - Concurrent writes @@ -34,7 +34,7 @@ go get nhooyr.io/websocket ## Examples -For a production quality example that demonstrates the full API, see the [echo example](https://godoc.org/nhooyr.io/websocket#example-package--Echo). +For a production quality example that demonstrates the complete API, see the [echo example](https://godoc.org/nhooyr.io/websocket#example-package--Echo). ### Server @@ -111,6 +111,8 @@ Advantages of nhooyr.io/websocket: - Gorilla's implementation is slower and uses [unsafe](https://golang.org/pkg/unsafe/). - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support - Gorilla only supports no context takeover mode + - Uses [klauspost/compress](https://github.com/klauspost/compress) for optimized compression + - See [gorilla/websocket#203](https://github.com/gorilla/websocket/issues/203) - [CloseRead](https://godoc.org/nhooyr.io/websocket#Conn.CloseRead) helper ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492)) - Actively maintained ([gorilla/websocket#370](https://github.com/gorilla/websocket/issues/370)) diff --git a/accept.go b/accept.go index cc9babb0..75d6d643 100644 --- a/accept.go +++ b/accept.go @@ -37,9 +37,17 @@ type AcceptOptions struct { // If used incorrectly your WebSocket server will be open to CSRF attacks. InsecureSkipVerify bool - // CompressionOptions controls the compression options. - // See docs on the CompressionOptions type. - CompressionOptions *CompressionOptions + // CompressionMode controls the compression mode. + // Defaults to CompressionNoContextTakeover. + // + // See docs on CompressionMode for details. + CompressionMode CompressionMode + + // CompressionThreshold controls the minimum size of a message before compression is applied. + // + // Defaults to 512 bytes for CompressionNoContextTakeover and 128 bytes + // for CompressionContextTakeover. + CompressionThreshold int } // Accept accepts a WebSocket handshake from a client and upgrades the @@ -61,10 +69,6 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con } opts = &*opts - if opts.CompressionOptions == nil { - opts.CompressionOptions = &CompressionOptions{} - } - errCode, err := verifyClientRequest(w, r) if err != nil { http.Error(w, err.Error(), errCode) @@ -97,7 +101,7 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con w.Header().Set("Sec-WebSocket-Protocol", subproto) } - copts, err := acceptCompression(r, w, opts.CompressionOptions.Mode) + copts, err := acceptCompression(r, w, opts.CompressionMode) if err != nil { return nil, err } @@ -120,7 +124,7 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con rwc: netConn, client: false, copts: copts, - flateThreshold: opts.CompressionOptions.Threshold, + flateThreshold: opts.CompressionThreshold, br: brw.Reader, bw: brw.Writer, diff --git a/accept_js.go b/accept_js.go index efc92817..5db12d7b 100644 --- a/accept_js.go +++ b/accept_js.go @@ -8,9 +8,10 @@ import ( // AcceptOptions represents Accept's options. type AcceptOptions struct { - Subprotocols []string - InsecureSkipVerify bool - CompressionOptions *CompressionOptions + Subprotocols []string + InsecureSkipVerify bool + CompressionMode CompressionMode + CompressionThreshold int } // Accept is stubbed out for Wasm. diff --git a/close_notjs.go b/close_notjs.go index 160a1237..3367ea01 100644 --- a/close_notjs.go +++ b/close_notjs.go @@ -35,7 +35,7 @@ func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { defer errd.Wrap(&err, "failed to close WebSocket") err = c.writeClose(code, reason) - if err != nil && CloseStatus(err) == -1 { + if err != nil && CloseStatus(err) == -1 && err != errAlreadyWroteClose { return err } @@ -46,13 +46,15 @@ func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { return nil } +var errAlreadyWroteClose = xerrors.New("already wrote close") + func (c *Conn) writeClose(code StatusCode, reason string) error { c.closeMu.Lock() closing := c.wroteClose c.wroteClose = true c.closeMu.Unlock() if closing { - return xerrors.New("already wrote close") + return errAlreadyWroteClose } ce := CloseError{ diff --git a/compress.go b/compress.go index 918b3b49..57446d01 100644 --- a/compress.go +++ b/compress.go @@ -1,20 +1,5 @@ package websocket -// CompressionOptions represents the available deflate extension options. -// See https://tools.ietf.org/html/rfc7692 -type CompressionOptions struct { - // Mode controls the compression mode. - // - // See docs on CompressionMode. - Mode CompressionMode - - // Threshold controls the minimum size of a message before compression is applied. - // - // Defaults to 512 bytes for CompressionNoContextTakeover and 256 bytes - // for CompressionContextTakeover. - Threshold int -} - // CompressionMode represents the modes available to the deflate extension. // See https://tools.ietf.org/html/rfc7692 // @@ -38,7 +23,7 @@ const ( // CompressionContextTakeover uses a flate.Reader and flate.Writer per connection. // This enables reusing the sliding window from previous messages. // As most WebSocket protocols are repetitive, this can be very efficient. - // It carries an overhead of 64 kB for every connection compared to CompressionNoContextTakeover. + // It carries an overhead of 8 kB for every connection compared to CompressionNoContextTakeover. // // If the peer negotiates NoContextTakeover on the client or server side, it will be // used instead as this is required by the RFC. diff --git a/conn_notjs.go b/conn_notjs.go index e6ff7df3..8598ded3 100644 --- a/conn_notjs.go +++ b/conn_notjs.go @@ -101,7 +101,7 @@ func newConn(cfg connConfig) *Conn { } if c.flate() && c.flateThreshold == 0 { - c.flateThreshold = 256 + c.flateThreshold = 128 if !c.msgWriterState.flateContextTakeover() { c.flateThreshold = 512 } diff --git a/conn_test.go b/conn_test.go index 3b7fcdb5..7755048c 100644 --- a/conn_test.go +++ b/conn_test.go @@ -36,19 +36,18 @@ func TestConn(t *testing.T) { t.Run("fuzzData", func(t *testing.T) { t.Parallel() - copts := func() *websocket.CompressionOptions { - return &websocket.CompressionOptions{ - Mode: websocket.CompressionMode(xrand.Int(int(websocket.CompressionDisabled) + 1)), - Threshold: xrand.Int(9999), - } + compressionMode := func() websocket.CompressionMode { + return websocket.CompressionMode(xrand.Int(int(websocket.CompressionDisabled) + 1)) } for i := 0; i < 5; i++ { t.Run("", func(t *testing.T) { tt, c1, c2 := newConnTest(t, &websocket.DialOptions{ - CompressionOptions: copts(), + CompressionMode: compressionMode(), + CompressionThreshold: xrand.Int(9999), }, &websocket.AcceptOptions{ - CompressionOptions: copts(), + CompressionMode: compressionMode(), + CompressionThreshold: xrand.Int(9999), }) defer tt.cleanup() @@ -394,9 +393,9 @@ func BenchmarkConn(b *testing.B) { for _, bc := range benchCases { b.Run(bc.name, func(b *testing.B) { bb, c1, c2 := newConnTest(b, &websocket.DialOptions{ - CompressionOptions: &websocket.CompressionOptions{Mode: bc.mode}, + CompressionMode: bc.mode, }, &websocket.AcceptOptions{ - CompressionOptions: &websocket.CompressionOptions{Mode: bc.mode}, + CompressionMode: bc.mode, }) defer bb.cleanup() diff --git a/dial.go b/dial.go index 3e2042e5..09546ac6 100644 --- a/dial.go +++ b/dial.go @@ -33,9 +33,17 @@ type DialOptions struct { // Subprotocols lists the WebSocket subprotocols to negotiate with the server. Subprotocols []string - // CompressionOptions controls the compression options. - // See docs on the CompressionOptions type. - CompressionOptions *CompressionOptions + // CompressionMode controls the compression mode. + // Defaults to CompressionNoContextTakeover. + // + // See docs on CompressionMode for details. + CompressionMode CompressionMode + + // CompressionThreshold controls the minimum size of a message before compression is applied. + // + // Defaults to 512 bytes for CompressionNoContextTakeover and 128 bytes + // for CompressionContextTakeover. + CompressionThreshold int } // Dial performs a WebSocket handshake on url. @@ -67,9 +75,6 @@ func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) ( if opts.HTTPHeader == nil { opts.HTTPHeader = http.Header{} } - if opts.CompressionOptions == nil { - opts.CompressionOptions = &CompressionOptions{} - } secWebSocketKey, err := secWebSocketKey(rand) if err != nil { @@ -107,7 +112,7 @@ func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) ( rwc: rwc, client: true, copts: copts, - flateThreshold: opts.CompressionOptions.Threshold, + flateThreshold: opts.CompressionThreshold, br: getBufioReader(rwc), bw: getBufioWriter(rwc), }), resp, nil @@ -141,8 +146,8 @@ func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, secWe if len(opts.Subprotocols) > 0 { req.Header.Set("Sec-WebSocket-Protocol", strings.Join(opts.Subprotocols, ",")) } - if opts.CompressionOptions.Mode != CompressionDisabled { - copts := opts.CompressionOptions.Mode.opts() + if opts.CompressionMode != CompressionDisabled { + copts := opts.CompressionMode.opts() copts.setHeader(req.Header) } diff --git a/doc.go b/doc.go index c8f5550b..efa920e3 100644 --- a/doc.go +++ b/doc.go @@ -6,7 +6,7 @@ // // Use Dial to dial a WebSocket server. // -// Accept to accept a WebSocket client. +// Use Accept to accept a WebSocket client. // // Conn represents the resulting WebSocket connection. // @@ -25,7 +25,8 @@ // // Some important caveats to be aware of: // +// - Accept always errors out // - Conn.Ping is no-op // - HTTPClient, HTTPHeader and CompressionMode in DialOptions are no-op -// - *http.Response from Dial is &http.Response{} on success +// - *http.Response from Dial is &http.Response{} with a 101 status code on success package websocket // import "nhooyr.io/websocket" diff --git a/ws_js.go b/ws_js.go index 05c4c062..ecf3d78c 100644 --- a/ws_js.go +++ b/ws_js.go @@ -152,6 +152,11 @@ func (c *Conn) read(ctx context.Context) (MessageType, []byte, error) { } } +// Ping is mocked out for Wasm. +func (c *Conn) Ping(ctx context.Context) error { + return nil +} + // Write writes a message of the given type to the connection. // Always non blocking. func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { From d15ccd2494e18cb7dc173048272577374d392d91 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 15 Feb 2020 22:15:57 -0500 Subject: [PATCH 269/519] Simplify README.md badges --- README.md | 7 +++---- ci/test.mk | 1 - 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 631a14c9..d1847fc8 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,7 @@ # websocket -[![release](https://img.shields.io/github/v/release/nhooyr/websocket?color=6b9ded&sort=semver)](https://github.com/nhooyr/websocket/releases) [![godoc](https://godoc.org/nhooyr.io/websocket?status.svg)](https://godoc.org/nhooyr.io/websocket) -[![coverage](https://img.shields.io/coveralls/github/nhooyr/websocket?color=65d6a4)](https://coveralls.io/github/nhooyr/websocket) -[![ci](https://github.com/nhooyr/websocket/workflows/ci/badge.svg)](https://github.com/nhooyr/websocket/actions) +[![ci](https://img.shields.io/github/workflow/status/nhooyr/websocket/ci?label=ci)](https://github.com/nhooyr/websocket/actions) websocket is a minimal and idiomatic WebSocket library for Go. @@ -17,7 +15,8 @@ go get nhooyr.io/websocket - Minimal and idiomatic API - First class [context.Context](https://blog.golang.org/context) support -- Thorough tests, fully passes the WebSocket [autobahn-testsuite](https://github.com/crossbario/autobahn-testsuite) +- Fully passes the WebSocket [autobahn-testsuite](https://github.com/crossbario/autobahn-testsuite) +- Thorough unit tests with [90% coverage](https://coveralls.io/github/nhooyr/websocket) - [Minimal dependencies](https://godoc.org/nhooyr.io/websocket?imports) - JSON and protobuf helpers in the [wsjson](https://godoc.org/nhooyr.io/websocket/wsjson) and [wspb](https://godoc.org/nhooyr.io/websocket/wspb) subpackages - Zero alloc reads and writes diff --git a/ci/test.mk b/ci/test.mk index 3d1f0ed1..c62a25b6 100644 --- a/ci/test.mk +++ b/ci/test.mk @@ -7,7 +7,6 @@ ci/out/coverage.html: gotest go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html coveralls: gotest - # https://github.com/coverallsapp/github-action/blob/master/src/run.ts echo "--- coveralls" goveralls -coverprofile=ci/out/coverage.prof From 22a4006afea03e1b34bd14843b1752d5195f20dc Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 15 Feb 2020 22:31:40 -0500 Subject: [PATCH 270/519] Remove dependency on golang.org/x/xerrors See https://github.com/nhooyr/websocket/issues/182 --- accept.go | 28 ++++++++++++++-------------- accept_js.go | 5 ++--- accept_test.go | 5 ++--- autobahn_test.go | 12 +++++------- close.go | 9 ++++----- close_notjs.go | 20 ++++++++++---------- conn_notjs.go | 18 +++++++++--------- conn_test.go | 5 ++--- dial.go | 32 ++++++++++++++++---------------- example_echo_test.go | 8 ++++---- frame.go | 5 ++--- go.mod | 3 +-- internal/errd/wrap.go | 32 ++------------------------------ internal/test/wstest/echo.go | 7 +++---- internal/test/wstest/pipe.go | 7 +++---- internal/xsync/go.go | 4 ++-- netconn.go | 5 ++--- read.go | 34 +++++++++++++++++----------------- write.go | 21 +++++++++++---------- ws_js.go | 36 ++++++++++++++++++------------------ wsjson/wsjson.go | 9 ++++----- wspb/wspb.go | 8 ++++---- 22 files changed, 137 insertions(+), 176 deletions(-) diff --git a/accept.go b/accept.go index 75d6d643..fda8cdc6 100644 --- a/accept.go +++ b/accept.go @@ -6,14 +6,14 @@ import ( "bytes" "crypto/sha1" "encoding/base64" + "errors" + "fmt" "io" "net/http" "net/textproto" "net/url" "strings" - "golang.org/x/xerrors" - "nhooyr.io/websocket/internal/errd" ) @@ -85,7 +85,7 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con hj, ok := w.(http.Hijacker) if !ok { - err = xerrors.New("http.ResponseWriter does not implement http.Hijacker") + err = errors.New("http.ResponseWriter does not implement http.Hijacker") http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented) return nil, err } @@ -110,7 +110,7 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con netConn, brw, err := hj.Hijack() if err != nil { - err = xerrors.Errorf("failed to hijack connection: %w", err) + err = fmt.Errorf("failed to hijack connection: %w", err) http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return nil, err } @@ -133,32 +133,32 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con func verifyClientRequest(w http.ResponseWriter, r *http.Request) (errCode int, _ error) { if !r.ProtoAtLeast(1, 1) { - return http.StatusUpgradeRequired, xerrors.Errorf("WebSocket protocol violation: handshake request must be at least HTTP/1.1: %q", r.Proto) + return http.StatusUpgradeRequired, fmt.Errorf("WebSocket protocol violation: handshake request must be at least HTTP/1.1: %q", r.Proto) } if !headerContainsToken(r.Header, "Connection", "Upgrade") { w.Header().Set("Connection", "Upgrade") w.Header().Set("Upgrade", "websocket") - return http.StatusUpgradeRequired, xerrors.Errorf("WebSocket protocol violation: Connection header %q does not contain Upgrade", r.Header.Get("Connection")) + return http.StatusUpgradeRequired, fmt.Errorf("WebSocket protocol violation: Connection header %q does not contain Upgrade", r.Header.Get("Connection")) } if !headerContainsToken(r.Header, "Upgrade", "websocket") { w.Header().Set("Connection", "Upgrade") w.Header().Set("Upgrade", "websocket") - return http.StatusUpgradeRequired, xerrors.Errorf("WebSocket protocol violation: Upgrade header %q does not contain websocket", r.Header.Get("Upgrade")) + return http.StatusUpgradeRequired, fmt.Errorf("WebSocket protocol violation: Upgrade header %q does not contain websocket", r.Header.Get("Upgrade")) } if r.Method != "GET" { - return http.StatusMethodNotAllowed, xerrors.Errorf("WebSocket protocol violation: handshake request method is not GET but %q", r.Method) + return http.StatusMethodNotAllowed, fmt.Errorf("WebSocket protocol violation: handshake request method is not GET but %q", r.Method) } if r.Header.Get("Sec-WebSocket-Version") != "13" { w.Header().Set("Sec-WebSocket-Version", "13") - return http.StatusBadRequest, xerrors.Errorf("unsupported WebSocket protocol version (only 13 is supported): %q", r.Header.Get("Sec-WebSocket-Version")) + return http.StatusBadRequest, fmt.Errorf("unsupported WebSocket protocol version (only 13 is supported): %q", r.Header.Get("Sec-WebSocket-Version")) } if r.Header.Get("Sec-WebSocket-Key") == "" { - return http.StatusBadRequest, xerrors.New("WebSocket protocol violation: missing Sec-WebSocket-Key") + return http.StatusBadRequest, errors.New("WebSocket protocol violation: missing Sec-WebSocket-Key") } return 0, nil @@ -169,10 +169,10 @@ func authenticateOrigin(r *http.Request) error { if origin != "" { u, err := url.Parse(origin) if err != nil { - return xerrors.Errorf("failed to parse Origin header %q: %w", origin, err) + return fmt.Errorf("failed to parse Origin header %q: %w", origin, err) } if !strings.EqualFold(u.Host, r.Host) { - return xerrors.Errorf("request Origin %q is not authorized for Host %q", origin, r.Host) + return fmt.Errorf("request Origin %q is not authorized for Host %q", origin, r.Host) } } return nil @@ -223,7 +223,7 @@ func acceptDeflate(w http.ResponseWriter, ext websocketExtension, mode Compressi continue } - err := xerrors.Errorf("unsupported permessage-deflate parameter: %q", p) + err := fmt.Errorf("unsupported permessage-deflate parameter: %q", p) http.Error(w, err.Error(), http.StatusBadRequest) return nil, err } @@ -253,7 +253,7 @@ func acceptWebkitDeflate(w http.ResponseWriter, ext websocketExtension, mode Com // // Either way, we're only implementing this for webkit which never sends the max_window_bits // parameter so we don't need to worry about it. - err := xerrors.Errorf("unsupported x-webkit-deflate-frame parameter: %q", p) + err := fmt.Errorf("unsupported x-webkit-deflate-frame parameter: %q", p) http.Error(w, err.Error(), http.StatusBadRequest) return nil, err } diff --git a/accept_js.go b/accept_js.go index 5db12d7b..724b35b5 100644 --- a/accept_js.go +++ b/accept_js.go @@ -1,9 +1,8 @@ package websocket import ( + "errors" "net/http" - - "golang.org/x/xerrors" ) // AcceptOptions represents Accept's options. @@ -16,5 +15,5 @@ type AcceptOptions struct { // Accept is stubbed out for Wasm. func Accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, error) { - return nil, xerrors.New("unimplemented") + return nil, errors.New("unimplemented") } diff --git a/accept_test.go b/accept_test.go index 53338e17..523d4685 100644 --- a/accept_test.go +++ b/accept_test.go @@ -4,14 +4,13 @@ package websocket import ( "bufio" + "errors" "net" "net/http" "net/http/httptest" "strings" "testing" - "golang.org/x/xerrors" - "nhooyr.io/websocket/internal/test/assert" ) @@ -80,7 +79,7 @@ func TestAccept(t *testing.T) { w := mockHijacker{ ResponseWriter: httptest.NewRecorder(), hijack: func() (conn net.Conn, writer *bufio.ReadWriter, err error) { - return nil, nil, xerrors.New("haha") + return nil, nil, errors.New("haha") }, } diff --git a/autobahn_test.go b/autobahn_test.go index fb24a06b..50473534 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -15,8 +15,6 @@ import ( "testing" "time" - "golang.org/x/xerrors" - "nhooyr.io/websocket" "nhooyr.io/websocket/internal/errd" "nhooyr.io/websocket/internal/test/assert" @@ -108,7 +106,7 @@ func wstestClientServer(ctx context.Context) (url string, closeFn func(), err er "exclude-cases": excludedAutobahnCases, }) if err != nil { - return "", nil, xerrors.Errorf("failed to write spec: %w", err) + return "", nil, fmt.Errorf("failed to write spec: %w", err) } ctx, cancel := context.WithTimeout(context.Background(), time.Minute*15) @@ -126,7 +124,7 @@ func wstestClientServer(ctx context.Context) (url string, closeFn func(), err er wstest := exec.CommandContext(ctx, "wstest", args...) err = wstest.Start() if err != nil { - return "", nil, xerrors.Errorf("failed to start wstest: %w", err) + return "", nil, fmt.Errorf("failed to start wstest: %w", err) } return url, func() { @@ -209,7 +207,7 @@ func unusedListenAddr() (_ string, err error) { func tempJSONFile(v interface{}) (string, error) { f, err := ioutil.TempFile("", "temp.json") if err != nil { - return "", xerrors.Errorf("temp file: %w", err) + return "", fmt.Errorf("temp file: %w", err) } defer f.Close() @@ -217,12 +215,12 @@ func tempJSONFile(v interface{}) (string, error) { e.SetIndent("", "\t") err = e.Encode(v) if err != nil { - return "", xerrors.Errorf("json encode: %w", err) + return "", fmt.Errorf("json encode: %w", err) } err = f.Close() if err != nil { - return "", xerrors.Errorf("close temp file: %w", err) + return "", fmt.Errorf("close temp file: %w", err) } return f.Name(), nil diff --git a/close.go b/close.go index 20073233..7cbc19e9 100644 --- a/close.go +++ b/close.go @@ -1,9 +1,8 @@ package websocket import ( + "errors" "fmt" - - "golang.org/x/xerrors" ) // StatusCode represents a WebSocket status code. @@ -53,7 +52,7 @@ const ( // CloseError is returned when the connection is closed with a status and reason. // -// Use Go 1.13's xerrors.As to check for this error. +// Use Go 1.13's errors.As to check for this error. // Also see the CloseStatus helper. type CloseError struct { Code StatusCode @@ -64,13 +63,13 @@ func (ce CloseError) Error() string { return fmt.Sprintf("status = %v and reason = %q", ce.Code, ce.Reason) } -// CloseStatus is a convenience wrapper around Go 1.13's xerrors.As to grab +// CloseStatus is a convenience wrapper around Go 1.13's errors.As to grab // the status code from a CloseError. // // -1 will be returned if the passed error is nil or not a CloseError. func CloseStatus(err error) StatusCode { var ce CloseError - if xerrors.As(err, &ce) { + if errors.As(err, &ce) { return ce.Code } return -1 diff --git a/close_notjs.go b/close_notjs.go index 3367ea01..c25b088f 100644 --- a/close_notjs.go +++ b/close_notjs.go @@ -5,11 +5,11 @@ package websocket import ( "context" "encoding/binary" + "errors" + "fmt" "log" "time" - "golang.org/x/xerrors" - "nhooyr.io/websocket/internal/errd" ) @@ -46,7 +46,7 @@ func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { return nil } -var errAlreadyWroteClose = xerrors.New("already wrote close") +var errAlreadyWroteClose = errors.New("already wrote close") func (c *Conn) writeClose(code StatusCode, reason string) error { c.closeMu.Lock() @@ -62,7 +62,7 @@ func (c *Conn) writeClose(code StatusCode, reason string) error { Reason: reason, } - c.setCloseErr(xerrors.Errorf("sent close frame: %w", ce)) + c.setCloseErr(fmt.Errorf("sent close frame: %w", ce)) var p []byte var err error @@ -119,7 +119,7 @@ func parseClosePayload(p []byte) (CloseError, error) { } if len(p) < 2 { - return CloseError{}, xerrors.Errorf("close payload %q too small, cannot even contain the 2 byte status code", p) + return CloseError{}, fmt.Errorf("close payload %q too small, cannot even contain the 2 byte status code", p) } ce := CloseError{ @@ -128,7 +128,7 @@ func parseClosePayload(p []byte) (CloseError, error) { } if !validWireCloseCode(ce.Code) { - return CloseError{}, xerrors.Errorf("invalid status code %v", ce.Code) + return CloseError{}, fmt.Errorf("invalid status code %v", ce.Code) } return ce, nil @@ -155,7 +155,7 @@ func validWireCloseCode(code StatusCode) bool { func (ce CloseError) bytes() ([]byte, error) { p, err := ce.bytesErr() if err != nil { - err = xerrors.Errorf("failed to marshal close frame: %w", err) + err = fmt.Errorf("failed to marshal close frame: %w", err) ce = CloseError{ Code: StatusInternalError, } @@ -168,11 +168,11 @@ const maxCloseReason = maxControlPayload - 2 func (ce CloseError) bytesErr() ([]byte, error) { if len(ce.Reason) > maxCloseReason { - return nil, xerrors.Errorf("reason string max is %v but got %q with length %v", maxCloseReason, ce.Reason, len(ce.Reason)) + return nil, fmt.Errorf("reason string max is %v but got %q with length %v", maxCloseReason, ce.Reason, len(ce.Reason)) } if !validWireCloseCode(ce.Code) { - return nil, xerrors.Errorf("status code %v cannot be set", ce.Code) + return nil, fmt.Errorf("status code %v cannot be set", ce.Code) } buf := make([]byte, 2+len(ce.Reason)) @@ -189,7 +189,7 @@ func (c *Conn) setCloseErr(err error) { func (c *Conn) setCloseErrLocked(err error) { if c.closeErr == nil { - c.closeErr = xerrors.Errorf("WebSocket closed: %w", err) + c.closeErr = fmt.Errorf("WebSocket closed: %w", err) } } diff --git a/conn_notjs.go b/conn_notjs.go index 8598ded3..7ee60fbc 100644 --- a/conn_notjs.go +++ b/conn_notjs.go @@ -5,13 +5,13 @@ package websocket import ( "bufio" "context" + "errors" + "fmt" "io" "runtime" "strconv" "sync" "sync/atomic" - - "golang.org/x/xerrors" ) // Conn represents a WebSocket connection. @@ -108,7 +108,7 @@ func newConn(cfg connConfig) *Conn { } runtime.SetFinalizer(c, func(c *Conn) { - c.close(xerrors.New("connection garbage collected")) + c.close(errors.New("connection garbage collected")) }) go c.timeoutLoop() @@ -165,10 +165,10 @@ func (c *Conn) timeoutLoop() { case readCtx = <-c.readTimeout: case <-readCtx.Done(): - c.setCloseErr(xerrors.Errorf("read timed out: %w", readCtx.Err())) - go c.writeError(StatusPolicyViolation, xerrors.New("timed out")) + c.setCloseErr(fmt.Errorf("read timed out: %w", readCtx.Err())) + go c.writeError(StatusPolicyViolation, errors.New("timed out")) case <-writeCtx.Done(): - c.close(xerrors.Errorf("write timed out: %w", writeCtx.Err())) + c.close(fmt.Errorf("write timed out: %w", writeCtx.Err())) return } } @@ -190,7 +190,7 @@ func (c *Conn) Ping(ctx context.Context) error { err := c.ping(ctx, strconv.Itoa(int(p))) if err != nil { - return xerrors.Errorf("failed to ping: %w", err) + return fmt.Errorf("failed to ping: %w", err) } return nil } @@ -217,7 +217,7 @@ func (c *Conn) ping(ctx context.Context, p string) error { case <-c.closed: return c.closeErr case <-ctx.Done(): - err := xerrors.Errorf("failed to wait for pong: %w", ctx.Err()) + err := fmt.Errorf("failed to wait for pong: %w", ctx.Err()) c.close(err) return err case <-pong: @@ -242,7 +242,7 @@ func (m *mu) Lock(ctx context.Context) error { case <-m.c.closed: return m.c.closeErr case <-ctx.Done(): - err := xerrors.Errorf("failed to acquire lock: %w", ctx.Err()) + err := fmt.Errorf("failed to acquire lock: %w", ctx.Err()) m.c.close(err) return err case m.ch <- struct{}{}: diff --git a/conn_test.go b/conn_test.go index 7755048c..c68a25f6 100644 --- a/conn_test.go +++ b/conn_test.go @@ -19,7 +19,6 @@ import ( "github.com/golang/protobuf/ptypes" "github.com/golang/protobuf/ptypes/duration" - "golang.org/x/xerrors" "nhooyr.io/websocket" "nhooyr.io/websocket/internal/test/assert" @@ -289,10 +288,10 @@ func TestWasm(t *testing.T) { func assertCloseStatus(exp websocket.StatusCode, err error) error { if websocket.CloseStatus(err) == -1 { - return xerrors.Errorf("expected websocket.CloseError: %T %v", err, err) + return fmt.Errorf("expected websocket.CloseError: %T %v", err, err) } if websocket.CloseStatus(err) != exp { - return xerrors.Errorf("expected close status %v but got ", exp, err) + return fmt.Errorf("expected close status %v but got %v", exp, err) } return nil } diff --git a/dial.go b/dial.go index 09546ac6..6e37a589 100644 --- a/dial.go +++ b/dial.go @@ -8,6 +8,8 @@ import ( "context" "crypto/rand" "encoding/base64" + "errors" + "fmt" "io" "io/ioutil" "net/http" @@ -15,8 +17,6 @@ import ( "strings" "sync" - "golang.org/x/xerrors" - "nhooyr.io/websocket/internal/errd" ) @@ -78,7 +78,7 @@ func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) ( secWebSocketKey, err := secWebSocketKey(rand) if err != nil { - return nil, nil, xerrors.Errorf("failed to generate Sec-WebSocket-Key: %w", err) + return nil, nil, fmt.Errorf("failed to generate Sec-WebSocket-Key: %w", err) } resp, err := handshakeRequest(ctx, urls, opts, secWebSocketKey) @@ -104,7 +104,7 @@ func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) ( rwc, ok := respBody.(io.ReadWriteCloser) if !ok { - return nil, resp, xerrors.Errorf("response body is not a io.ReadWriteCloser: %T", respBody) + return nil, resp, fmt.Errorf("response body is not a io.ReadWriteCloser: %T", respBody) } return newConn(connConfig{ @@ -120,12 +120,12 @@ func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) ( func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, secWebSocketKey string) (*http.Response, error) { if opts.HTTPClient.Timeout > 0 { - return nil, xerrors.New("use context for cancellation instead of http.Client.Timeout; see https://github.com/nhooyr/websocket/issues/67") + return nil, errors.New("use context for cancellation instead of http.Client.Timeout; see https://github.com/nhooyr/websocket/issues/67") } u, err := url.Parse(urls) if err != nil { - return nil, xerrors.Errorf("failed to parse url: %w", err) + return nil, fmt.Errorf("failed to parse url: %w", err) } switch u.Scheme { @@ -134,7 +134,7 @@ func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, secWe case "wss": u.Scheme = "https" default: - return nil, xerrors.Errorf("unexpected url scheme: %q", u.Scheme) + return nil, fmt.Errorf("unexpected url scheme: %q", u.Scheme) } req, _ := http.NewRequestWithContext(ctx, "GET", u.String(), nil) @@ -153,7 +153,7 @@ func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, secWe resp, err := opts.HTTPClient.Do(req) if err != nil { - return nil, xerrors.Errorf("failed to send handshake request: %w", err) + return nil, fmt.Errorf("failed to send handshake request: %w", err) } return resp, nil } @@ -165,26 +165,26 @@ func secWebSocketKey(rr io.Reader) (string, error) { b := make([]byte, 16) _, err := io.ReadFull(rr, b) if err != nil { - return "", xerrors.Errorf("failed to read random data from rand.Reader: %w", err) + return "", fmt.Errorf("failed to read random data from rand.Reader: %w", err) } return base64.StdEncoding.EncodeToString(b), nil } func verifyServerResponse(opts *DialOptions, secWebSocketKey string, resp *http.Response) (*compressionOptions, error) { if resp.StatusCode != http.StatusSwitchingProtocols { - return nil, xerrors.Errorf("expected handshake response status code %v but got %v", http.StatusSwitchingProtocols, resp.StatusCode) + return nil, fmt.Errorf("expected handshake response status code %v but got %v", http.StatusSwitchingProtocols, resp.StatusCode) } if !headerContainsToken(resp.Header, "Connection", "Upgrade") { - return nil, xerrors.Errorf("WebSocket protocol violation: Connection header %q does not contain Upgrade", resp.Header.Get("Connection")) + return nil, fmt.Errorf("WebSocket protocol violation: Connection header %q does not contain Upgrade", resp.Header.Get("Connection")) } if !headerContainsToken(resp.Header, "Upgrade", "WebSocket") { - return nil, xerrors.Errorf("WebSocket protocol violation: Upgrade header %q does not contain websocket", resp.Header.Get("Upgrade")) + return nil, fmt.Errorf("WebSocket protocol violation: Upgrade header %q does not contain websocket", resp.Header.Get("Upgrade")) } if resp.Header.Get("Sec-WebSocket-Accept") != secWebSocketAccept(secWebSocketKey) { - return nil, xerrors.Errorf("WebSocket protocol violation: invalid Sec-WebSocket-Accept %q, key %q", + return nil, fmt.Errorf("WebSocket protocol violation: invalid Sec-WebSocket-Accept %q, key %q", resp.Header.Get("Sec-WebSocket-Accept"), secWebSocketKey, ) @@ -210,7 +210,7 @@ func verifySubprotocol(subprotos []string, resp *http.Response) error { } } - return xerrors.Errorf("WebSocket protocol violation: unexpected Sec-WebSocket-Protocol from server: %q", proto) + return fmt.Errorf("WebSocket protocol violation: unexpected Sec-WebSocket-Protocol from server: %q", proto) } func verifyServerExtensions(h http.Header) (*compressionOptions, error) { @@ -221,7 +221,7 @@ func verifyServerExtensions(h http.Header) (*compressionOptions, error) { ext := exts[0] if ext.name != "permessage-deflate" || len(exts) > 1 { - return nil, xerrors.Errorf("WebSocket protcol violation: unsupported extensions from server: %+v", exts[1:]) + return nil, fmt.Errorf("WebSocket protcol violation: unsupported extensions from server: %+v", exts[1:]) } copts := &compressionOptions{} @@ -232,7 +232,7 @@ func verifyServerExtensions(h http.Header) (*compressionOptions, error) { case "server_no_context_takeover": copts.serverNoContextTakeover = true default: - return nil, xerrors.Errorf("unsupported permessage-deflate parameter: %q", p) + return nil, fmt.Errorf("unsupported permessage-deflate parameter: %q", p) } } diff --git a/example_echo_test.go b/example_echo_test.go index 1daec8a5..cd195d2e 100644 --- a/example_echo_test.go +++ b/example_echo_test.go @@ -4,6 +4,7 @@ package websocket_test import ( "context" + "errors" "fmt" "io" "log" @@ -12,7 +13,6 @@ import ( "time" "golang.org/x/time/rate" - "golang.org/x/xerrors" "nhooyr.io/websocket" "nhooyr.io/websocket/wsjson" @@ -78,7 +78,7 @@ func echoServer(w http.ResponseWriter, r *http.Request) error { if c.Subprotocol() != "echo" { c.Close(websocket.StatusPolicyViolation, "client must speak the echo subprotocol") - return xerrors.New("client does not speak echo sub protocol") + return errors.New("client does not speak echo sub protocol") } l := rate.NewLimiter(rate.Every(time.Millisecond*100), 10) @@ -88,7 +88,7 @@ func echoServer(w http.ResponseWriter, r *http.Request) error { return nil } if err != nil { - return xerrors.Errorf("failed to echo with %v: %w", r.RemoteAddr, err) + return fmt.Errorf("failed to echo with %v: %w", r.RemoteAddr, err) } } } @@ -117,7 +117,7 @@ func echo(ctx context.Context, c *websocket.Conn, l *rate.Limiter) error { _, err = io.Copy(w, r) if err != nil { - return xerrors.Errorf("failed to io.Copy: %w", err) + return fmt.Errorf("failed to io.Copy: %w", err) } err = w.Close() diff --git a/frame.go b/frame.go index 4acaecf4..2a036f94 100644 --- a/frame.go +++ b/frame.go @@ -3,12 +3,11 @@ package websocket import ( "bufio" "encoding/binary" + "fmt" "io" "math" "math/bits" - "golang.org/x/xerrors" - "nhooyr.io/websocket/internal/errd" ) @@ -87,7 +86,7 @@ func readFrameHeader(r *bufio.Reader, readBuf []byte) (h header, err error) { } if h.payloadLength < 0 { - return header{}, xerrors.Errorf("received negative payload length: %v", h.payloadLength) + return header{}, fmt.Errorf("received negative payload length: %v", h.payloadLength) } if h.masked { diff --git a/go.mod b/go.mod index a10c7b1e..801d6be6 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module nhooyr.io/websocket -go 1.12 +go 1.13 require ( github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee // indirect @@ -11,5 +11,4 @@ require ( github.com/gorilla/websocket v1.4.1 github.com/klauspost/compress v1.10.0 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 - golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 ) diff --git a/internal/errd/wrap.go b/internal/errd/wrap.go index ed0b7754..6e779131 100644 --- a/internal/errd/wrap.go +++ b/internal/errd/wrap.go @@ -2,41 +2,13 @@ package errd import ( "fmt" - - "golang.org/x/xerrors" ) -type wrapError struct { - msg string - err error - frame xerrors.Frame -} - -func (e *wrapError) Error() string { - return fmt.Sprint(e) -} - -func (e *wrapError) Format(s fmt.State, v rune) { xerrors.FormatError(e, s, v) } - -func (e *wrapError) FormatError(p xerrors.Printer) (next error) { - p.Print(e.msg) - e.frame.Format(p) - return e.err -} - -func (e *wrapError) Unwrap() error { - return e.err -} - -// Wrap wraps err with xerrors.Errorf if err is non nil. +// Wrap wraps err with fmt.Errorf if err is non nil. // Intended for use with defer and a named error return. // Inspired by https://github.com/golang/go/issues/32676. func Wrap(err *error, f string, v ...interface{}) { if *err != nil { - *err = &wrapError{ - msg: fmt.Sprintf(f, v...), - err: *err, - frame: xerrors.Caller(1), - } + *err = fmt.Errorf(f+": %w", append(v, *err)...) } } diff --git a/internal/test/wstest/echo.go b/internal/test/wstest/echo.go index 714767fc..ab7bc25a 100644 --- a/internal/test/wstest/echo.go +++ b/internal/test/wstest/echo.go @@ -3,11 +3,10 @@ package wstest import ( "bytes" "context" + "fmt" "io" "time" - "golang.org/x/xerrors" - "nhooyr.io/websocket" "nhooyr.io/websocket/internal/test/cmp" "nhooyr.io/websocket/internal/test/xrand" @@ -73,11 +72,11 @@ func Echo(ctx context.Context, c *websocket.Conn, max int) error { } if expType != actType { - return xerrors.Errorf("unexpected message typ (%v): %v", expType, actType) + return fmt.Errorf("unexpected message typ (%v): %v", expType, actType) } if !bytes.Equal(msg, act) { - return xerrors.Errorf("unexpected msg read: %v", cmp.Diff(msg, act)) + return fmt.Errorf("unexpected msg read: %v", cmp.Diff(msg, act)) } return nil diff --git a/internal/test/wstest/pipe.go b/internal/test/wstest/pipe.go index 81705a8a..0a2899ee 100644 --- a/internal/test/wstest/pipe.go +++ b/internal/test/wstest/pipe.go @@ -5,12 +5,11 @@ package wstest import ( "bufio" "context" + "fmt" "net" "net/http" "net/http/httptest" - "golang.org/x/xerrors" - "nhooyr.io/websocket" "nhooyr.io/websocket/internal/errd" "nhooyr.io/websocket/internal/test/xrand" @@ -39,11 +38,11 @@ func Pipe(dialOpts *websocket.DialOptions, acceptOpts *websocket.AcceptOptions) clientConn, _, err := websocket.Dial(context.Background(), "ws://example.com", dialOpts) if err != nil { - return nil, nil, xerrors.Errorf("failed to dial with fake transport: %w", err) + return nil, nil, fmt.Errorf("failed to dial with fake transport: %w", err) } if serverConn == nil { - return nil, nil, xerrors.Errorf("failed to get server conn from fake transport: %w", acceptErr) + return nil, nil, fmt.Errorf("failed to get server conn from fake transport: %w", acceptErr) } if xrand.Bool() { diff --git a/internal/xsync/go.go b/internal/xsync/go.go index 712739aa..7a61f27f 100644 --- a/internal/xsync/go.go +++ b/internal/xsync/go.go @@ -1,7 +1,7 @@ package xsync import ( - "golang.org/x/xerrors" + "fmt" ) // Go allows running a function in another goroutine @@ -13,7 +13,7 @@ func Go(fn func() error) <-chan error { r := recover() if r != nil { select { - case errs <- xerrors.Errorf("panic in go fn: %v", r): + case errs <- fmt.Errorf("panic in go fn: %v", r): default: } } diff --git a/netconn.go b/netconn.go index a2d8f4f3..64aadf0b 100644 --- a/netconn.go +++ b/netconn.go @@ -2,13 +2,12 @@ package websocket import ( "context" + "fmt" "io" "math" "net" "sync" "time" - - "golang.org/x/xerrors" ) // NetConn converts a *websocket.Conn into a net.Conn. @@ -108,7 +107,7 @@ func (c *netConn) Read(p []byte) (int, error) { return 0, err } if typ != c.msgType { - err := xerrors.Errorf("unexpected frame type read (expected %v): %v", c.msgType, typ) + err := fmt.Errorf("unexpected frame type read (expected %v): %v", c.msgType, typ) c.c.Close(StatusUnsupportedData, err.Error()) return 0, err } diff --git a/read.go b/read.go index bbad30d1..a1efecab 100644 --- a/read.go +++ b/read.go @@ -5,13 +5,13 @@ package websocket import ( "bufio" "context" + "errors" + "fmt" "io" "io/ioutil" "strings" "time" - "golang.org/x/xerrors" - "nhooyr.io/websocket/internal/errd" "nhooyr.io/websocket/internal/xsync" ) @@ -144,13 +144,13 @@ func (c *Conn) readLoop(ctx context.Context) (header, error) { } if h.rsv1 && c.readRSV1Illegal(h) || h.rsv2 || h.rsv3 { - err := xerrors.Errorf("received header with unexpected rsv bits set: %v:%v:%v", h.rsv1, h.rsv2, h.rsv3) + err := fmt.Errorf("received header with unexpected rsv bits set: %v:%v:%v", h.rsv1, h.rsv2, h.rsv3) c.writeError(StatusProtocolError, err) return header{}, err } if !c.client && !h.masked { - return header{}, xerrors.New("received unmasked frame from client") + return header{}, errors.New("received unmasked frame from client") } switch h.opcode { @@ -161,12 +161,12 @@ func (c *Conn) readLoop(ctx context.Context) (header, error) { if h.opcode == opClose && CloseStatus(err) != -1 { return header{}, err } - return header{}, xerrors.Errorf("failed to handle control frame %v: %w", h.opcode, err) + return header{}, fmt.Errorf("failed to handle control frame %v: %w", h.opcode, err) } case opContinuation, opText, opBinary: return h, nil default: - err := xerrors.Errorf("received unknown opcode %v", h.opcode) + err := fmt.Errorf("received unknown opcode %v", h.opcode) c.writeError(StatusProtocolError, err) return header{}, err } @@ -217,7 +217,7 @@ func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { case <-ctx.Done(): return n, ctx.Err() default: - err = xerrors.Errorf("failed to read frame payload: %w", err) + err = fmt.Errorf("failed to read frame payload: %w", err) c.close(err) return n, err } @@ -234,13 +234,13 @@ func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { func (c *Conn) handleControl(ctx context.Context, h header) (err error) { if h.payloadLength < 0 || h.payloadLength > maxControlPayload { - err := xerrors.Errorf("received control frame payload with invalid length: %d", h.payloadLength) + err := fmt.Errorf("received control frame payload with invalid length: %d", h.payloadLength) c.writeError(StatusProtocolError, err) return err } if !h.fin { - err := xerrors.New("received fragmented control frame") + err := errors.New("received fragmented control frame") c.writeError(StatusProtocolError, err) return err } @@ -277,12 +277,12 @@ func (c *Conn) handleControl(ctx context.Context, h header) (err error) { ce, err := parseClosePayload(b) if err != nil { - err = xerrors.Errorf("received invalid close payload: %w", err) + err = fmt.Errorf("received invalid close payload: %w", err) c.writeError(StatusProtocolError, err) return err } - err = xerrors.Errorf("received close frame: %w", ce) + err = fmt.Errorf("received close frame: %w", ce) c.setCloseErr(err) c.writeClose(ce.Code, ce.Reason) c.close(err) @@ -299,7 +299,7 @@ func (c *Conn) reader(ctx context.Context) (_ MessageType, _ io.Reader, err erro defer c.readMu.Unlock() if !c.msgReader.fin { - return 0, nil, xerrors.New("previous message not read to completion") + return 0, nil, errors.New("previous message not read to completion") } h, err := c.readLoop(ctx) @@ -308,7 +308,7 @@ func (c *Conn) reader(ctx context.Context) (_ MessageType, _ io.Reader, err erro } if h.opcode == opContinuation { - err := xerrors.New("received continuation frame without text or binary frame") + err := errors.New("received continuation frame without text or binary frame") c.writeError(StatusProtocolError, err) return 0, nil, err } @@ -357,10 +357,10 @@ func (mr *msgReader) setFrame(h header) { func (mr *msgReader) Read(p []byte) (n int, err error) { defer func() { - if xerrors.Is(err, io.ErrUnexpectedEOF) && mr.fin && mr.flate { + if errors.Is(err, io.ErrUnexpectedEOF) && mr.fin && mr.flate { err = io.EOF } - if xerrors.Is(err, io.EOF) { + if errors.Is(err, io.EOF) { err = io.EOF mr.putFlateReader() return @@ -397,7 +397,7 @@ func (mr *msgReader) read(p []byte) (int, error) { return 0, err } if h.opcode != opContinuation { - err := xerrors.New("received new data message without finishing the previous message") + err := errors.New("received new data message without finishing the previous message") mr.c.writeError(StatusProtocolError, err) return 0, err } @@ -448,7 +448,7 @@ func (lr *limitReader) reset(r io.Reader) { func (lr *limitReader) Read(p []byte) (int, error) { if lr.n <= 0 { - err := xerrors.Errorf("read limited at %v bytes", lr.limit.Load()) + err := fmt.Errorf("read limited at %v bytes", lr.limit.Load()) lr.c.writeError(StatusMessageTooBig, err) return 0, err } diff --git a/write.go b/write.go index b560b44c..81b9141a 100644 --- a/write.go +++ b/write.go @@ -7,12 +7,13 @@ import ( "context" "crypto/rand" "encoding/binary" + "errors" + "fmt" "io" "sync" "time" "github.com/klauspost/compress/flate" - "golang.org/x/xerrors" "nhooyr.io/websocket/internal/errd" ) @@ -27,7 +28,7 @@ import ( func (c *Conn) Writer(ctx context.Context, typ MessageType) (io.WriteCloser, error) { w, err := c.writer(ctx, typ) if err != nil { - return nil, xerrors.Errorf("failed to get writer: %w", err) + return nil, fmt.Errorf("failed to get writer: %w", err) } return w, nil } @@ -41,7 +42,7 @@ func (c *Conn) Writer(ctx context.Context, typ MessageType) (io.WriteCloser, err func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { _, err := c.write(ctx, typ, p) if err != nil { - return xerrors.Errorf("failed to write msg: %w", err) + return fmt.Errorf("failed to write msg: %w", err) } return nil } @@ -53,14 +54,14 @@ type msgWriter struct { func (mw *msgWriter) Write(p []byte) (int, error) { if mw.closed { - return 0, xerrors.New("cannot use closed writer") + return 0, errors.New("cannot use closed writer") } return mw.mw.Write(p) } func (mw *msgWriter) Close() error { if mw.closed { - return xerrors.New("cannot use closed writer") + return errors.New("cannot use closed writer") } mw.closed = true return mw.mw.Close() @@ -182,7 +183,7 @@ func (mw *msgWriterState) Write(p []byte) (_ int, err error) { func (mw *msgWriterState) write(p []byte) (int, error) { n, err := mw.c.writeFrame(mw.ctx, false, mw.flate, mw.opcode, p) if err != nil { - return n, xerrors.Errorf("failed to write data frame: %w", err) + return n, fmt.Errorf("failed to write data frame: %w", err) } mw.opcode = opContinuation return n, nil @@ -197,7 +198,7 @@ func (mw *msgWriterState) Close() (err error) { _, err = mw.c.writeFrame(mw.ctx, true, mw.flate, mw.opcode, nil) if err != nil { - return xerrors.Errorf("failed to write fin frame: %w", err) + return fmt.Errorf("failed to write fin frame: %w", err) } if mw.flate && !mw.flateContextTakeover() { @@ -218,7 +219,7 @@ func (c *Conn) writeControl(ctx context.Context, opcode opcode, p []byte) error _, err := c.writeFrame(ctx, true, false, opcode, p) if err != nil { - return xerrors.Errorf("failed to write control frame %v: %w", opcode, err) + return fmt.Errorf("failed to write control frame %v: %w", opcode, err) } return nil } @@ -245,7 +246,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco c.writeHeader.masked = true _, err = io.ReadFull(rand.Reader, c.writeHeaderBuf[:4]) if err != nil { - return 0, xerrors.Errorf("failed to generate masking key: %w", err) + return 0, fmt.Errorf("failed to generate masking key: %w", err) } c.writeHeader.maskKey = binary.LittleEndian.Uint32(c.writeHeaderBuf[:]) } @@ -268,7 +269,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco if c.writeHeader.fin { err = c.bw.Flush() if err != nil { - return n, xerrors.Errorf("failed to flush: %w", err) + return n, fmt.Errorf("failed to flush: %w", err) } } diff --git a/ws_js.go b/ws_js.go index ecf3d78c..2b560ce8 100644 --- a/ws_js.go +++ b/ws_js.go @@ -3,6 +3,8 @@ package websocket // import "nhooyr.io/websocket" import ( "bytes" "context" + "errors" + "fmt" "io" "net/http" "reflect" @@ -10,8 +12,6 @@ import ( "sync" "syscall/js" - "golang.org/x/xerrors" - "nhooyr.io/websocket/internal/bpool" "nhooyr.io/websocket/internal/wsjs" "nhooyr.io/websocket/internal/xsync" @@ -45,7 +45,7 @@ func (c *Conn) close(err error, wasClean bool) { runtime.SetFinalizer(c, nil) if !wasClean { - err = xerrors.Errorf("unclean connection close: %w", err) + err = fmt.Errorf("unclean connection close: %w", err) } c.setCloseErr(err) c.closeWasClean = wasClean @@ -87,7 +87,7 @@ func (c *Conn) init() { }) runtime.SetFinalizer(c, func(c *Conn) { - c.setCloseErr(xerrors.New("connection garbage collected")) + c.setCloseErr(errors.New("connection garbage collected")) c.closeWithInternal() }) } @@ -100,15 +100,15 @@ func (c *Conn) closeWithInternal() { // The maximum time spent waiting is bounded by the context. func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { if c.isReadClosed.Load() == 1 { - return 0, nil, xerrors.New("WebSocket connection read closed") + return 0, nil, errors.New("WebSocket connection read closed") } typ, p, err := c.read(ctx) if err != nil { - return 0, nil, xerrors.Errorf("failed to read: %w", err) + return 0, nil, fmt.Errorf("failed to read: %w", err) } if int64(len(p)) > c.msgReadLimit.Load() { - err := xerrors.Errorf("read limited at %v bytes", c.msgReadLimit.Load()) + err := fmt.Errorf("read limited at %v bytes", c.msgReadLimit.Load()) c.Close(StatusMessageTooBig, err.Error()) return 0, nil, err } @@ -166,7 +166,7 @@ func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { // to match the Go API. It can only error if the message type // is unexpected or the passed bytes contain invalid UTF-8 for // MessageText. - err := xerrors.Errorf("failed to write: %w", err) + err := fmt.Errorf("failed to write: %w", err) c.setCloseErr(err) c.closeWithInternal() return err @@ -184,7 +184,7 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) error { case MessageText: return c.ws.SendText(string(p)) default: - return xerrors.Errorf("unexpected message type: %v", typ) + return fmt.Errorf("unexpected message type: %v", typ) } } @@ -195,7 +195,7 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) error { func (c *Conn) Close(code StatusCode, reason string) error { err := c.exportedClose(code, reason) if err != nil { - return xerrors.Errorf("failed to close WebSocket: %w", err) + return fmt.Errorf("failed to close WebSocket: %w", err) } return nil } @@ -204,13 +204,13 @@ func (c *Conn) exportedClose(code StatusCode, reason string) error { c.closingMu.Lock() defer c.closingMu.Unlock() - ce := xerrors.Errorf("sent close: %w", CloseError{ + ce := fmt.Errorf("sent close: %w", CloseError{ Code: code, Reason: reason, }) if c.isClosed() { - return xerrors.Errorf("tried to close with %q but connection already closed: %w", ce, c.closeErr) + return fmt.Errorf("tried to close with %q but connection already closed: %w", ce, c.closeErr) } c.setCloseErr(ce) @@ -245,7 +245,7 @@ type DialOptions struct { func Dial(ctx context.Context, url string, opts *DialOptions) (*Conn, *http.Response, error) { c, resp, err := dial(ctx, url, opts) if err != nil { - return nil, nil, xerrors.Errorf("failed to WebSocket dial %q: %w", url, err) + return nil, nil, fmt.Errorf("failed to WebSocket dial %q: %w", url, err) } return c, resp, nil } @@ -318,25 +318,25 @@ type writer struct { func (w writer) Write(p []byte) (int, error) { if w.closed { - return 0, xerrors.New("cannot write to closed writer") + return 0, errors.New("cannot write to closed writer") } n, err := w.b.Write(p) if err != nil { - return n, xerrors.Errorf("failed to write message: %w", err) + return n, fmt.Errorf("failed to write message: %w", err) } return n, nil } func (w writer) Close() error { if w.closed { - return xerrors.New("cannot close closed writer") + return errors.New("cannot close closed writer") } w.closed = true defer bpool.Put(w.b) err := w.c.Write(w.ctx, w.typ, w.b.Bytes()) if err != nil { - return xerrors.Errorf("failed to close writer: %w", err) + return fmt.Errorf("failed to close writer: %w", err) } return nil } @@ -361,7 +361,7 @@ func (c *Conn) SetReadLimit(n int64) { func (c *Conn) setCloseErr(err error) { c.closeErrOnce.Do(func() { - c.closeErr = xerrors.Errorf("WebSocket closed: %w", err) + c.closeErr = fmt.Errorf("WebSocket closed: %w", err) }) } diff --git a/wsjson/wsjson.go b/wsjson/wsjson.go index e6f06a2f..99996a69 100644 --- a/wsjson/wsjson.go +++ b/wsjson/wsjson.go @@ -4,8 +4,7 @@ package wsjson // import "nhooyr.io/websocket/wsjson" import ( "context" "encoding/json" - - "golang.org/x/xerrors" + "fmt" "nhooyr.io/websocket" "nhooyr.io/websocket/internal/bpool" @@ -28,7 +27,7 @@ func read(ctx context.Context, c *websocket.Conn, v interface{}) (err error) { if typ != websocket.MessageText { c.Close(websocket.StatusUnsupportedData, "expected text message") - return xerrors.Errorf("expected text message for JSON but got: %v", typ) + return fmt.Errorf("expected text message for JSON but got: %v", typ) } b := bpool.Get() @@ -42,7 +41,7 @@ func read(ctx context.Context, c *websocket.Conn, v interface{}) (err error) { err = json.Unmarshal(b.Bytes(), v) if err != nil { c.Close(websocket.StatusInvalidFramePayloadData, "failed to unmarshal JSON") - return xerrors.Errorf("failed to unmarshal JSON: %w", err) + return fmt.Errorf("failed to unmarshal JSON: %w", err) } return nil @@ -66,7 +65,7 @@ func write(ctx context.Context, c *websocket.Conn, v interface{}) (err error) { // a copy of the byte slice but Encoder does as it directly writes to w. err = json.NewEncoder(w).Encode(v) if err != nil { - return xerrors.Errorf("failed to marshal JSON: %w", err) + return fmt.Errorf("failed to marshal JSON: %w", err) } return w.Close() diff --git a/wspb/wspb.go b/wspb/wspb.go index 06ac3368..e43042d5 100644 --- a/wspb/wspb.go +++ b/wspb/wspb.go @@ -4,9 +4,9 @@ package wspb // import "nhooyr.io/websocket/wspb" import ( "bytes" "context" + "fmt" "github.com/golang/protobuf/proto" - "golang.org/x/xerrors" "nhooyr.io/websocket" "nhooyr.io/websocket/internal/bpool" @@ -29,7 +29,7 @@ func read(ctx context.Context, c *websocket.Conn, v proto.Message) (err error) { if typ != websocket.MessageBinary { c.Close(websocket.StatusUnsupportedData, "expected binary message") - return xerrors.Errorf("expected binary message for protobuf but got: %v", typ) + return fmt.Errorf("expected binary message for protobuf but got: %v", typ) } b := bpool.Get() @@ -43,7 +43,7 @@ func read(ctx context.Context, c *websocket.Conn, v proto.Message) (err error) { err = proto.Unmarshal(b.Bytes(), v) if err != nil { c.Close(websocket.StatusInvalidFramePayloadData, "failed to unmarshal protobuf") - return xerrors.Errorf("failed to unmarshal protobuf: %w", err) + return fmt.Errorf("failed to unmarshal protobuf: %w", err) } return nil @@ -66,7 +66,7 @@ func write(ctx context.Context, c *websocket.Conn, v proto.Message) (err error) err = pb.Marshal(v) if err != nil { - return xerrors.Errorf("failed to marshal protobuf: %w", err) + return fmt.Errorf("failed to marshal protobuf: %w", err) } return c.Write(ctx, websocket.MessageBinary, pb.Bytes()) From e7e4b516c2957867acabb30221a149ad9b9d0bf7 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 15 Feb 2020 22:34:02 -0500 Subject: [PATCH 271/519] Remove CI badge from README Just clutter. --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index d1847fc8..5dddf84a 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,6 @@ # websocket [![godoc](https://godoc.org/nhooyr.io/websocket?status.svg)](https://godoc.org/nhooyr.io/websocket) -[![ci](https://img.shields.io/github/workflow/status/nhooyr/websocket/ci?label=ci)](https://github.com/nhooyr/websocket/actions) websocket is a minimal and idiomatic WebSocket library for Go. @@ -110,8 +109,7 @@ Advantages of nhooyr.io/websocket: - Gorilla's implementation is slower and uses [unsafe](https://golang.org/pkg/unsafe/). - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support - Gorilla only supports no context takeover mode - - Uses [klauspost/compress](https://github.com/klauspost/compress) for optimized compression - - See [gorilla/websocket#203](https://github.com/gorilla/websocket/issues/203) + - We use [klauspost/compress](https://github.com/klauspost/compress) for much lower memory usage ([gorilla/websocket#203](https://github.com/gorilla/websocket/issues/203)) - [CloseRead](https://godoc.org/nhooyr.io/websocket#Conn.CloseRead) helper ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492)) - Actively maintained ([gorilla/websocket#370](https://github.com/gorilla/websocket/issues/370)) From 5f3fa5c791f2e01c7bae27b1392e7de1a6943cb3 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 15 Feb 2020 22:58:43 -0500 Subject: [PATCH 272/519] Add shfmt and shellcheck --- Makefile | 6 ------ ci/ensure_fmt.sh | 23 +++++++++++++++++++++++ ci/fmt.mk | 13 +++++-------- ci/image/Dockerfile | 6 ++++-- ci/lint.mk | 5 ++++- 5 files changed, 36 insertions(+), 17 deletions(-) create mode 100755 ci/ensure_fmt.sh diff --git a/Makefile b/Makefile index ad1ba257..f9f31c49 100644 --- a/Makefile +++ b/Makefile @@ -2,12 +2,6 @@ all: fmt lint test .SILENT: -.PHONY: * - -.ONESHELL: -SHELL = bash -.SHELLFLAGS = -ceuo pipefail - include ci/fmt.mk include ci/lint.mk include ci/test.mk diff --git a/ci/ensure_fmt.sh b/ci/ensure_fmt.sh new file mode 100755 index 00000000..6fe9cb18 --- /dev/null +++ b/ci/ensure_fmt.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +set -euo pipefail + +main() { + local files + mapfile -t files < <(git ls-files --other --modified --exclude-standard) + if [[ ${files[*]} == "" ]]; then + return + fi + + echo "Files need generation or are formatted incorrectly:" + for f in "${files[@]}"; do + echo " $f" + done + + echo + echo "Please run the following locally:" + echo " make fmt" + exit 1 +} + +main "$@" diff --git a/ci/fmt.mk b/ci/fmt.mk index f3969721..f313562c 100644 --- a/ci/fmt.mk +++ b/ci/fmt.mk @@ -1,12 +1,6 @@ -fmt: modtidy gofmt goimports prettier +fmt: modtidy gofmt goimports prettier shfmt ifdef CI - if [[ $$(git ls-files --other --modified --exclude-standard) != "" ]]; then - echo "Files need generation or are formatted incorrectly:" - git -c color.ui=always status | grep --color=no '\e\[31m' - echo "Please run the following locally:" - echo " make fmt" - exit 1 - fi + ./ci/ensure_fmt.sh endif modtidy: gen @@ -23,3 +17,6 @@ prettier: gen: stringer -type=opcode,MessageType,StatusCode -output=stringer.go + +shfmt: + shfmt -i 2 -w -s -sr $$(git ls-files "*.sh") diff --git a/ci/image/Dockerfile b/ci/image/Dockerfile index 88c96502..ed408eda 100644 --- a/ci/image/Dockerfile +++ b/ci/image/Dockerfile @@ -1,10 +1,12 @@ FROM golang:1 RUN apt-get update -RUN apt-get install -y chromium npm +RUN apt-get install -y chromium npm shellcheck + +ARG SHFMT_URL=https://github.com/mvdan/sh/releases/download/v3.0.1/shfmt_v3.0.1_linux_amd64 +RUN curl -L $SHFMT_URL > /usr/local/bin/shfmt && chmod +x /usr/local/bin/shfmt ENV GOFLAGS="-mod=readonly" -ENV PAGER=cat ENV CI=true ENV MAKEFLAGS="--jobs=16 --output-sync=target" diff --git a/ci/lint.mk b/ci/lint.mk index 031f0de3..4335e7b1 100644 --- a/ci/lint.mk +++ b/ci/lint.mk @@ -1,4 +1,4 @@ -lint: govet golint +lint: govet golint govet-wasm golint-wasm shellcheck govet: go vet ./... @@ -11,3 +11,6 @@ golint: golint-wasm: GOOS=js GOARCH=wasm golint -set_exit_status ./... + +shellcheck: + shellcheck $$(git ls-files "*.sh") From 4735f36780487f54358587f14dc4f562af0f0c1f Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 15 Feb 2020 23:26:13 -0500 Subject: [PATCH 273/519] Minor fixes --- accept.go | 39 ++++++++++++++++++++++- accept_test.go | 1 + compress_notjs.go | 13 ++++++-- conn_test.go | 56 ++++++++++++++++++++++++++-------- dial.go | 33 ++++++++++++++++++-- internal/test/assert/assert.go | 14 +++++++-- internal/test/cmp/cmp.go | 16 ---------- internal/test/wstest/echo.go | 4 +-- 8 files changed, 139 insertions(+), 37 deletions(-) delete mode 100644 internal/test/cmp/cmp.go diff --git a/accept.go b/accept.go index fda8cdc6..5a162de0 100644 --- a/accept.go +++ b/accept.go @@ -12,6 +12,7 @@ import ( "net/http" "net/textproto" "net/url" + "strconv" "strings" "nhooyr.io/websocket/internal/errd" @@ -208,6 +209,7 @@ func acceptCompression(r *http.Request, w http.ResponseWriter, mode CompressionM func acceptDeflate(w http.ResponseWriter, ext websocketExtension, mode CompressionMode) (*compressionOptions, error) { copts := mode.opts() + copts.serverMaxWindowBits = 8 for _, p := range ext.params { switch p { @@ -219,7 +221,27 @@ func acceptDeflate(w http.ResponseWriter, ext websocketExtension, mode Compressi continue } - if strings.HasPrefix(p, "client_max_window_bits") || strings.HasPrefix(p, "server_max_window_bits") { + if strings.HasPrefix(p, "client_max_window_bits") { + continue + + // bits, ok := parseExtensionParameter(p, 15) + // if !ok || bits < 8 || bits > 16 { + // err := fmt.Errorf("invalid client_max_window_bits: %q", p) + // http.Error(w, err.Error(), http.StatusBadRequest) + // return nil, err + // } + // copts.clientMaxWindowBits = bits + // continue + } + + if false && strings.HasPrefix(p, "server_max_window_bits") { + // We always send back 8 but make sure to validate. + bits, ok := parseExtensionParameter(p, 0) + if !ok || bits < 8 || bits > 16 { + err := fmt.Errorf("invalid server_max_window_bits: %q", p) + http.Error(w, err.Error(), http.StatusBadRequest) + return nil, err + } continue } @@ -233,6 +255,21 @@ func acceptDeflate(w http.ResponseWriter, ext websocketExtension, mode Compressi return copts, nil } +// parseExtensionParameter parses the value in the extension parameter p. +// It falls back to defaultVal if there is no value. +// If defaultVal == 0, then ok == false if there is no value. +func parseExtensionParameter(p string, defaultVal int) (int, bool) { + ps := strings.Split(p, "=") + if len(ps) == 1 { + if defaultVal > 0 { + return defaultVal, true + } + return 0, false + } + i, e := strconv.Atoi(strings.Trim(ps[1], `"`)) + return i, e == nil +} + func acceptWebkitDeflate(w http.ResponseWriter, ext websocketExtension, mode CompressionMode) (*compressionOptions, error) { copts := mode.opts() // The peer must explicitly request it. diff --git a/accept_test.go b/accept_test.go index 523d4685..555f0dc0 100644 --- a/accept_test.go +++ b/accept_test.go @@ -327,6 +327,7 @@ func Test_acceptCompression(t *testing.T) { expCopts: &compressionOptions{ clientNoContextTakeover: true, serverNoContextTakeover: true, + serverMaxWindowBits: 8, }, }, { diff --git a/compress_notjs.go b/compress_notjs.go index a6911056..ef82eb4d 100644 --- a/compress_notjs.go +++ b/compress_notjs.go @@ -3,6 +3,7 @@ package websocket import ( + "fmt" "io" "net/http" "sync" @@ -19,7 +20,10 @@ func (m CompressionMode) opts() *compressionOptions { type compressionOptions struct { clientNoContextTakeover bool + clientMaxWindowBits int + serverNoContextTakeover bool + serverMaxWindowBits int } func (copts *compressionOptions) setHeader(h http.Header) { @@ -30,6 +34,12 @@ func (copts *compressionOptions) setHeader(h http.Header) { if copts.serverNoContextTakeover { s += "; server_no_context_takeover" } + if false && copts.serverMaxWindowBits > 0 { + s += fmt.Sprintf("; server_max_window_bits=%v", copts.serverMaxWindowBits) + } + if false && copts.clientMaxWindowBits > 0 { + s += fmt.Sprintf("; client_max_window_bits=%v", copts.clientMaxWindowBits) + } h.Set("Sec-WebSocket-Extensions", s) } @@ -152,9 +162,8 @@ func (sw *slidingWindow) close() { } swPoolMu.Lock() - defer swPoolMu.Unlock() - swPool[cap(sw.buf)].Put(sw.buf) + swPoolMu.Unlock() sw.buf = nil } diff --git a/conn_test.go b/conn_test.go index c68a25f6..64e6736f 100644 --- a/conn_test.go +++ b/conn_test.go @@ -114,13 +114,21 @@ func TestConn(t *testing.T) { for i := 0; i < count; i++ { go func() { - errs <- c1.Write(tt.ctx, websocket.MessageBinary, msg) + select { + case errs <- c1.Write(tt.ctx, websocket.MessageBinary, msg): + case <-tt.ctx.Done(): + return + } }() } for i := 0; i < count; i++ { - err := <-errs - assert.Success(t, err) + select { + case err := <-errs: + assert.Success(t, err) + case <-tt.ctx.Done(): + t.Fatal(tt.ctx.Err()) + } } err := c1.Close(websocket.StatusNormalClosure, "") @@ -171,8 +179,12 @@ func TestConn(t *testing.T) { _, err = n1.Read(nil) assert.Equal(t, "read error", err, io.EOF) - err = <-errs - assert.Success(t, err) + select { + case err := <-errs: + assert.Success(t, err) + case <-tt.ctx.Done(): + t.Fatal(tt.ctx.Err()) + } assert.Equal(t, "read msg", []byte("hello"), b) }) @@ -195,8 +207,12 @@ func TestConn(t *testing.T) { _, err := ioutil.ReadAll(n1) assert.Contains(t, err, `unexpected frame type read (expected MessageBinary): MessageText`) - err = <-errs - assert.Success(t, err) + select { + case err := <-errs: + assert.Success(t, err) + case <-tt.ctx.Done(): + t.Fatal(tt.ctx.Err()) + } }) t.Run("wsjson", func(t *testing.T) { @@ -218,8 +234,12 @@ func TestConn(t *testing.T) { assert.Success(t, err) assert.Equal(t, "read msg", exp, act) - err = <-werr - assert.Success(t, err) + select { + case err := <-werr: + assert.Success(t, err) + case <-tt.ctx.Done(): + t.Fatal(tt.ctx.Err()) + } err = c1.Close(websocket.StatusNormalClosure, "") assert.Success(t, err) @@ -411,14 +431,22 @@ func BenchmarkConn(b *testing.B) { go func() { for range writes { - werrs <- c1.Write(bb.ctx, websocket.MessageText, msg) + select { + case werrs <- c1.Write(bb.ctx, websocket.MessageText, msg): + case <-bb.ctx.Done(): + return + } } }() b.SetBytes(int64(len(msg))) b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { - writes <- struct{}{} + select { + case writes <- struct{}{}: + case <-bb.ctx.Done(): + b.Fatal(bb.ctx.Err()) + } typ, r, err := c1.Reader(bb.ctx) if err != nil { @@ -445,7 +473,11 @@ func BenchmarkConn(b *testing.B) { assert.Equal(b, "msg", msg, readBuf) } - err = <-werrs + select { + case err = <-werrs: + case <-bb.ctx.Done(): + b.Fatal(bb.ctx.Err()) + } if err != nil { b.Fatal(err) } diff --git a/dial.go b/dial.go index 6e37a589..8ff39597 100644 --- a/dial.go +++ b/dial.go @@ -16,6 +16,7 @@ import ( "net/url" "strings" "sync" + "time" "nhooyr.io/websocket/internal/errd" ) @@ -91,6 +92,12 @@ func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) ( if err != nil { // We read a bit of the body for easier debugging. r := io.LimitReader(respBody, 1024) + + timer := time.AfterFunc(time.Second*3, func() { + respBody.Close() + }) + defer timer.Stop() + b, _ := ioutil.ReadAll(r) respBody.Close() resp.Body = ioutil.NopCloser(bytes.NewReader(b)) @@ -148,6 +155,7 @@ func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, secWe } if opts.CompressionMode != CompressionDisabled { copts := opts.CompressionMode.opts() + copts.clientMaxWindowBits = 8 copts.setHeader(req.Header) } @@ -225,15 +233,36 @@ func verifyServerExtensions(h http.Header) (*compressionOptions, error) { } copts := &compressionOptions{} + copts.clientMaxWindowBits = 8 for _, p := range ext.params { switch p { case "client_no_context_takeover": copts.clientNoContextTakeover = true + continue case "server_no_context_takeover": copts.serverNoContextTakeover = true - default: - return nil, fmt.Errorf("unsupported permessage-deflate parameter: %q", p) + continue + } + + if false && strings.HasPrefix(p, "server_max_window_bits") { + bits, ok := parseExtensionParameter(p, 0) + if !ok || bits < 8 || bits > 16 { + return nil, fmt.Errorf("invalid server_max_window_bits: %q", p) + } + copts.serverMaxWindowBits = bits + continue } + + if false && strings.HasPrefix(p, "client_max_window_bits") { + bits, ok := parseExtensionParameter(p, 0) + if !ok || bits < 8 || bits > 16 { + return nil, fmt.Errorf("invalid client_max_window_bits: %q", p) + } + copts.clientMaxWindowBits = 8 + continue + } + + return nil, fmt.Errorf("unsupported permessage-deflate parameter: %q", p) } return copts, nil diff --git a/internal/test/assert/assert.go b/internal/test/assert/assert.go index 602b887e..6eaf7fc3 100644 --- a/internal/test/assert/assert.go +++ b/internal/test/assert/assert.go @@ -2,17 +2,27 @@ package assert import ( "fmt" + "reflect" "strings" "testing" - "nhooyr.io/websocket/internal/test/cmp" + "github.com/golang/protobuf/proto" + "github.com/google/go-cmp/cmp" + "github.com/google/go-cmp/cmp/cmpopts" ) +// Diff returns a human readable diff between v1 and v2 +func Diff(v1, v2 interface{}) string { + return cmp.Diff(v1, v2, cmpopts.EquateErrors(), cmp.Exporter(func(r reflect.Type) bool { + return true + }), cmp.Comparer(proto.Equal)) +} + // Equal asserts exp == act. func Equal(t testing.TB, name string, exp, act interface{}) { t.Helper() - if diff := cmp.Diff(exp, act); diff != "" { + if diff := Diff(exp, act); diff != "" { t.Fatalf("unexpected %v: %v", name, diff) } } diff --git a/internal/test/cmp/cmp.go b/internal/test/cmp/cmp.go deleted file mode 100644 index eadcb5d9..00000000 --- a/internal/test/cmp/cmp.go +++ /dev/null @@ -1,16 +0,0 @@ -package cmp - -import ( - "reflect" - - "github.com/golang/protobuf/proto" - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" -) - -// Diff returns a human readable diff between v1 and v2 -func Diff(v1, v2 interface{}) string { - return cmp.Diff(v1, v2, cmpopts.EquateErrors(), cmp.Exporter(func(r reflect.Type) bool { - return true - }), cmp.Comparer(proto.Equal)) -} diff --git a/internal/test/wstest/echo.go b/internal/test/wstest/echo.go index ab7bc25a..8f4e47c8 100644 --- a/internal/test/wstest/echo.go +++ b/internal/test/wstest/echo.go @@ -8,7 +8,7 @@ import ( "time" "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/test/cmp" + "nhooyr.io/websocket/internal/test/assert" "nhooyr.io/websocket/internal/test/xrand" "nhooyr.io/websocket/internal/xsync" ) @@ -76,7 +76,7 @@ func Echo(ctx context.Context, c *websocket.Conn, max int) error { } if !bytes.Equal(msg, act) { - return fmt.Errorf("unexpected msg read: %v", cmp.Diff(msg, act)) + return fmt.Errorf("unexpected msg read: %v", assert.Diff(msg, act)) } return nil From 95bfb8f5cd987dbe2229c68bb1aa635237f54679 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 16 Feb 2020 02:36:26 -0500 Subject: [PATCH 274/519] Fix negotation of flate parameters --- accept.go | 29 ++--------------------------- accept_test.go | 1 - autobahn_test.go | 1 + compress_notjs.go | 14 ++++---------- dial.go | 45 +++++++++++++++------------------------------ dial_test.go | 2 +- 6 files changed, 23 insertions(+), 69 deletions(-) diff --git a/accept.go b/accept.go index 5a162de0..ccda3261 100644 --- a/accept.go +++ b/accept.go @@ -209,7 +209,6 @@ func acceptCompression(r *http.Request, w http.ResponseWriter, mode CompressionM func acceptDeflate(w http.ResponseWriter, ext websocketExtension, mode CompressionMode) (*compressionOptions, error) { copts := mode.opts() - copts.serverMaxWindowBits = 8 for _, p := range ext.params { switch p { @@ -222,26 +221,7 @@ func acceptDeflate(w http.ResponseWriter, ext websocketExtension, mode Compressi } if strings.HasPrefix(p, "client_max_window_bits") { - continue - - // bits, ok := parseExtensionParameter(p, 15) - // if !ok || bits < 8 || bits > 16 { - // err := fmt.Errorf("invalid client_max_window_bits: %q", p) - // http.Error(w, err.Error(), http.StatusBadRequest) - // return nil, err - // } - // copts.clientMaxWindowBits = bits - // continue - } - - if false && strings.HasPrefix(p, "server_max_window_bits") { - // We always send back 8 but make sure to validate. - bits, ok := parseExtensionParameter(p, 0) - if !ok || bits < 8 || bits > 16 { - err := fmt.Errorf("invalid server_max_window_bits: %q", p) - http.Error(w, err.Error(), http.StatusBadRequest) - return nil, err - } + // We cannot adjust the read sliding window so cannot make use of this. continue } @@ -256,14 +236,9 @@ func acceptDeflate(w http.ResponseWriter, ext websocketExtension, mode Compressi } // parseExtensionParameter parses the value in the extension parameter p. -// It falls back to defaultVal if there is no value. -// If defaultVal == 0, then ok == false if there is no value. -func parseExtensionParameter(p string, defaultVal int) (int, bool) { +func parseExtensionParameter(p string) (int, bool) { ps := strings.Split(p, "=") if len(ps) == 1 { - if defaultVal > 0 { - return defaultVal, true - } return 0, false } i, e := strconv.Atoi(strings.Trim(ps[1], `"`)) diff --git a/accept_test.go b/accept_test.go index 555f0dc0..523d4685 100644 --- a/accept_test.go +++ b/accept_test.go @@ -327,7 +327,6 @@ func Test_acceptCompression(t *testing.T) { expCopts: &compressionOptions{ clientNoContextTakeover: true, serverNoContextTakeover: true, - serverMaxWindowBits: 8, }, }, { diff --git a/autobahn_test.go b/autobahn_test.go index 50473534..e56a4912 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -28,6 +28,7 @@ var excludedAutobahnCases = []string{ // We skip the tests related to requestMaxWindowBits as that is unimplemented due // to limitations in compress/flate. See https://github.com/golang/go/issues/3155 + // Same with klauspost/compress which doesn't allow adjusting the sliding window size. "13.3.*", "13.4.*", "13.5.*", "13.6.*", } diff --git a/compress_notjs.go b/compress_notjs.go index ef82eb4d..809a272c 100644 --- a/compress_notjs.go +++ b/compress_notjs.go @@ -3,7 +3,6 @@ package websocket import ( - "fmt" "io" "net/http" "sync" @@ -20,10 +19,7 @@ func (m CompressionMode) opts() *compressionOptions { type compressionOptions struct { clientNoContextTakeover bool - clientMaxWindowBits int - serverNoContextTakeover bool - serverMaxWindowBits int } func (copts *compressionOptions) setHeader(h http.Header) { @@ -34,12 +30,6 @@ func (copts *compressionOptions) setHeader(h http.Header) { if copts.serverNoContextTakeover { s += "; server_no_context_takeover" } - if false && copts.serverMaxWindowBits > 0 { - s += fmt.Sprintf("; server_max_window_bits=%v", copts.serverMaxWindowBits) - } - if false && copts.clientMaxWindowBits > 0 { - s += fmt.Sprintf("; client_max_window_bits=%v", copts.clientMaxWindowBits) - } h.Set("Sec-WebSocket-Extensions", s) } @@ -147,6 +137,10 @@ func (sw *slidingWindow) init(n int) { return } + if n == 0 { + n = 32768 + } + p := slidingWindowPool(n) buf, ok := p.Get().([]byte) if ok { diff --git a/dial.go b/dial.go index 8ff39597..f882f122 100644 --- a/dial.go +++ b/dial.go @@ -82,7 +82,12 @@ func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) ( return nil, nil, fmt.Errorf("failed to generate Sec-WebSocket-Key: %w", err) } - resp, err := handshakeRequest(ctx, urls, opts, secWebSocketKey) + var copts *compressionOptions + if opts.CompressionMode != CompressionDisabled { + copts = opts.CompressionMode.opts() + } + + resp, err := handshakeRequest(ctx, urls, opts, copts, secWebSocketKey) if err != nil { return nil, resp, err } @@ -104,7 +109,7 @@ func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) ( } }() - copts, err := verifyServerResponse(opts, secWebSocketKey, resp) + copts, err = verifyServerResponse(opts, copts, secWebSocketKey, resp) if err != nil { return nil, resp, err } @@ -125,7 +130,7 @@ func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) ( }), resp, nil } -func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, secWebSocketKey string) (*http.Response, error) { +func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, copts *compressionOptions, secWebSocketKey string) (*http.Response, error) { if opts.HTTPClient.Timeout > 0 { return nil, errors.New("use context for cancellation instead of http.Client.Timeout; see https://github.com/nhooyr/websocket/issues/67") } @@ -153,9 +158,7 @@ func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, secWe if len(opts.Subprotocols) > 0 { req.Header.Set("Sec-WebSocket-Protocol", strings.Join(opts.Subprotocols, ",")) } - if opts.CompressionMode != CompressionDisabled { - copts := opts.CompressionMode.opts() - copts.clientMaxWindowBits = 8 + if copts != nil { copts.setHeader(req.Header) } @@ -178,7 +181,7 @@ func secWebSocketKey(rr io.Reader) (string, error) { return base64.StdEncoding.EncodeToString(b), nil } -func verifyServerResponse(opts *DialOptions, secWebSocketKey string, resp *http.Response) (*compressionOptions, error) { +func verifyServerResponse(opts *DialOptions, copts *compressionOptions, secWebSocketKey string, resp *http.Response) (*compressionOptions, error) { if resp.StatusCode != http.StatusSwitchingProtocols { return nil, fmt.Errorf("expected handshake response status code %v but got %v", http.StatusSwitchingProtocols, resp.StatusCode) } @@ -203,7 +206,7 @@ func verifyServerResponse(opts *DialOptions, secWebSocketKey string, resp *http. return nil, err } - return verifyServerExtensions(resp.Header) + return verifyServerExtensions(copts, resp.Header) } func verifySubprotocol(subprotos []string, resp *http.Response) error { @@ -221,19 +224,19 @@ func verifySubprotocol(subprotos []string, resp *http.Response) error { return fmt.Errorf("WebSocket protocol violation: unexpected Sec-WebSocket-Protocol from server: %q", proto) } -func verifyServerExtensions(h http.Header) (*compressionOptions, error) { +func verifyServerExtensions(copts *compressionOptions, h http.Header) (*compressionOptions, error) { exts := websocketExtensions(h) if len(exts) == 0 { return nil, nil } ext := exts[0] - if ext.name != "permessage-deflate" || len(exts) > 1 { + if ext.name != "permessage-deflate" || len(exts) > 1 || copts == nil { return nil, fmt.Errorf("WebSocket protcol violation: unsupported extensions from server: %+v", exts[1:]) } - copts := &compressionOptions{} - copts.clientMaxWindowBits = 8 + copts = &*copts + for _, p := range ext.params { switch p { case "client_no_context_takeover": @@ -244,24 +247,6 @@ func verifyServerExtensions(h http.Header) (*compressionOptions, error) { continue } - if false && strings.HasPrefix(p, "server_max_window_bits") { - bits, ok := parseExtensionParameter(p, 0) - if !ok || bits < 8 || bits > 16 { - return nil, fmt.Errorf("invalid server_max_window_bits: %q", p) - } - copts.serverMaxWindowBits = bits - continue - } - - if false && strings.HasPrefix(p, "client_max_window_bits") { - bits, ok := parseExtensionParameter(p, 0) - if !ok || bits < 8 || bits > 16 { - return nil, fmt.Errorf("invalid client_max_window_bits: %q", p) - } - copts.clientMaxWindowBits = 8 - continue - } - return nil, fmt.Errorf("unsupported permessage-deflate parameter: %q", p) } diff --git a/dial_test.go b/dial_test.go index 06084cc5..7f13a934 100644 --- a/dial_test.go +++ b/dial_test.go @@ -221,7 +221,7 @@ func Test_verifyServerHandshake(t *testing.T) { opts := &DialOptions{ Subprotocols: strings.Split(r.Header.Get("Sec-WebSocket-Protocol"), ","), } - _, err = verifyServerResponse(opts, key, resp) + _, err = verifyServerResponse(opts, opts.CompressionMode.opts(), key, resp) if tc.success { assert.Success(t, err) } else { From 7eb511956b173b8c9db1111402518703db1029d4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 16 Feb 2020 18:50:14 -0500 Subject: [PATCH 275/519] Fix bug in headerTokens Closes #189 --- accept.go | 1 + accept_test.go | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/accept.go b/accept.go index ccda3261..479138fc 100644 --- a/accept.go +++ b/accept.go @@ -325,6 +325,7 @@ func headerTokens(h http.Header, key string) []string { v = strings.TrimSpace(v) for _, t := range strings.Split(v, ",") { t = strings.ToLower(t) + t = strings.TrimSpace(t) tokens = append(tokens, t) } } diff --git a/accept_test.go b/accept_test.go index 523d4685..49667799 100644 --- a/accept_test.go +++ b/accept_test.go @@ -155,7 +155,7 @@ func Test_verifyClientHandshake(t *testing.T) { { name: "success", h: map[string]string{ - "Connection": "Upgrade", + "Connection": "keep-alive, Upgrade", "Upgrade": "websocket", "Sec-WebSocket-Version": "13", "Sec-WebSocket-Key": "meow123", From 87c97177d93eff245802f7c44c7546d53039d897 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 16 Feb 2020 21:07:34 -0500 Subject: [PATCH 276/519] Curb CI failure https://github.com/nhooyr/websocket/runs/449477040 seems to have timed out. --- conn_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conn_test.go b/conn_test.go index 64e6736f..a7bfba0a 100644 --- a/conn_test.go +++ b/conn_test.go @@ -329,7 +329,7 @@ func newConnTest(t testing.TB, dialOpts *websocket.DialOptions, acceptOpts *webs } t.Helper() - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) tt = &connTest{t: t, ctx: ctx} tt.appendDone(cancel) From ba1c24d1c11237e77e6817b63f1203dfa18402ad Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 16 Feb 2020 21:03:43 -0500 Subject: [PATCH 277/519] Add chat example Closes #174 --- ci/fmt.mk | 2 +- example-chat/chat.go | 94 +++++++++++++++++++++++++++++++++++++++++ example-chat/go.mod | 5 +++ example-chat/go.sum | 12 ++++++ example-chat/index.css | 67 +++++++++++++++++++++++++++++ example-chat/index.html | 23 ++++++++++ example-chat/index.js | 52 +++++++++++++++++++++++ example-chat/main.go | 38 +++++++++++++++++ 8 files changed, 292 insertions(+), 1 deletion(-) create mode 100644 example-chat/chat.go create mode 100644 example-chat/go.mod create mode 100644 example-chat/go.sum create mode 100644 example-chat/index.css create mode 100644 example-chat/index.html create mode 100644 example-chat/index.js create mode 100644 example-chat/main.go diff --git a/ci/fmt.mk b/ci/fmt.mk index f313562c..3512d02f 100644 --- a/ci/fmt.mk +++ b/ci/fmt.mk @@ -13,7 +13,7 @@ goimports: gen goimports -w "-local=$$(go list -m)" . prettier: - prettier --write --print-width=120 --no-semi --trailing-comma=all --loglevel=warn $$(git ls-files "*.yml" "*.md") + prettier --write --print-width=120 --no-semi --trailing-comma=all --loglevel=warn $$(git ls-files "*.yml" "*.md" "*.js" "*.css" "*.html") gen: stringer -type=opcode,MessageType,StatusCode -output=stringer.go diff --git a/example-chat/chat.go b/example-chat/chat.go new file mode 100644 index 00000000..fcd6290b --- /dev/null +++ b/example-chat/chat.go @@ -0,0 +1,94 @@ +package main + +import ( + "context" + "io" + "io/ioutil" + "log" + "net/http" + "sync" + "time" + + "nhooyr.io/websocket" +) + +type chatServer struct { + subscribersMu sync.RWMutex + subscribers map[chan []byte]struct{} +} + +func (cs *chatServer) subscribeHandler(w http.ResponseWriter, r *http.Request) { + println("HELLO") + + c, err := websocket.Accept(w, r, nil) + if err != nil { + log.Print(err) + return + } + + cs.subscribe(r.Context(), c) +} + +func (cs *chatServer) publishHandler(w http.ResponseWriter, r *http.Request) { + body := io.LimitReader(r.Body, 8192) + msg, err := ioutil.ReadAll(body) + if err != nil { + return + } + + cs.publish(msg) +} + +func (cs *chatServer) publish(msg []byte) { + cs.subscribersMu.RLock() + defer cs.subscribersMu.RUnlock() + + for c := range cs.subscribers { + select { + case c <- msg: + default: + } + } +} + +func (cs *chatServer) addSubscriber(msgs chan []byte) { + cs.subscribersMu.Lock() + if cs.subscribers == nil { + cs.subscribers = make(map[chan []byte]struct{}) + } + cs.subscribers[msgs] = struct{}{} + cs.subscribersMu.Unlock() +} + +func (cs *chatServer) deleteSubscriber(msgs chan []byte) { + cs.subscribersMu.Lock() + delete(cs.subscribers, msgs) + cs.subscribersMu.Unlock() +} + +func (cs *chatServer) subscribe(ctx context.Context, c *websocket.Conn) error { + ctx = c.CloseRead(ctx) + + msgs := make(chan []byte, 16) + cs.addSubscriber(msgs) + defer cs.deleteSubscriber(msgs) + + for { + select { + case msg := <-msgs: + err := writeTimeout(ctx, time.Second*5, c, msg) + if err != nil { + return err + } + case <-ctx.Done(): + return ctx.Err() + } + } +} + +func writeTimeout(ctx context.Context, timeout time.Duration, c *websocket.Conn, msg []byte) error { + ctx, cancel := context.WithTimeout(ctx, timeout) + defer cancel() + + return c.Write(ctx, websocket.MessageText, msg) +} diff --git a/example-chat/go.mod b/example-chat/go.mod new file mode 100644 index 00000000..34fa5a69 --- /dev/null +++ b/example-chat/go.mod @@ -0,0 +1,5 @@ +module nhooyr.io/websocket/example-chat + +go 1.13 + +require nhooyr.io/websocket v1.8.2 diff --git a/example-chat/go.sum b/example-chat/go.sum new file mode 100644 index 00000000..0755fca5 --- /dev/null +++ b/example-chat/go.sum @@ -0,0 +1,12 @@ +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/klauspost/compress v1.10.0 h1:92XGj1AcYzA6UrVdd4qIIBrT8OroryvRvdmg/IfmC7Y= +github.com/klauspost/compress v1.10.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +nhooyr.io/websocket v1.8.2 h1:LwdzfyyOZKtVFoXay6A39Acu03KmidSZ3YUUvPa13PA= +nhooyr.io/websocket v1.8.2/go.mod h1:LiqdCg1Cu7TPWxEvPjPa0TGYxCsy4pHNTN9gGluwBpQ= diff --git a/example-chat/index.css b/example-chat/index.css new file mode 100644 index 00000000..2c2b0419 --- /dev/null +++ b/example-chat/index.css @@ -0,0 +1,67 @@ +body { + width: 100vw; + height: 100vh; + min-width: 320px; +} + +#root { + padding: 20px; + max-width: 500px; + margin: auto; + max-height: 100vh; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +#root > * + * { + margin: 20px 0 0 0; +} + +#message-log { + width: 100%; + height: 100vh; + flex-grow: 1; + overflow: auto; +} + +#message-log p:first-child { + margin-top: 0; + margin-bottom: 0; +} + +#message-log > * + * { + margin: 10px 0 0 0; +} + +#publish-form { + appearance: none; + + display: flex; + align-items: center; + justify-content: center; + width: 100%; +} + +#publish-form input[type="text"] { + flex-grow: 1; + word-break: normal; + border-radius: 5px; +} + +#publish-form input[type="submit"] { + color: white; + background-color: black; + border-radius: 5px; + margin-left: 10px; +} + +#publish-form input[type="submit"]:hover { + background-color: red; +} + +#publish-form input[type="submit"]:active { + background-color: red; +} diff --git a/example-chat/index.html b/example-chat/index.html new file mode 100644 index 00000000..3c69dd64 --- /dev/null +++ b/example-chat/index.html @@ -0,0 +1,23 @@ + + + + + nhooyr.io/websocket - Chat Example + + + + + + + + +
+
+
+ + +
+
+ + + diff --git a/example-chat/index.js b/example-chat/index.js new file mode 100644 index 00000000..2ea64db8 --- /dev/null +++ b/example-chat/index.js @@ -0,0 +1,52 @@ +;(() => { + let conn + let submitted = false + function dial() { + conn = new WebSocket(`ws://${location.host}/subscribe`) + + conn.addEventListener("close", () => { + conn = undefined + setTimeout(dial, 1000) + }) + conn.addEventListener("message", ev => { + if (typeof ev.data !== "string") { + return + } + appendLog(ev.data) + if (submitted) { + messageLog.scrollTo(0, messageLog.scrollHeight) + submitted = false + } + }) + + return conn + } + dial() + + const messageLog = document.getElementById("message-log") + const publishForm = document.getElementById("publish-form") + const messageInput = document.getElementById("message-input") + + function appendLog(text) { + const p = document.createElement("p") + p.innerText = `${new Date().toLocaleTimeString()}: ${text}` + messageLog.append(p) + } + appendLog("Submit a message to get started!") + + publishForm.onsubmit = ev => { + ev.preventDefault() + + const msg = messageInput.value + if (msg === "") { + return + } + messageInput.value = "" + + submitted = true + fetch("/publish", { + method: "POST", + body: msg, + }) + } +})() diff --git a/example-chat/main.go b/example-chat/main.go new file mode 100644 index 00000000..7a517242 --- /dev/null +++ b/example-chat/main.go @@ -0,0 +1,38 @@ +package main + +import ( + "fmt" + "log" + "net" + "net/http" + "time" +) + +func main() { + err := run() + if err != nil { + log.Fatal(err) + } +} + +func run() error { + l, err := net.Listen("tcp", "localhost:0") + if err != nil { + return err + } + fmt.Printf("listening on http://%v\n", l.Addr()) + + var ws chatServer + + m := http.NewServeMux() + m.Handle("/", http.FileServer(http.Dir("."))) + m.HandleFunc("/subscribe", ws.subscribeHandler) + m.HandleFunc("/publish", ws.publishHandler) + + s := http.Server{ + Handler: m, + ReadTimeout: time.Second * 10, + WriteTimeout: time.Second * 10, + } + return s.Serve(l) +} From bdae16ee1ec348292b42f8b288d541d1ce3be77f Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 16 Feb 2020 21:17:34 -0500 Subject: [PATCH 278/519] Add docs to chat example --- README.md | 2 ++ example/README.md | 25 +++++++++++++++++++++++++ {example-chat => example}/chat.go | 6 ++++-- {example-chat => example}/go.mod | 0 {example-chat => example}/go.sum | 0 {example-chat => example}/index.css | 0 {example-chat => example}/index.html | 0 {example-chat => example}/index.js | 20 +++++++++++--------- {example-chat => example}/main.go | 0 9 files changed, 42 insertions(+), 11 deletions(-) create mode 100644 example/README.md rename {example-chat => example}/chat.go (93%) rename {example-chat => example}/go.mod (100%) rename {example-chat => example}/go.sum (100%) rename {example-chat => example}/index.css (100%) rename {example-chat => example}/index.html (100%) rename {example-chat => example}/index.js (68%) rename {example-chat => example}/main.go (100%) diff --git a/README.md b/README.md index 5dddf84a..305ccddb 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ go get nhooyr.io/websocket For a production quality example that demonstrates the complete API, see the [echo example](https://godoc.org/nhooyr.io/websocket#example-package--Echo). +For a full stack example, see the [./example](./example) subdirectory which contains a full chat example. + ### Server ```go diff --git a/example/README.md b/example/README.md new file mode 100644 index 00000000..ce5a1f1c --- /dev/null +++ b/example/README.md @@ -0,0 +1,25 @@ +# Chat Example + +This directory contains a full stack example +of a simple chat webapp using nhooyr.io/websocket. + +```bash +$ cd example +$ go run . +listening on http://127.0.0.1:51055 +``` + +Visit the printed URL to submit and view broadcasted messages in a browser. + +![Image of Example](https://i.imgur.com/iSdpZFT.png) + +## Structure + +The frontend is contained in `index.html`, `index.js` and `index.css`. It setups the +DOM with a form at the buttom to submit messages and at the top is a scrollable div +that is populated with new messages as they are broadcast. The messages are received +via a WebSocket and messages are published via a POST HTTP endpoint. + +The server portion is `main.go` and `chat.go` and implements serving the static frontend +assets as well as the `/subscribe` WebSocket endpoint for subscribing to +broadcast messages and `/publish` for publishing messages. diff --git a/example-chat/chat.go b/example/chat.go similarity index 93% rename from example-chat/chat.go rename to example/chat.go index fcd6290b..9aa36886 100644 --- a/example-chat/chat.go +++ b/example/chat.go @@ -18,8 +18,6 @@ type chatServer struct { } func (cs *chatServer) subscribeHandler(w http.ResponseWriter, r *http.Request) { - println("HELLO") - c, err := websocket.Accept(w, r, nil) if err != nil { log.Print(err) @@ -30,6 +28,10 @@ func (cs *chatServer) subscribeHandler(w http.ResponseWriter, r *http.Request) { } func (cs *chatServer) publishHandler(w http.ResponseWriter, r *http.Request) { + if r.Method != "POST" { + http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) + return + } body := io.LimitReader(r.Body, 8192) msg, err := ioutil.ReadAll(body) if err != nil { diff --git a/example-chat/go.mod b/example/go.mod similarity index 100% rename from example-chat/go.mod rename to example/go.mod diff --git a/example-chat/go.sum b/example/go.sum similarity index 100% rename from example-chat/go.sum rename to example/go.sum diff --git a/example-chat/index.css b/example/index.css similarity index 100% rename from example-chat/index.css rename to example/index.css diff --git a/example-chat/index.html b/example/index.html similarity index 100% rename from example-chat/index.html rename to example/index.html diff --git a/example-chat/index.js b/example/index.js similarity index 68% rename from example-chat/index.js rename to example/index.js index 2ea64db8..32e13c90 100644 --- a/example-chat/index.js +++ b/example/index.js @@ -1,25 +1,25 @@ ;(() => { let conn - let submitted = false + let expectingMessage = false function dial() { conn = new WebSocket(`ws://${location.host}/subscribe`) - conn.addEventListener("close", () => { - conn = undefined + conn.addEventListener("close", (ev) => { + console.error("subscribe WebSocket closed", ev) + console.info("reconnecting in 1000ms", ev) setTimeout(dial, 1000) }) conn.addEventListener("message", ev => { if (typeof ev.data !== "string") { + console.error("unexpected message type", typeof ev.data) return } appendLog(ev.data) - if (submitted) { + if (expectingMessage) { messageLog.scrollTo(0, messageLog.scrollHeight) - submitted = false + expectingMessage = false } }) - - return conn } dial() @@ -34,7 +34,7 @@ } appendLog("Submit a message to get started!") - publishForm.onsubmit = ev => { + publishForm.onsubmit = async ev => { ev.preventDefault() const msg = messageInput.value @@ -43,10 +43,12 @@ } messageInput.value = "" - submitted = true + expectingMessage = true fetch("/publish", { method: "POST", body: msg, + }).catch(err => { + console.error("failed to publish", err) }) } })() diff --git a/example-chat/main.go b/example/main.go similarity index 100% rename from example-chat/main.go rename to example/main.go From 2c8283379a032055e9057b83cd7f0ef3f54b7361 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 16 Feb 2020 22:15:14 -0500 Subject: [PATCH 279/519] Make chat example responsive --- example/README.md | 2 +- example/index.css | 40 +++++++++++++++++++++++++++------------- example/index.html | 12 +++++++----- example/index.js | 15 ++++++++------- example/main.go | 13 ++++++++++--- 5 files changed, 53 insertions(+), 29 deletions(-) diff --git a/example/README.md b/example/README.md index ce5a1f1c..24544b44 100644 --- a/example/README.md +++ b/example/README.md @@ -5,7 +5,7 @@ of a simple chat webapp using nhooyr.io/websocket. ```bash $ cd example -$ go run . +$ go run . localhost:0 listening on http://127.0.0.1:51055 ``` diff --git a/example/index.css b/example/index.css index 2c2b0419..29804662 100644 --- a/example/index.css +++ b/example/index.css @@ -1,14 +1,13 @@ body { width: 100vw; - height: 100vh; min-width: 320px; } #root { - padding: 20px; - max-width: 500px; + padding: 40px 20px; + max-width: 480px; margin: auto; - max-height: 100vh; + height: 100vh; display: flex; flex-direction: column; @@ -20,42 +19,57 @@ body { margin: 20px 0 0 0; } +/* 100vh on safari does not include the bottom bar. */ +@supports (-webkit-overflow-scrolling: touch) { + #root { + height: 85vh; + } +} + #message-log { width: 100%; - height: 100vh; flex-grow: 1; overflow: auto; } #message-log p:first-child { - margin-top: 0; - margin-bottom: 0; + margin: 0; } #message-log > * + * { margin: 10px 0 0 0; } -#publish-form { - appearance: none; +#publish-form-container { + width: 100%; +} - display: flex; - align-items: center; - justify-content: center; +#publish-form { width: 100%; + display: flex; + height: 40px; +} + +#publish-form > * + * { + margin: 0 0 0 10px; } #publish-form input[type="text"] { flex-grow: 1; + + -moz-appearance: none; + -webkit-appearance: none; word-break: normal; border-radius: 5px; + border: 1px solid #ccc; } #publish-form input[type="submit"] { color: white; background-color: black; border-radius: 5px; - margin-left: 10px; + padding: 5px 10px; + border: none; } #publish-form input[type="submit"]:hover { diff --git a/example/index.html b/example/index.html index 3c69dd64..3b6e75c5 100644 --- a/example/index.html +++ b/example/index.html @@ -5,18 +5,20 @@ nhooyr.io/websocket - Chat Example - +
-
- - -
+
+
+ + +
+
diff --git a/example/index.js b/example/index.js index 32e13c90..61e938c1 100644 --- a/example/index.js +++ b/example/index.js @@ -4,19 +4,21 @@ function dial() { conn = new WebSocket(`ws://${location.host}/subscribe`) - conn.addEventListener("close", (ev) => { - console.error("subscribe WebSocket closed", ev) - console.info("reconnecting in 1000ms", ev) + conn.addEventListener("close", ev => { + console.info("websocket disconnected, reconnecting in 1000ms", ev) setTimeout(dial, 1000) }) + conn.addEventListener("open", ev => { + console.info("websocket connected") + }) conn.addEventListener("message", ev => { if (typeof ev.data !== "string") { console.error("unexpected message type", typeof ev.data) return } - appendLog(ev.data) + const p = appendLog(ev.data) if (expectingMessage) { - messageLog.scrollTo(0, messageLog.scrollHeight) + p.scrollIntoView() expectingMessage = false } }) @@ -31,6 +33,7 @@ const p = document.createElement("p") p.innerText = `${new Date().toLocaleTimeString()}: ${text}` messageLog.append(p) + return p } appendLog("Submit a message to get started!") @@ -47,8 +50,6 @@ fetch("/publish", { method: "POST", body: msg, - }).catch(err => { - console.error("failed to publish", err) }) } })() diff --git a/example/main.go b/example/main.go index 7a517242..af643f26 100644 --- a/example/main.go +++ b/example/main.go @@ -1,14 +1,17 @@ package main import ( - "fmt" + "errors" "log" "net" "net/http" + "os" "time" ) func main() { + log.SetFlags(0) + err := run() if err != nil { log.Fatal(err) @@ -16,11 +19,15 @@ func main() { } func run() error { - l, err := net.Listen("tcp", "localhost:0") + if len(os.Args) < 2 { + return errors.New("please provide an address to listen on as the first argument") + } + + l, err := net.Listen("tcp", os.Args[1]) if err != nil { return err } - fmt.Printf("listening on http://%v\n", l.Addr()) + log.Printf("listening on http://%v", l.Addr()) var ws chatServer From 4908f7846fcd785df64d1ed1070826ea282568ab Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 16 Feb 2020 22:26:04 -0500 Subject: [PATCH 280/519] Cleanup example README --- example/README.md | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/example/README.md b/example/README.md index 24544b44..8fb2b45b 100644 --- a/example/README.md +++ b/example/README.md @@ -1,7 +1,6 @@ # Chat Example -This directory contains a full stack example -of a simple chat webapp using nhooyr.io/websocket. +This directory contains a full stack example of a simple chat webapp using nhooyr.io/websocket. ```bash $ cd example @@ -15,11 +14,11 @@ Visit the printed URL to submit and view broadcasted messages in a browser. ## Structure -The frontend is contained in `index.html`, `index.js` and `index.css`. It setups the -DOM with a form at the buttom to submit messages and at the top is a scrollable div -that is populated with new messages as they are broadcast. The messages are received -via a WebSocket and messages are published via a POST HTTP endpoint. +The frontend is contained in `index.html`, `index.js` and `index.css`. It sets up the +DOM with a scrollable div at the top that is populated with new messages as they are broadcast. +At the bottom it adds a form to submit messages. +The messages are received via the WebSocket `/subscribe` endpoint and published via +the HTTP POST `/publish` endpoint. The server portion is `main.go` and `chat.go` and implements serving the static frontend -assets as well as the `/subscribe` WebSocket endpoint for subscribing to -broadcast messages and `/publish` for publishing messages. +assets, the `/subscribe` WebSocket endpoint and the HTTP POST `/publish` endpoint. From 8c89e66f640c5f857f65d89f9db374c34d1489d5 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 16 Feb 2020 22:28:05 -0500 Subject: [PATCH 281/519] Shorten example screenshot --- example/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/example/README.md b/example/README.md index 8fb2b45b..abc1ac8f 100644 --- a/example/README.md +++ b/example/README.md @@ -10,7 +10,7 @@ listening on http://127.0.0.1:51055 Visit the printed URL to submit and view broadcasted messages in a browser. -![Image of Example](https://i.imgur.com/iSdpZFT.png) +![Image of Example](https://i.imgur.com/VwJl9Bh.png) ## Structure From f8afe038afe6c68cdf4f1cafe044f75f43a5946b Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 16 Feb 2020 22:54:18 -0500 Subject: [PATCH 282/519] Comment chat example --- README.md | 2 +- conn_test.go | 2 +- example/README.md | 3 ++ example/chat.go | 79 +++++++++++++++++++++++++++++++++-------------- example/index.js | 13 ++++++-- example/main.go | 2 ++ 6 files changed, 72 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 305ccddb..109d0b04 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ go get nhooyr.io/websocket For a production quality example that demonstrates the complete API, see the [echo example](https://godoc.org/nhooyr.io/websocket#example-package--Echo). -For a full stack example, see the [./example](./example) subdirectory which contains a full chat example. +For a full stack example, see the [./example](./example) subdirectory which contains a chat example with a browser client. ### Server diff --git a/conn_test.go b/conn_test.go index a7bfba0a..14b7efc4 100644 --- a/conn_test.go +++ b/conn_test.go @@ -329,7 +329,7 @@ func newConnTest(t testing.TB, dialOpts *websocket.DialOptions, acceptOpts *webs } t.Helper() - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + ctx, cancel := context.WithTimeout(context.Background(), time.Second * 30) tt = &connTest{t: t, ctx: ctx} tt.appendDone(cancel) diff --git a/example/README.md b/example/README.md index abc1ac8f..a0ae0cca 100644 --- a/example/README.md +++ b/example/README.md @@ -22,3 +22,6 @@ the HTTP POST `/publish` endpoint. The server portion is `main.go` and `chat.go` and implements serving the static frontend assets, the `/subscribe` WebSocket endpoint and the HTTP POST `/publish` endpoint. + +The code is well commented. I would recommend starting in `main.go` and then `chat.go` followed by +`index.html` and then `index.js`. diff --git a/example/chat.go b/example/chat.go index 9aa36886..b6910053 100644 --- a/example/chat.go +++ b/example/chat.go @@ -2,6 +2,7 @@ package main import ( "context" + "errors" "io" "io/ioutil" "log" @@ -12,11 +13,14 @@ import ( "nhooyr.io/websocket" ) +// chatServer enables broadcasting to a set of subscribers. type chatServer struct { subscribersMu sync.RWMutex - subscribers map[chan []byte]struct{} + subscribers map[chan<- []byte]struct{} } +// subscribeHandler accepts the WebSocket connection and then subscribes +// it to all future messages. func (cs *chatServer) subscribeHandler(w http.ResponseWriter, r *http.Request) { c, err := websocket.Accept(w, r, nil) if err != nil { @@ -24,9 +28,21 @@ func (cs *chatServer) subscribeHandler(w http.ResponseWriter, r *http.Request) { return } - cs.subscribe(r.Context(), c) + err = cs.subscribe(r.Context(), c) + if errors.Is(err, context.Canceled) { + return + } + if websocket.CloseStatus(err) == websocket.StatusNormalClosure || + websocket.CloseStatus(err) == websocket.StatusGoingAway { + return + } + if err != nil { + log.Print(err) + } } +// publishHandler reads the request body with a limit of 8192 bytes and then publishes +// the received message. func (cs *chatServer) publishHandler(w http.ResponseWriter, r *http.Request) { if r.Method != "POST" { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) @@ -35,12 +51,44 @@ func (cs *chatServer) publishHandler(w http.ResponseWriter, r *http.Request) { body := io.LimitReader(r.Body, 8192) msg, err := ioutil.ReadAll(body) if err != nil { + http.Error(w, http.StatusText(http.StatusRequestEntityTooLarge), http.StatusRequestEntityTooLarge) return } cs.publish(msg) } +// subscribe subscribes the given WebSocket to all broadcast messages. +// It creates a msgs chan with a buffer of 16 to give some room to slower +// connections and then registers it. It then listens for all messages +// and writes them to the WebSocket. If the context is cancelled or +// an error occurs, it returns and deletes the subscription. +// +// It uses CloseRead to keep reading from the connection to process control +// messages and cancel the context if the connection drops. +func (cs *chatServer) subscribe(ctx context.Context, c *websocket.Conn) error { + ctx = c.CloseRead(ctx) + + msgs := make(chan []byte, 16) + cs.addSubscriber(msgs) + defer cs.deleteSubscriber(msgs) + + for { + select { + case msg := <-msgs: + err := writeTimeout(ctx, time.Second*5, c, msg) + if err != nil { + return err + } + case <-ctx.Done(): + return ctx.Err() + } + } +} + +// publish publishes the msg to all subscribers. +// It never blocks and so messages to slow subscribers +// are dropped. func (cs *chatServer) publish(msg []byte) { cs.subscribersMu.RLock() defer cs.subscribersMu.RUnlock() @@ -53,41 +101,24 @@ func (cs *chatServer) publish(msg []byte) { } } -func (cs *chatServer) addSubscriber(msgs chan []byte) { +// addSubscriber registers a subscriber with a channel +// on which to send messages. +func (cs *chatServer) addSubscriber(msgs chan<- []byte) { cs.subscribersMu.Lock() if cs.subscribers == nil { - cs.subscribers = make(map[chan []byte]struct{}) + cs.subscribers = make(map[chan<- []byte]struct{}) } cs.subscribers[msgs] = struct{}{} cs.subscribersMu.Unlock() } +// deleteSubscriber deletes the subscriber with the given msgs channel. func (cs *chatServer) deleteSubscriber(msgs chan []byte) { cs.subscribersMu.Lock() delete(cs.subscribers, msgs) cs.subscribersMu.Unlock() } -func (cs *chatServer) subscribe(ctx context.Context, c *websocket.Conn) error { - ctx = c.CloseRead(ctx) - - msgs := make(chan []byte, 16) - cs.addSubscriber(msgs) - defer cs.deleteSubscriber(msgs) - - for { - select { - case msg := <-msgs: - err := writeTimeout(ctx, time.Second*5, c, msg) - if err != nil { - return err - } - case <-ctx.Done(): - return ctx.Err() - } - } -} - func writeTimeout(ctx context.Context, timeout time.Duration, c *websocket.Conn, msg []byte) error { ctx, cancel := context.WithTimeout(ctx, timeout) defer cancel() diff --git a/example/index.js b/example/index.js index 61e938c1..8fb3dfb8 100644 --- a/example/index.js +++ b/example/index.js @@ -1,8 +1,10 @@ ;(() => { - let conn + // expectingMessage is set to true + // if the user has just submitted a message + // and so we should scroll the next message into view when received. let expectingMessage = false function dial() { - conn = new WebSocket(`ws://${location.host}/subscribe`) + const conn = new WebSocket(`ws://${location.host}/subscribe`) conn.addEventListener("close", ev => { console.info("websocket disconnected, reconnecting in 1000ms", ev) @@ -11,6 +13,8 @@ conn.addEventListener("open", ev => { console.info("websocket connected") }) + + // This is where we handle messages received. conn.addEventListener("message", ev => { if (typeof ev.data !== "string") { console.error("unexpected message type", typeof ev.data) @@ -29,15 +33,18 @@ const publishForm = document.getElementById("publish-form") const messageInput = document.getElementById("message-input") + // appendLog appends the passed text to messageLog. function appendLog(text) { const p = document.createElement("p") + // Adding a timestamp to each message makes the log easier to read. p.innerText = `${new Date().toLocaleTimeString()}: ${text}` messageLog.append(p) return p } appendLog("Submit a message to get started!") - publishForm.onsubmit = async ev => { + // onsubmit publishes the message from the user when the form is submitted. + publishForm.onsubmit = ev => { ev.preventDefault() const msg = messageInput.value diff --git a/example/main.go b/example/main.go index af643f26..2a520924 100644 --- a/example/main.go +++ b/example/main.go @@ -18,6 +18,8 @@ func main() { } } +// run initializes the chatServer and routes and then +// starts a http.Server for the passed in address. func run() error { if len(os.Args) < 2 { return errors.New("please provide an address to listen on as the first argument") From ce5a002f0ff92d5dd70a8884d65f60831c94ce1b Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 16 Feb 2020 22:57:28 -0500 Subject: [PATCH 283/519] Rename example directory to chat-example --- README.md | 2 +- {example => chat-example}/README.md | 2 +- {example => chat-example}/chat.go | 2 +- {example => chat-example}/go.mod | 0 {example => chat-example}/go.sum | 0 {example => chat-example}/index.css | 0 {example => chat-example}/index.html | 2 +- {example => chat-example}/index.js | 0 {example => chat-example}/main.go | 0 conn_test.go | 2 +- 10 files changed, 5 insertions(+), 5 deletions(-) rename {example => chat-example}/README.md (98%) rename {example => chat-example}/chat.go (98%) rename {example => chat-example}/go.mod (100%) rename {example => chat-example}/go.sum (100%) rename {example => chat-example}/index.css (100%) rename {example => chat-example}/index.html (94%) rename {example => chat-example}/index.js (100%) rename {example => chat-example}/main.go (100%) diff --git a/README.md b/README.md index 109d0b04..e967cd8a 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ go get nhooyr.io/websocket For a production quality example that demonstrates the complete API, see the [echo example](https://godoc.org/nhooyr.io/websocket#example-package--Echo). -For a full stack example, see the [./example](./example) subdirectory which contains a chat example with a browser client. +For a full stack example, see [./chat-example](./chat-example). ### Server diff --git a/example/README.md b/chat-example/README.md similarity index 98% rename from example/README.md rename to chat-example/README.md index a0ae0cca..ef06275d 100644 --- a/example/README.md +++ b/chat-example/README.md @@ -3,7 +3,7 @@ This directory contains a full stack example of a simple chat webapp using nhooyr.io/websocket. ```bash -$ cd example +$ cd chat-example $ go run . localhost:0 listening on http://127.0.0.1:51055 ``` diff --git a/example/chat.go b/chat-example/chat.go similarity index 98% rename from example/chat.go rename to chat-example/chat.go index b6910053..6b23a8a1 100644 --- a/example/chat.go +++ b/chat-example/chat.go @@ -33,7 +33,7 @@ func (cs *chatServer) subscribeHandler(w http.ResponseWriter, r *http.Request) { return } if websocket.CloseStatus(err) == websocket.StatusNormalClosure || - websocket.CloseStatus(err) == websocket.StatusGoingAway { + websocket.CloseStatus(err) == websocket.StatusGoingAway { return } if err != nil { diff --git a/example/go.mod b/chat-example/go.mod similarity index 100% rename from example/go.mod rename to chat-example/go.mod diff --git a/example/go.sum b/chat-example/go.sum similarity index 100% rename from example/go.sum rename to chat-example/go.sum diff --git a/example/index.css b/chat-example/index.css similarity index 100% rename from example/index.css rename to chat-example/index.css diff --git a/example/index.html b/chat-example/index.html similarity index 94% rename from example/index.html rename to chat-example/index.html index 3b6e75c5..e2383a42 100644 --- a/example/index.html +++ b/chat-example/index.html @@ -16,7 +16,7 @@
- +
diff --git a/example/index.js b/chat-example/index.js similarity index 100% rename from example/index.js rename to chat-example/index.js diff --git a/example/main.go b/chat-example/main.go similarity index 100% rename from example/main.go rename to chat-example/main.go diff --git a/conn_test.go b/conn_test.go index 14b7efc4..64e6736f 100644 --- a/conn_test.go +++ b/conn_test.go @@ -329,7 +329,7 @@ func newConnTest(t testing.TB, dialOpts *websocket.DialOptions, acceptOpts *webs } t.Helper() - ctx, cancel := context.WithTimeout(context.Background(), time.Second * 30) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) tt = &connTest{t: t, ctx: ctx} tt.appendDone(cancel) From d44dcb91f1ce2da8356beb9621843df53fbc6ed8 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 17 Feb 2020 01:01:33 -0500 Subject: [PATCH 284/519] chat-example: Add missing defer c.Close --- chat-example/chat.go | 1 + chat-example/index.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/chat-example/chat.go b/chat-example/chat.go index 6b23a8a1..e6e355d0 100644 --- a/chat-example/chat.go +++ b/chat-example/chat.go @@ -27,6 +27,7 @@ func (cs *chatServer) subscribeHandler(w http.ResponseWriter, r *http.Request) { log.Print(err) return } + defer c.Close(websocket.StatusInternalError, "") err = cs.subscribe(r.Context(), c) if errors.Is(err, context.Canceled) { diff --git a/chat-example/index.html b/chat-example/index.html index e2383a42..76ae8370 100644 --- a/chat-example/index.html +++ b/chat-example/index.html @@ -16,7 +16,7 @@
- +
From 9a71667d7302e382ffbfa8f99c932e98dd970e7b Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 18 Feb 2020 00:45:50 -0500 Subject: [PATCH 285/519] Switch from Github Actions to Travis Much simpler config. Github Actions just isn't there yet. --- .github/workflows/ci.yml | 55 ---------------------------------------- .travis.yml | 40 +++++++++++++++++++++++++++++ ci/image/Dockerfile | 18 ------------- close_notjs.go | 4 +-- conn_notjs.go | 23 ++++++++++------- conn_test.go | 4 +++ dial.go | 12 ++++----- read.go | 15 +++++++---- write.go | 15 +++++++---- 9 files changed, 86 insertions(+), 100 deletions(-) delete mode 100644 .github/workflows/ci.yml create mode 100644 .travis.yml delete mode 100644 ci/image/Dockerfile diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 4534425f..00000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: ci -on: [push, pull_request] - -jobs: - fmt: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - uses: actions/cache@v1 - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - name: Run make fmt - uses: ./ci/image - with: - args: make fmt - - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - uses: actions/cache@v1 - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - name: Run make lint - uses: ./ci/image - with: - args: make lint - - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - uses: actions/cache@v1 - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go- - - name: Run make test - uses: ./ci/image - with: - args: make test - env: - COVERALLS_TOKEN: ${{ secrets.COVERALLS_TOKEN }} - - name: Upload coverage.html - uses: actions/upload-artifact@master - with: - name: coverage - path: ci/out/coverage.html diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..41d3c201 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,40 @@ +language: go +go: 1.x +dist: bionic + +env: + global: + - SHFMT_URL=https://github.com/mvdan/sh/releases/download/v3.0.1/shfmt_v3.0.1_linux_amd64 + - GOFLAGS="-mod=readonly" + +jobs: + include: + - name: Format + before_script: + - sudo apt-get install -y npm + - sudo npm install -g prettier + - sudo curl -L "$SHFMT_URL" > /usr/local/bin/shfmt && sudo chmod +x /usr/local/bin/shfmt + - go get golang.org/x/tools/cmd/stringer + - go get golang.org/x/tools/cmd/goimports + script: make -j16 fmt + - name: Lint + before_script: + - sudo apt-get install -y shellcheck + - go get golang.org/x/lint/golint + script: make -j16 lint + - name: Test + before_script: + - sudo apt-get install -y chromium-browser + - go get github.com/agnivade/wasmbrowsertest + - go get github.com/mattn/goveralls + script: make -j16 test + +addons: + apt: + update: true + +cache: + npm: true + directories: + - ~/.cache + - ~/gopath/pkg diff --git a/ci/image/Dockerfile b/ci/image/Dockerfile deleted file mode 100644 index ed408eda..00000000 --- a/ci/image/Dockerfile +++ /dev/null @@ -1,18 +0,0 @@ -FROM golang:1 - -RUN apt-get update -RUN apt-get install -y chromium npm shellcheck - -ARG SHFMT_URL=https://github.com/mvdan/sh/releases/download/v3.0.1/shfmt_v3.0.1_linux_amd64 -RUN curl -L $SHFMT_URL > /usr/local/bin/shfmt && chmod +x /usr/local/bin/shfmt - -ENV GOFLAGS="-mod=readonly" -ENV CI=true -ENV MAKEFLAGS="--jobs=16 --output-sync=target" - -RUN npm install -g prettier -RUN go get golang.org/x/tools/cmd/stringer -RUN go get golang.org/x/tools/cmd/goimports -RUN go get golang.org/x/lint/golint -RUN go get github.com/agnivade/wasmbrowsertest -RUN go get github.com/mattn/goveralls diff --git a/close_notjs.go b/close_notjs.go index c25b088f..25372995 100644 --- a/close_notjs.go +++ b/close_notjs.go @@ -86,11 +86,11 @@ func (c *Conn) waitCloseHandshake() error { ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() - err := c.readMu.Lock(ctx) + err := c.readMu.lock(ctx) if err != nil { return err } - defer c.readMu.Unlock() + defer c.readMu.unlock() if c.readCloseFrameErr != nil { return c.readCloseFrameErr diff --git a/conn_notjs.go b/conn_notjs.go index 7ee60fbc..2ec5f5bf 100644 --- a/conn_notjs.go +++ b/conn_notjs.go @@ -139,16 +139,9 @@ func (c *Conn) close(err error) { c.rwc.Close() go func() { - if c.client { - c.writeFrameMu.Lock(context.Background()) - putBufioWriter(c.bw) - } c.msgWriterState.close() c.msgReader.close() - if c.client { - putBufioReader(c.br) - } }() } @@ -237,7 +230,11 @@ func newMu(c *Conn) *mu { } } -func (m *mu) Lock(ctx context.Context) error { +func (m *mu) forceLock() { + m.ch <- struct{}{} +} + +func (m *mu) lock(ctx context.Context) error { select { case <-m.c.closed: return m.c.closeErr @@ -246,11 +243,19 @@ func (m *mu) Lock(ctx context.Context) error { m.c.close(err) return err case m.ch <- struct{}{}: + // To make sure the connection is certainly alive. + // As it's possible the send on m.ch was selected + // the receive on closed. + select { + case <-m.c.closed: + return m.c.closeErr + default: + } return nil } } -func (m *mu) Unlock() { +func (m *mu) unlock() { select { case <-m.ch: default: diff --git a/conn_test.go b/conn_test.go index 64e6736f..535afe24 100644 --- a/conn_test.go +++ b/conn_test.go @@ -268,6 +268,10 @@ func TestConn(t *testing.T) { func TestWasm(t *testing.T) { t.Parallel() + if os.Getenv("CI") != "" { + t.Skip("skipping on CI") + } + var wg sync.WaitGroup s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { wg.Add(1) diff --git a/dial.go b/dial.go index f882f122..50a0ecce 100644 --- a/dial.go +++ b/dial.go @@ -253,10 +253,10 @@ func verifyServerExtensions(copts *compressionOptions, h http.Header) (*compress return copts, nil } -var readerPool sync.Pool +var bufioReaderPool sync.Pool func getBufioReader(r io.Reader) *bufio.Reader { - br, ok := readerPool.Get().(*bufio.Reader) + br, ok := bufioReaderPool.Get().(*bufio.Reader) if !ok { return bufio.NewReader(r) } @@ -265,13 +265,13 @@ func getBufioReader(r io.Reader) *bufio.Reader { } func putBufioReader(br *bufio.Reader) { - readerPool.Put(br) + bufioReaderPool.Put(br) } -var writerPool sync.Pool +var bufioWriterPool sync.Pool func getBufioWriter(w io.Writer) *bufio.Writer { - bw, ok := writerPool.Get().(*bufio.Writer) + bw, ok := bufioWriterPool.Get().(*bufio.Writer) if !ok { return bufio.NewWriter(w) } @@ -280,5 +280,5 @@ func getBufioWriter(w io.Writer) *bufio.Writer { } func putBufioWriter(bw *bufio.Writer) { - writerPool.Put(bw) + bufioWriterPool.Put(bw) } diff --git a/read.go b/read.go index a1efecab..f2c7a801 100644 --- a/read.go +++ b/read.go @@ -109,12 +109,17 @@ func (mr *msgReader) putFlateReader() { } func (mr *msgReader) close() { - mr.c.readMu.Lock(context.Background()) + mr.c.readMu.forceLock() mr.putFlateReader() mr.dict.close() if mr.flateBufio != nil { putBufioReader(mr.flateBufio) } + + if mr.c.client { + putBufioReader(mr.c.br) + mr.c.br = nil + } } func (mr *msgReader) flateContextTakeover() bool { @@ -292,11 +297,11 @@ func (c *Conn) handleControl(ctx context.Context, h header) (err error) { func (c *Conn) reader(ctx context.Context) (_ MessageType, _ io.Reader, err error) { defer errd.Wrap(&err, "failed to get reader") - err = c.readMu.Lock(ctx) + err = c.readMu.lock(ctx) if err != nil { return 0, nil, err } - defer c.readMu.Unlock() + defer c.readMu.unlock() if !c.msgReader.fin { return 0, nil, errors.New("previous message not read to completion") @@ -368,11 +373,11 @@ func (mr *msgReader) Read(p []byte) (n int, err error) { errd.Wrap(&err, "failed to read") }() - err = mr.c.readMu.Lock(mr.ctx) + err = mr.c.readMu.lock(mr.ctx) if err != nil { return 0, err } - defer mr.c.readMu.Unlock() + defer mr.c.readMu.unlock() n, err = mr.limitReader.Read(p) if mr.flate && mr.flateContextTakeover() { diff --git a/write.go b/write.go index 81b9141a..2d20b292 100644 --- a/write.go +++ b/write.go @@ -125,7 +125,7 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error } if !c.flate() { - defer c.msgWriterState.mu.Unlock() + defer c.msgWriterState.mu.unlock() return c.writeFrame(ctx, true, false, c.msgWriterState.opcode, p) } @@ -139,7 +139,7 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error } func (mw *msgWriterState) reset(ctx context.Context, typ MessageType) error { - err := mw.mu.Lock(ctx) + err := mw.mu.lock(ctx) if err != nil { return err } @@ -204,11 +204,16 @@ func (mw *msgWriterState) Close() (err error) { if mw.flate && !mw.flateContextTakeover() { mw.dict.close() } - mw.mu.Unlock() + mw.mu.unlock() return nil } func (mw *msgWriterState) close() { + if mw.c.client { + mw.c.writeFrameMu.forceLock() + putBufioWriter(mw.c.bw) + } + mw.writeMu.Lock() mw.dict.close() } @@ -226,11 +231,11 @@ func (c *Conn) writeControl(ctx context.Context, opcode opcode, p []byte) error // frame handles all writes to the connection. func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opcode, p []byte) (int, error) { - err := c.writeFrameMu.Lock(ctx) + err := c.writeFrameMu.lock(ctx) if err != nil { return 0, err } - defer c.writeFrameMu.Unlock() + defer c.writeFrameMu.unlock() select { case <-c.closed: From 1200707bd313a46fda2fb828ff840dac4e12283f Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 20 Feb 2020 18:49:10 -0500 Subject: [PATCH 286/519] Ensure connection is closed at all error points Closes #191 --- read.go | 26 ++++++++++++-------------- write.go | 19 +++++++++++++++---- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/read.go b/read.go index f2c7a801..381cea3d 100644 --- a/read.go +++ b/read.go @@ -304,7 +304,9 @@ func (c *Conn) reader(ctx context.Context) (_ MessageType, _ io.Reader, err erro defer c.readMu.unlock() if !c.msgReader.fin { - return 0, nil, errors.New("previous message not read to completion") + err = errors.New("previous message not read to completion") + c.close(fmt.Errorf("failed to get reader: %w", err)) + return 0, nil, err } h, err := c.readLoop(ctx) @@ -361,21 +363,9 @@ func (mr *msgReader) setFrame(h header) { } func (mr *msgReader) Read(p []byte) (n int, err error) { - defer func() { - if errors.Is(err, io.ErrUnexpectedEOF) && mr.fin && mr.flate { - err = io.EOF - } - if errors.Is(err, io.EOF) { - err = io.EOF - mr.putFlateReader() - return - } - errd.Wrap(&err, "failed to read") - }() - err = mr.c.readMu.lock(mr.ctx) if err != nil { - return 0, err + return 0, fmt.Errorf("failed to read: %w", err) } defer mr.c.readMu.unlock() @@ -384,6 +374,14 @@ func (mr *msgReader) Read(p []byte) (n int, err error) { p = p[:n] mr.dict.write(p) } + if errors.Is(err, io.EOF) || errors.Is(err, io.ErrUnexpectedEOF) && mr.fin && mr.flate { + mr.putFlateReader() + return n, io.EOF + } + if err != nil { + err = fmt.Errorf("failed to read: %w", err) + mr.c.close(err) + } return n, err } diff --git a/write.go b/write.go index 2d20b292..d94486e2 100644 --- a/write.go +++ b/write.go @@ -155,11 +155,16 @@ func (mw *msgWriterState) reset(ctx context.Context, typ MessageType) error { // Write writes the given bytes to the WebSocket connection. func (mw *msgWriterState) Write(p []byte) (_ int, err error) { - defer errd.Wrap(&err, "failed to write") - mw.writeMu.Lock() defer mw.writeMu.Unlock() + defer func() { + err = fmt.Errorf("failed to write: %w", err) + if err != nil { + mw.c.close(err) + } + }() + if mw.c.flate() { // Only enables flate if the length crosses the // threshold on the first frame @@ -230,8 +235,8 @@ func (c *Conn) writeControl(ctx context.Context, opcode opcode, p []byte) error } // frame handles all writes to the connection. -func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opcode, p []byte) (int, error) { - err := c.writeFrameMu.lock(ctx) +func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opcode, p []byte) (_ int, err error) { + err = c.writeFrameMu.lock(ctx) if err != nil { return 0, err } @@ -243,6 +248,12 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco case c.writeTimeout <- ctx: } + defer func() { + if err != nil { + c.close(fmt.Errorf("failed to write frame: %w", err)) + } + }() + c.writeHeader.fin = fin c.writeHeader.opcode = opcode c.writeHeader.payloadLength = int64(len(p)) From 2e0dd1c74967c77af530fb33e0762344d044033c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 20 Feb 2020 19:03:09 -0500 Subject: [PATCH 287/519] Make writeMu a channel based mutex Will prevent deadlock if a writer is used after close. --- write.go | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/write.go b/write.go index d94486e2..baa5e6e2 100644 --- a/write.go +++ b/write.go @@ -10,7 +10,6 @@ import ( "errors" "fmt" "io" - "sync" "time" "github.com/klauspost/compress/flate" @@ -71,7 +70,7 @@ type msgWriterState struct { c *Conn mu *mu - writeMu sync.Mutex + writeMu *mu ctx context.Context opcode opcode @@ -83,8 +82,9 @@ type msgWriterState struct { func newMsgWriterState(c *Conn) *msgWriterState { mw := &msgWriterState{ - c: c, - mu: newMu(c), + c: c, + mu: newMu(c), + writeMu: newMu(c), } return mw } @@ -155,12 +155,15 @@ func (mw *msgWriterState) reset(ctx context.Context, typ MessageType) error { // Write writes the given bytes to the WebSocket connection. func (mw *msgWriterState) Write(p []byte) (_ int, err error) { - mw.writeMu.Lock() - defer mw.writeMu.Unlock() + err = mw.writeMu.lock(mw.ctx) + if err != nil { + return 0, fmt.Errorf("failed to write: %w", err) + } + defer mw.writeMu.unlock() defer func() { - err = fmt.Errorf("failed to write: %w", err) if err != nil { + err = fmt.Errorf("failed to write: %w", err) mw.c.close(err) } }() @@ -198,8 +201,11 @@ func (mw *msgWriterState) write(p []byte) (int, error) { func (mw *msgWriterState) Close() (err error) { defer errd.Wrap(&err, "failed to close writer") - mw.writeMu.Lock() - defer mw.writeMu.Unlock() + err = mw.writeMu.lock(mw.ctx) + if err != nil { + return err + } + defer mw.writeMu.unlock() _, err = mw.c.writeFrame(mw.ctx, true, mw.flate, mw.opcode, nil) if err != nil { @@ -219,7 +225,7 @@ func (mw *msgWriterState) close() { putBufioWriter(mw.c.bw) } - mw.writeMu.Lock() + mw.writeMu.forceLock() mw.dict.close() } @@ -250,7 +256,8 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco defer func() { if err != nil { - c.close(fmt.Errorf("failed to write frame: %w", err)) + err = fmt.Errorf("failed to write frame: %w", err) + c.close(err) } }() From e8f5bc8900a1c929f71a1c892451e19f82f9f6ee Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 21 Feb 2020 01:41:40 -0500 Subject: [PATCH 288/519] Add Example_crossOrigin Closes #194 --- example_test.go | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/example_test.go b/example_test.go index 075107b0..666914d2 100644 --- a/example_test.go +++ b/example_test.go @@ -6,6 +6,7 @@ import ( "context" "log" "net/http" + "net/url" "time" "nhooyr.io/websocket" @@ -115,3 +116,30 @@ func Example_writeOnly() { err := http.ListenAndServe("localhost:8080", fn) log.Fatal(err) } + +// This example demonstrates how to safely accept cross origin WebSockets +// from the origin example.com. +func Example_crossOrigin() { + fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + origin := r.Header.Get("Origin") + if origin != "" { + u, err := url.Parse(origin) + if err != nil || u.Host != "example.com" { + http.Error(w, "bad origin header", http.StatusForbidden) + return + } + } + + c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + InsecureSkipVerify: true, + }) + if err != nil { + log.Println(err) + return + } + c.Close(websocket.StatusNormalClosure, "cross origin WebSocket accepted") + }) + + err := http.ListenAndServe("localhost:8080", fn) + log.Fatal(err) +} From 500b9d734508a2c8ab09457ac9895b895bc86470 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 25 Feb 2020 22:20:19 -0500 Subject: [PATCH 289/519] Add OriginPatterns to AcceptOptions Closes #194 --- accept.go | 73 +++++++++++++++++++++++++++++++------------------ accept_test.go | 31 +++++++++++++++++---- example_test.go | 12 +------- 3 files changed, 74 insertions(+), 42 deletions(-) diff --git a/accept.go b/accept.go index 479138fc..47e20b52 100644 --- a/accept.go +++ b/accept.go @@ -9,10 +9,11 @@ import ( "errors" "fmt" "io" + "log" "net/http" "net/textproto" "net/url" - "strconv" + "path/filepath" "strings" "nhooyr.io/websocket/internal/errd" @@ -25,18 +26,27 @@ type AcceptOptions struct { // reject it, close the connection when c.Subprotocol() == "". Subprotocols []string - // InsecureSkipVerify disables Accept's origin verification behaviour. By default, - // the connection will only be accepted if the request origin is equal to the request - // host. + // InsecureSkipVerify is used to disable Accept's origin verification behaviour. // - // This is only required if you want javascript served from a different domain - // to access your WebSocket server. + // Deprecated: Use OriginPatterns with a match all pattern of * instead to control + // origin authorization yourself. + InsecureSkipVerify bool + + // OriginPatterns lists the host patterns for authorized origins. + // The request host is always authorized. + // Use this to enable cross origin WebSockets. + // + // i.e javascript running on example.com wants to access a WebSocket server at chat.example.com. + // In such a case, example.com is the origin and chat.example.com is the request host. + // One would set this field to []string{"example.com"} to authorize example.com to connect. // - // See https://stackoverflow.com/a/37837709/4283659 + // Each pattern is matched case insensitively against the request origin host + // with filepath.Match. + // See https://golang.org/pkg/path/filepath/#Match // // Please ensure you understand the ramifications of enabling this. // If used incorrectly your WebSocket server will be open to CSRF attacks. - InsecureSkipVerify bool + OriginPatterns []string // CompressionMode controls the compression mode. // Defaults to CompressionNoContextTakeover. @@ -77,8 +87,12 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con } if !opts.InsecureSkipVerify { - err = authenticateOrigin(r) + err = authenticateOrigin(r, opts.OriginPatterns) if err != nil { + if errors.Is(err, filepath.ErrBadPattern) { + log.Printf("websocket: %v", err) + err = errors.New(http.StatusText(http.StatusForbidden)) + } http.Error(w, err.Error(), http.StatusForbidden) return nil, err } @@ -165,18 +179,35 @@ func verifyClientRequest(w http.ResponseWriter, r *http.Request) (errCode int, _ return 0, nil } -func authenticateOrigin(r *http.Request) error { +func authenticateOrigin(r *http.Request, originHosts []string) error { origin := r.Header.Get("Origin") - if origin != "" { - u, err := url.Parse(origin) + if origin == "" { + return nil + } + + u, err := url.Parse(origin) + if err != nil { + return fmt.Errorf("failed to parse Origin header %q: %w", origin, err) + } + + if strings.EqualFold(r.Host, u.Host) { + return nil + } + + for _, hostPattern := range originHosts { + matched, err := match(hostPattern, u.Host) if err != nil { - return fmt.Errorf("failed to parse Origin header %q: %w", origin, err) + return fmt.Errorf("failed to parse filepath pattern %q: %w", hostPattern, err) } - if !strings.EqualFold(u.Host, r.Host) { - return fmt.Errorf("request Origin %q is not authorized for Host %q", origin, r.Host) + if matched { + return nil } } - return nil + return fmt.Errorf("request Origin %q is not authorized for Host %q", origin, r.Host) +} + +func match(pattern, s string) (bool, error) { + return filepath.Match(strings.ToLower(pattern), strings.ToLower(s)) } func selectSubprotocol(r *http.Request, subprotocols []string) string { @@ -235,16 +266,6 @@ func acceptDeflate(w http.ResponseWriter, ext websocketExtension, mode Compressi return copts, nil } -// parseExtensionParameter parses the value in the extension parameter p. -func parseExtensionParameter(p string) (int, bool) { - ps := strings.Split(p, "=") - if len(ps) == 1 { - return 0, false - } - i, e := strconv.Atoi(strings.Trim(ps[1], `"`)) - return i, e == nil -} - func acceptWebkitDeflate(w http.ResponseWriter, ext websocketExtension, mode CompressionMode) (*compressionOptions, error) { copts := mode.opts() // The peer must explicitly request it. diff --git a/accept_test.go b/accept_test.go index 49667799..40a7b40c 100644 --- a/accept_test.go +++ b/accept_test.go @@ -244,10 +244,11 @@ func Test_authenticateOrigin(t *testing.T) { t.Parallel() testCases := []struct { - name string - origin string - host string - success bool + name string + origin string + host string + originPatterns []string + success bool }{ { name: "none", @@ -278,6 +279,26 @@ func Test_authenticateOrigin(t *testing.T) { host: "example.com", success: true, }, + { + name: "originPatterns", + origin: "https://two.examplE.com", + host: "example.com", + originPatterns: []string{ + "*.example.com", + "bar.com", + }, + success: true, + }, + { + name: "originPatternsUnauthorized", + origin: "https://two.examplE.com", + host: "example.com", + originPatterns: []string{ + "exam3.com", + "bar.com", + }, + success: false, + }, } for _, tc := range testCases { @@ -288,7 +309,7 @@ func Test_authenticateOrigin(t *testing.T) { r := httptest.NewRequest("GET", "http://"+tc.host+"/", nil) r.Header.Set("Origin", tc.origin) - err := authenticateOrigin(r) + err := authenticateOrigin(r, tc.originPatterns) if tc.success { assert.Success(t, err) } else { diff --git a/example_test.go b/example_test.go index 666914d2..c56e53f3 100644 --- a/example_test.go +++ b/example_test.go @@ -6,7 +6,6 @@ import ( "context" "log" "net/http" - "net/url" "time" "nhooyr.io/websocket" @@ -121,17 +120,8 @@ func Example_writeOnly() { // from the origin example.com. func Example_crossOrigin() { fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - origin := r.Header.Get("Origin") - if origin != "" { - u, err := url.Parse(origin) - if err != nil || u.Host != "example.com" { - http.Error(w, "bad origin header", http.StatusForbidden) - return - } - } - c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ - InsecureSkipVerify: true, + OriginPatterns: []string{"example.com"}, }) if err != nil { log.Println(err) From 97345d8042c26a1e56f8e3cc5b494230677e4ab0 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 26 Feb 2020 00:29:13 -0500 Subject: [PATCH 290/519] Simplify wstest.Pipe --- conn_test.go | 6 ++++-- internal/test/wstest/pipe.go | 26 ++++---------------------- 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/conn_test.go b/conn_test.go index 535afe24..28da3c07 100644 --- a/conn_test.go +++ b/conn_test.go @@ -337,8 +337,10 @@ func newConnTest(t testing.TB, dialOpts *websocket.DialOptions, acceptOpts *webs tt = &connTest{t: t, ctx: ctx} tt.appendDone(cancel) - c1, c2, err := wstest.Pipe(dialOpts, acceptOpts) - assert.Success(tt.t, err) + c1, c2 = wstest.Pipe(dialOpts, acceptOpts) + if xrand.Bool() { + c1, c2 = c2, c1 + } tt.appendDone(func() { c2.Close(websocket.StatusInternalError, "") c1.Close(websocket.StatusInternalError, "") diff --git a/internal/test/wstest/pipe.go b/internal/test/wstest/pipe.go index 0a2899ee..1534f316 100644 --- a/internal/test/wstest/pipe.go +++ b/internal/test/wstest/pipe.go @@ -5,26 +5,19 @@ package wstest import ( "bufio" "context" - "fmt" "net" "net/http" "net/http/httptest" "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/errd" - "nhooyr.io/websocket/internal/test/xrand" ) // Pipe is used to create an in memory connection // between two websockets analogous to net.Pipe. -func Pipe(dialOpts *websocket.DialOptions, acceptOpts *websocket.AcceptOptions) (_ *websocket.Conn, _ *websocket.Conn, err error) { - defer errd.Wrap(&err, "failed to create ws pipe") - - var serverConn *websocket.Conn - var acceptErr error +func Pipe(dialOpts *websocket.DialOptions, acceptOpts *websocket.AcceptOptions) (clientConn, serverConn *websocket.Conn) { tt := fakeTransport{ h: func(w http.ResponseWriter, r *http.Request) { - serverConn, acceptErr = websocket.Accept(w, r, acceptOpts) + serverConn, _ = websocket.Accept(w, r, acceptOpts) }, } @@ -36,19 +29,8 @@ func Pipe(dialOpts *websocket.DialOptions, acceptOpts *websocket.AcceptOptions) Transport: tt, } - clientConn, _, err := websocket.Dial(context.Background(), "ws://example.com", dialOpts) - if err != nil { - return nil, nil, fmt.Errorf("failed to dial with fake transport: %w", err) - } - - if serverConn == nil { - return nil, nil, fmt.Errorf("failed to get server conn from fake transport: %w", acceptErr) - } - - if xrand.Bool() { - return serverConn, clientConn, nil - } - return clientConn, serverConn, nil + clientConn, _, _ = websocket.Dial(context.Background(), "ws://example.com", dialOpts) + return clientConn, serverConn } type fakeTransport struct { From deb14cfd901a252aa2ac0395db8d5c553d271248 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 27 Feb 2020 16:09:06 -0500 Subject: [PATCH 291/519] Make sure to release lock when acquiring and connection is closed. Closes #205 --- conn_notjs.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/conn_notjs.go b/conn_notjs.go index 2ec5f5bf..bb2eb22f 100644 --- a/conn_notjs.go +++ b/conn_notjs.go @@ -245,9 +245,11 @@ func (m *mu) lock(ctx context.Context) error { case m.ch <- struct{}{}: // To make sure the connection is certainly alive. // As it's possible the send on m.ch was selected - // the receive on closed. + // over the receive on closed. select { case <-m.c.closed: + // Make sure to release. + m.unlock() return m.c.closeErr default: } From 97172f3339a9bef16fa82fde84b4b0c7a1357e56 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 25 Feb 2020 23:59:57 -0500 Subject: [PATCH 292/519] Add Grace to gracefully close WebSocket connections Closes #199 --- accept.go | 20 ++++++- conn_notjs.go | 5 ++ conn_test.go | 12 ++--- example_echo_test.go | 6 ++- example_test.go | 46 ++++++++++++++++ grace.go | 123 +++++++++++++++++++++++++++++++++++++++++++ ws_js.go | 2 + 7 files changed, 202 insertions(+), 12 deletions(-) create mode 100644 grace.go diff --git a/accept.go b/accept.go index 47e20b52..52a93459 100644 --- a/accept.go +++ b/accept.go @@ -75,6 +75,13 @@ func Accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Conn, err error) { defer errd.Wrap(&err, "failed to accept WebSocket connection") + g := graceFromRequest(r) + if g != nil && g.isClosing() { + err := errors.New("server closing") + http.Error(w, err.Error(), http.StatusServiceUnavailable) + return nil, err + } + if opts == nil { opts = &AcceptOptions{} } @@ -134,7 +141,7 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con b, _ := brw.Reader.Peek(brw.Reader.Buffered()) brw.Reader.Reset(io.MultiReader(bytes.NewReader(b), netConn)) - return newConn(connConfig{ + c := newConn(connConfig{ subprotocol: w.Header().Get("Sec-WebSocket-Protocol"), rwc: netConn, client: false, @@ -143,7 +150,16 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con br: brw.Reader, bw: brw.Writer, - }), nil + }) + + if g != nil { + err = g.addConn(c) + if err != nil { + return nil, err + } + } + + return c, nil } func verifyClientRequest(w http.ResponseWriter, r *http.Request) (errCode int, _ error) { diff --git a/conn_notjs.go b/conn_notjs.go index bb2eb22f..f604898e 100644 --- a/conn_notjs.go +++ b/conn_notjs.go @@ -33,6 +33,7 @@ type Conn struct { flateThreshold int br *bufio.Reader bw *bufio.Writer + g *Grace readTimeout chan context.Context writeTimeout chan context.Context @@ -138,6 +139,10 @@ func (c *Conn) close(err error) { // closeErr. c.rwc.Close() + if c.g != nil { + c.g.delConn(c) + } + go func() { c.msgWriterState.close() diff --git a/conn_test.go b/conn_test.go index 28da3c07..af4fa4c0 100644 --- a/conn_test.go +++ b/conn_test.go @@ -13,7 +13,6 @@ import ( "os" "os/exec" "strings" - "sync" "testing" "time" @@ -272,11 +271,9 @@ func TestWasm(t *testing.T) { t.Skip("skipping on CI") } - var wg sync.WaitGroup - s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - wg.Add(1) - defer wg.Done() - + var g websocket.Grace + defer g.Close() + s := httptest.NewServer(g.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ Subprotocols: []string{"echo"}, InsecureSkipVerify: true, @@ -294,8 +291,7 @@ func TestWasm(t *testing.T) { t.Errorf("echo server failed: %v", err) return } - })) - defer wg.Wait() + }))) defer s.Close() ctx, cancel := context.WithTimeout(context.Background(), time.Minute) diff --git a/example_echo_test.go b/example_echo_test.go index cd195d2e..0c0b84ea 100644 --- a/example_echo_test.go +++ b/example_echo_test.go @@ -31,13 +31,15 @@ func Example_echo() { } defer l.Close() + var g websocket.Grace + defer g.Close() s := &http.Server{ - Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + Handler: g.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := echoServer(w, r) if err != nil { log.Printf("echo server: %v", err) } - }), + })), ReadTimeout: time.Second * 15, WriteTimeout: time.Second * 15, } diff --git a/example_test.go b/example_test.go index c56e53f3..ce049bc3 100644 --- a/example_test.go +++ b/example_test.go @@ -6,6 +6,8 @@ import ( "context" "log" "net/http" + "os" + "os/signal" "time" "nhooyr.io/websocket" @@ -133,3 +135,47 @@ func Example_crossOrigin() { err := http.ListenAndServe("localhost:8080", fn) log.Fatal(err) } + +// This example demonstrates how to create a WebSocket server +// that gracefully exits when sent a signal. +// +// It starts a WebSocket server that keeps every connection open +// for 10 seconds. +// If you CTRL+C while a connection is open, it will wait at most 30s +// for all connections to terminate before shutting down. +func ExampleGrace() { + fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c, err := websocket.Accept(w, r, nil) + if err != nil { + log.Println(err) + return + } + defer c.Close(websocket.StatusInternalError, "the sky is falling") + + ctx := c.CloseRead(r.Context()) + select { + case <-ctx.Done(): + case <-time.After(time.Second * 10): + } + + c.Close(websocket.StatusNormalClosure, "") + }) + + var g websocket.Grace + s := &http.Server{ + Handler: g.Handler(fn), + ReadTimeout: time.Second * 15, + WriteTimeout: time.Second * 15, + } + go s.ListenAndServe() + + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, os.Interrupt) + sig := <-sigs + log.Printf("recieved %v, shutting down", sig) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + s.Shutdown(ctx) + g.Shutdown(ctx) +} diff --git a/grace.go b/grace.go new file mode 100644 index 00000000..8dadc43d --- /dev/null +++ b/grace.go @@ -0,0 +1,123 @@ +package websocket + +import ( + "context" + "errors" + "fmt" + "net/http" + "sync" + "time" +) + +// Grace enables graceful shutdown of accepted WebSocket connections. +// +// Use Handler to wrap WebSocket handlers to record accepted connections +// and then use Close or Shutdown to gracefully close these connections. +// +// Grace is intended to be used in harmony with net/http.Server's Shutdown and Close methods. +type Grace struct { + mu sync.Mutex + closing bool + conns map[*Conn]struct{} +} + +// Handler returns a handler that wraps around h to record +// all WebSocket connections accepted. +// +// Use Close or Shutdown to gracefully close recorded connections. +func (g *Grace) Handler(h http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ctx := context.WithValue(r.Context(), gracefulContextKey{}, g) + r = r.WithContext(ctx) + h.ServeHTTP(w, r) + }) +} + +func (g *Grace) isClosing() bool { + g.mu.Lock() + defer g.mu.Unlock() + return g.closing +} + +func graceFromRequest(r *http.Request) *Grace { + g, _ := r.Context().Value(gracefulContextKey{}).(*Grace) + return g +} + +func (g *Grace) addConn(c *Conn) error { + g.mu.Lock() + defer g.mu.Unlock() + if g.closing { + c.Close(StatusGoingAway, "server shutting down") + return errors.New("server shutting down") + } + if g.conns == nil { + g.conns = make(map[*Conn]struct{}) + } + g.conns[c] = struct{}{} + c.g = g + return nil +} + +func (g *Grace) delConn(c *Conn) { + g.mu.Lock() + defer g.mu.Unlock() + delete(g.conns, c) +} + +type gracefulContextKey struct{} + +// Close prevents the acceptance of new connections with +// http.StatusServiceUnavailable and closes all accepted +// connections with StatusGoingAway. +func (g *Grace) Close() error { + g.mu.Lock() + g.closing = true + var wg sync.WaitGroup + for c := range g.conns { + wg.Add(1) + go func(c *Conn) { + defer wg.Done() + c.Close(StatusGoingAway, "server shutting down") + }(c) + + delete(g.conns, c) + } + g.mu.Unlock() + + wg.Wait() + + return nil +} + +// Shutdown prevents the acceptance of new connections and waits until +// all connections close. If the context is cancelled before that, it +// calls Close to close all connections immediately. +func (g *Grace) Shutdown(ctx context.Context) error { + defer g.Close() + + g.mu.Lock() + g.closing = true + g.mu.Unlock() + + // Same poll period used by net/http. + t := time.NewTicker(500 * time.Millisecond) + defer t.Stop() + for { + if g.zeroConns() { + return nil + } + + select { + case <-t.C: + case <-ctx.Done(): + return fmt.Errorf("failed to shutdown WebSockets: %w", ctx.Err()) + } + } +} + +func (g *Grace) zeroConns() bool { + g.mu.Lock() + defer g.mu.Unlock() + return len(g.conns) == 0 +} diff --git a/ws_js.go b/ws_js.go index 2b560ce8..a8c8b771 100644 --- a/ws_js.go +++ b/ws_js.go @@ -38,6 +38,8 @@ type Conn struct { readSignal chan struct{} readBufMu sync.Mutex readBuf []wsjs.MessageEvent + + g *Grace } func (c *Conn) close(err error, wasClean bool) { From e335b09210e47739545fe30c69f3a0f56ede98a0 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 26 Feb 2020 14:47:40 -0500 Subject: [PATCH 293/519] Use grace in chat example --- accept.go | 4 ++-- chat-example/go.mod | 4 +++- chat-example/go.sum | 10 ++++++++-- chat-example/index.css | 2 +- chat-example/index.js | 13 ++++++++++--- chat-example/main.go | 29 +++++++++++++++++++++++++++-- example_test.go | 14 +++++++++++--- grace.go | 20 ++++++++++++-------- 8 files changed, 74 insertions(+), 22 deletions(-) diff --git a/accept.go b/accept.go index 52a93459..dd96c9bd 100644 --- a/accept.go +++ b/accept.go @@ -76,8 +76,8 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con defer errd.Wrap(&err, "failed to accept WebSocket connection") g := graceFromRequest(r) - if g != nil && g.isClosing() { - err := errors.New("server closing") + if g != nil && g.isShuttingdown() { + err := errors.New("server shutting down") http.Error(w, err.Error(), http.StatusServiceUnavailable) return nil, err } diff --git a/chat-example/go.mod b/chat-example/go.mod index 34fa5a69..c47a5a2f 100644 --- a/chat-example/go.mod +++ b/chat-example/go.mod @@ -2,4 +2,6 @@ module nhooyr.io/websocket/example-chat go 1.13 -require nhooyr.io/websocket v1.8.2 +require nhooyr.io/websocket v0.0.0 + +replace nhooyr.io/websocket => ../ diff --git a/chat-example/go.sum b/chat-example/go.sum index 0755fca5..e4bbd62d 100644 --- a/chat-example/go.sum +++ b/chat-example/go.sum @@ -1,12 +1,18 @@ +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/klauspost/compress v1.10.0 h1:92XGj1AcYzA6UrVdd4qIIBrT8OroryvRvdmg/IfmC7Y= github.com/klauspost/compress v1.10.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -nhooyr.io/websocket v1.8.2 h1:LwdzfyyOZKtVFoXay6A39Acu03KmidSZ3YUUvPa13PA= -nhooyr.io/websocket v1.8.2/go.mod h1:LiqdCg1Cu7TPWxEvPjPa0TGYxCsy4pHNTN9gGluwBpQ= diff --git a/chat-example/index.css b/chat-example/index.css index 29804662..73a8e0f3 100644 --- a/chat-example/index.css +++ b/chat-example/index.css @@ -5,7 +5,7 @@ body { #root { padding: 40px 20px; - max-width: 480px; + max-width: 600px; margin: auto; height: 100vh; diff --git a/chat-example/index.js b/chat-example/index.js index 8fb3dfb8..a42c2d30 100644 --- a/chat-example/index.js +++ b/chat-example/index.js @@ -7,8 +7,11 @@ const conn = new WebSocket(`ws://${location.host}/subscribe`) conn.addEventListener("close", ev => { - console.info("websocket disconnected, reconnecting in 1000ms", ev) - setTimeout(dial, 1000) + appendLog(`WebSocket Disconnected code: ${ev.code}, reason: ${ev.reason}`, true) + if (ev.code !== 1001) { + appendLog("Reconnecting in 1s", true) + setTimeout(dial, 1000) + } }) conn.addEventListener("open", ev => { console.info("websocket connected") @@ -34,10 +37,14 @@ const messageInput = document.getElementById("message-input") // appendLog appends the passed text to messageLog. - function appendLog(text) { + function appendLog(text, error) { const p = document.createElement("p") // Adding a timestamp to each message makes the log easier to read. p.innerText = `${new Date().toLocaleTimeString()}: ${text}` + if (error) { + p.style.color = "red" + p.style.fontStyle = "bold" + } messageLog.append(p) return p } diff --git a/chat-example/main.go b/chat-example/main.go index 2a520924..f985d382 100644 --- a/chat-example/main.go +++ b/chat-example/main.go @@ -1,12 +1,16 @@ package main import ( + "context" "errors" "log" "net" "net/http" "os" + "os/signal" "time" + + "nhooyr.io/websocket" ) func main() { @@ -38,10 +42,31 @@ func run() error { m.HandleFunc("/subscribe", ws.subscribeHandler) m.HandleFunc("/publish", ws.publishHandler) + var g websocket.Grace s := http.Server{ - Handler: m, + Handler: g.Handler(m), ReadTimeout: time.Second * 10, WriteTimeout: time.Second * 10, } - return s.Serve(l) + errc := make(chan error, 1) + go func() { + errc <- s.Serve(l) + }() + + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, os.Interrupt) + select { + case err := <-errc: + log.Printf("failed to serve: %v", err) + case sig := <-sigs: + log.Printf("terminating: %v", sig) + } + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + s.Shutdown(ctx) + g.Shutdown(ctx) + + return nil } diff --git a/example_test.go b/example_test.go index ce049bc3..462de376 100644 --- a/example_test.go +++ b/example_test.go @@ -167,12 +167,20 @@ func ExampleGrace() { ReadTimeout: time.Second * 15, WriteTimeout: time.Second * 15, } - go s.ListenAndServe() + + errc := make(chan error, 1) + go func() { + errc <- s.ListenAndServe() + }() sigs := make(chan os.Signal, 1) signal.Notify(sigs, os.Interrupt) - sig := <-sigs - log.Printf("recieved %v, shutting down", sig) + select { + case err := <-errc: + log.Printf("failed to listen and serve: %v", err) + case sig := <-sigs: + log.Printf("terminating: %v", sig) + } ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) defer cancel() diff --git a/grace.go b/grace.go index 8dadc43d..c53cd40b 100644 --- a/grace.go +++ b/grace.go @@ -15,10 +15,13 @@ import ( // and then use Close or Shutdown to gracefully close these connections. // // Grace is intended to be used in harmony with net/http.Server's Shutdown and Close methods. +// It's required as net/http's Shutdown and Close methods do not keep track of WebSocket +// connections. type Grace struct { - mu sync.Mutex - closing bool - conns map[*Conn]struct{} + mu sync.Mutex + closed bool + shuttingDown bool + conns map[*Conn]struct{} } // Handler returns a handler that wraps around h to record @@ -33,10 +36,10 @@ func (g *Grace) Handler(h http.Handler) http.Handler { }) } -func (g *Grace) isClosing() bool { +func (g *Grace) isShuttingdown() bool { g.mu.Lock() defer g.mu.Unlock() - return g.closing + return g.shuttingDown } func graceFromRequest(r *http.Request) *Grace { @@ -47,7 +50,7 @@ func graceFromRequest(r *http.Request) *Grace { func (g *Grace) addConn(c *Conn) error { g.mu.Lock() defer g.mu.Unlock() - if g.closing { + if g.closed { c.Close(StatusGoingAway, "server shutting down") return errors.New("server shutting down") } @@ -72,7 +75,8 @@ type gracefulContextKey struct{} // connections with StatusGoingAway. func (g *Grace) Close() error { g.mu.Lock() - g.closing = true + g.shuttingDown = true + g.closed = true var wg sync.WaitGroup for c := range g.conns { wg.Add(1) @@ -97,7 +101,7 @@ func (g *Grace) Shutdown(ctx context.Context) error { defer g.Close() g.mu.Lock() - g.closing = true + g.shuttingDown = true g.mu.Unlock() // Same poll period used by net/http. From 190981dcf7f6af74049e8c6eab9dd500b0a9a47f Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 26 Feb 2020 15:31:29 -0500 Subject: [PATCH 294/519] Add automated test to chat example --- chat-example/chat.go | 61 ++++++++++++----- chat-example/chat_test.go | 137 ++++++++++++++++++++++++++++++++++++++ chat-example/go.mod | 7 -- chat-example/main.go | 10 +-- 4 files changed, 183 insertions(+), 32 deletions(-) create mode 100644 chat-example/chat_test.go delete mode 100644 chat-example/go.mod diff --git a/chat-example/chat.go b/chat-example/chat.go index e6e355d0..9b264195 100644 --- a/chat-example/chat.go +++ b/chat-example/chat.go @@ -15,8 +15,28 @@ import ( // chatServer enables broadcasting to a set of subscribers. type chatServer struct { + registerOnce sync.Once + m http.ServeMux + subscribersMu sync.RWMutex - subscribers map[chan<- []byte]struct{} + subscribers map[*subscriber]struct{} +} + +// subscriber represents a subscriber. +// Messages are sent on the msgs channel and if the client +// cannot keep up with the messages, closeSlow is called. +type subscriber struct { + msgs chan []byte + closeSlow func() +} + +func (cs *chatServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + cs.registerOnce.Do(func() { + cs.m.Handle("/", http.FileServer(http.Dir("."))) + cs.m.HandleFunc("/subscribe", cs.subscribeHandler) + cs.m.HandleFunc("/publish", cs.publishHandler) + }) + cs.m.ServeHTTP(w, r) } // subscribeHandler accepts the WebSocket connection and then subscribes @@ -57,11 +77,13 @@ func (cs *chatServer) publishHandler(w http.ResponseWriter, r *http.Request) { } cs.publish(msg) + + w.WriteHeader(http.StatusAccepted) } // subscribe subscribes the given WebSocket to all broadcast messages. -// It creates a msgs chan with a buffer of 16 to give some room to slower -// connections and then registers it. It then listens for all messages +// It creates a subscriber with a buffered msgs chan to give some room to slower +// connections and then registers the subscriber. It then listens for all messages // and writes them to the WebSocket. If the context is cancelled or // an error occurs, it returns and deletes the subscription. // @@ -70,13 +92,18 @@ func (cs *chatServer) publishHandler(w http.ResponseWriter, r *http.Request) { func (cs *chatServer) subscribe(ctx context.Context, c *websocket.Conn) error { ctx = c.CloseRead(ctx) - msgs := make(chan []byte, 16) - cs.addSubscriber(msgs) - defer cs.deleteSubscriber(msgs) + s := &subscriber{ + msgs: make(chan []byte, 16), + closeSlow: func() { + c.Close(websocket.StatusPolicyViolation, "connection too slow to keep up with messages") + }, + } + cs.addSubscriber(s) + defer cs.deleteSubscriber(s) for { select { - case msg := <-msgs: + case msg := <-s.msgs: err := writeTimeout(ctx, time.Second*5, c, msg) if err != nil { return err @@ -94,29 +121,29 @@ func (cs *chatServer) publish(msg []byte) { cs.subscribersMu.RLock() defer cs.subscribersMu.RUnlock() - for c := range cs.subscribers { + for s := range cs.subscribers { select { - case c <- msg: + case s.msgs <- msg: default: + go s.closeSlow() } } } -// addSubscriber registers a subscriber with a channel -// on which to send messages. -func (cs *chatServer) addSubscriber(msgs chan<- []byte) { +// addSubscriber registers a subscriber. +func (cs *chatServer) addSubscriber(s *subscriber) { cs.subscribersMu.Lock() if cs.subscribers == nil { - cs.subscribers = make(map[chan<- []byte]struct{}) + cs.subscribers = make(map[*subscriber]struct{}) } - cs.subscribers[msgs] = struct{}{} + cs.subscribers[s] = struct{}{} cs.subscribersMu.Unlock() } -// deleteSubscriber deletes the subscriber with the given msgs channel. -func (cs *chatServer) deleteSubscriber(msgs chan []byte) { +// deleteSubscriber deletes the given subscriber. +func (cs *chatServer) deleteSubscriber(s *subscriber) { cs.subscribersMu.Lock() - delete(cs.subscribers, msgs) + delete(cs.subscribers, s) cs.subscribersMu.Unlock() } diff --git a/chat-example/chat_test.go b/chat-example/chat_test.go new file mode 100644 index 00000000..d1772381 --- /dev/null +++ b/chat-example/chat_test.go @@ -0,0 +1,137 @@ +// +build !js + +package main + +import ( + "context" + "errors" + "fmt" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "nhooyr.io/websocket" +) + +func TestGrace(t *testing.T) { + t.Parallel() + + var cs chatServer + var g websocket.Grace + s := httptest.NewServer(g.Handler(&cs)) + defer s.Close() + defer g.Close() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + cl1, err := newClient(ctx, s.URL) + assertSuccess(t, err) + defer cl1.Close() + + cl2, err := newClient(ctx, s.URL) + assertSuccess(t, err) + defer cl2.Close() + + err = cl1.publish(ctx, "hello") + assertSuccess(t, err) + + assertReceivedMessage(ctx, cl1, "hello") + assertReceivedMessage(ctx, cl2, "hello") +} + +type client struct { + msgs chan string + url string + c *websocket.Conn +} + +func newClient(ctx context.Context, url string) (*client, error) { + wsURL := strings.ReplaceAll(url, "http://", "ws://") + c, _, err := websocket.Dial(ctx, wsURL+"/subscribe", nil) + if err != nil { + return nil, err + } + + cl := &client{ + msgs: make(chan string, 16), + url: url, + c: c, + } + go cl.readLoop() + + return cl, nil +} + +func (cl *client) readLoop() { + defer cl.c.Close(websocket.StatusInternalError, "") + defer close(cl.msgs) + + for { + typ, b, err := cl.c.Read(context.Background()) + if err != nil { + return + } + + if typ != websocket.MessageText { + cl.c.Close(websocket.StatusUnsupportedData, "expected text message") + return + } + + select { + case cl.msgs <- string(b): + default: + cl.c.Close(websocket.StatusInternalError, "messages coming in too fast to handle") + return + } + } +} + +func (cl *client) receive(ctx context.Context) (string, error) { + select { + case msg, ok := <-cl.msgs: + if !ok { + return "", errors.New("client closed") + } + return msg, nil + case <-ctx.Done(): + return "", ctx.Err() + } +} + +func (cl *client) publish(ctx context.Context, msg string) error { + req, _ := http.NewRequestWithContext(ctx, http.MethodPost, cl.url+"/publish", strings.NewReader(msg)) + resp, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusAccepted { + return fmt.Errorf("publish request failed: %v", resp.StatusCode) + } + return nil +} + +func (cl *client) Close() error { + return cl.c.Close(websocket.StatusNormalClosure, "") +} + +func assertSuccess(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatal(err) + } +} + +func assertReceivedMessage(ctx context.Context, cl *client, msg string) error { + msg, err := cl.receive(ctx) + if err != nil { + return err + } + if msg != "hello" { + return fmt.Errorf("expected hello but got %q", msg) + } + return nil +} diff --git a/chat-example/go.mod b/chat-example/go.mod deleted file mode 100644 index c47a5a2f..00000000 --- a/chat-example/go.mod +++ /dev/null @@ -1,7 +0,0 @@ -module nhooyr.io/websocket/example-chat - -go 1.13 - -require nhooyr.io/websocket v0.0.0 - -replace nhooyr.io/websocket => ../ diff --git a/chat-example/main.go b/chat-example/main.go index f985d382..a265f60c 100644 --- a/chat-example/main.go +++ b/chat-example/main.go @@ -35,16 +35,10 @@ func run() error { } log.Printf("listening on http://%v", l.Addr()) - var ws chatServer - - m := http.NewServeMux() - m.Handle("/", http.FileServer(http.Dir("."))) - m.HandleFunc("/subscribe", ws.subscribeHandler) - m.HandleFunc("/publish", ws.publishHandler) - + var cs chatServer var g websocket.Grace s := http.Server{ - Handler: g.Handler(m), + Handler: g.Handler(&cs), ReadTimeout: time.Second * 10, WriteTimeout: time.Second * 10, } From da3aa8cfcc08909ea3cd41153637e5c1697bac59 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 26 Feb 2020 20:39:59 -0500 Subject: [PATCH 295/519] Improve chat example test --- chat-example/README.md | 6 + chat-example/chat.go | 67 ++++++--- chat-example/chat_test.go | 285 ++++++++++++++++++++++++++++---------- chat-example/index.js | 17 ++- chat-example/main.go | 4 +- ci/test.mk | 3 +- 6 files changed, 284 insertions(+), 98 deletions(-) diff --git a/chat-example/README.md b/chat-example/README.md index ef06275d..a4c99a93 100644 --- a/chat-example/README.md +++ b/chat-example/README.md @@ -25,3 +25,9 @@ assets, the `/subscribe` WebSocket endpoint and the HTTP POST `/publish` endpoin The code is well commented. I would recommend starting in `main.go` and then `chat.go` followed by `index.html` and then `index.js`. + +There are two automated tests for the server included in `chat_test.go`. The first is a simple one +client echo test. It publishes a single message and ensures it's received. + +The second is a complex concurrency test where 10 clients send 128 unique messages +of max 128 bytes concurrently. The test ensures all messages are seen by every client. diff --git a/chat-example/chat.go b/chat-example/chat.go index 9b264195..532e50f5 100644 --- a/chat-example/chat.go +++ b/chat-example/chat.go @@ -3,25 +3,57 @@ package main import ( "context" "errors" - "io" "io/ioutil" "log" "net/http" "sync" "time" + "golang.org/x/time/rate" + "nhooyr.io/websocket" ) // chatServer enables broadcasting to a set of subscribers. type chatServer struct { - registerOnce sync.Once - m http.ServeMux - - subscribersMu sync.RWMutex + // subscriberMessageBuffer controls the max number + // of messages that can be queued for a subscriber + // before it is kicked. + // + // Defaults to 16. + subscriberMessageBuffer int + + // publishLimiter controls the rate limit applied to the publish endpoint. + // + // Defaults to one publish every 100ms with a burst of 8. + publishLimiter *rate.Limiter + + // logf controls where logs are sent. + // Defaults to log.Printf. + logf func(f string, v ...interface{}) + + // serveMux routes the various endpoints to the appropriate handler. + serveMux http.ServeMux + + subscribersMu sync.Mutex subscribers map[*subscriber]struct{} } +// newChatServer constructs a chatServer with the defaults. +func newChatServer() *chatServer { + cs := &chatServer{ + subscriberMessageBuffer: 16, + logf: log.Printf, + subscribers: make(map[*subscriber]struct{}), + publishLimiter: rate.NewLimiter(rate.Every(time.Millisecond*100), 8), + } + cs.serveMux.Handle("/", http.FileServer(http.Dir("."))) + cs.serveMux.HandleFunc("/subscribe", cs.subscribeHandler) + cs.serveMux.HandleFunc("/publish", cs.publishHandler) + + return cs +} + // subscriber represents a subscriber. // Messages are sent on the msgs channel and if the client // cannot keep up with the messages, closeSlow is called. @@ -31,12 +63,7 @@ type subscriber struct { } func (cs *chatServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { - cs.registerOnce.Do(func() { - cs.m.Handle("/", http.FileServer(http.Dir("."))) - cs.m.HandleFunc("/subscribe", cs.subscribeHandler) - cs.m.HandleFunc("/publish", cs.publishHandler) - }) - cs.m.ServeHTTP(w, r) + cs.serveMux.ServeHTTP(w, r) } // subscribeHandler accepts the WebSocket connection and then subscribes @@ -44,7 +71,7 @@ func (cs *chatServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { func (cs *chatServer) subscribeHandler(w http.ResponseWriter, r *http.Request) { c, err := websocket.Accept(w, r, nil) if err != nil { - log.Print(err) + cs.logf("%v", err) return } defer c.Close(websocket.StatusInternalError, "") @@ -58,7 +85,8 @@ func (cs *chatServer) subscribeHandler(w http.ResponseWriter, r *http.Request) { return } if err != nil { - log.Print(err) + cs.logf("%v", err) + return } } @@ -69,7 +97,7 @@ func (cs *chatServer) publishHandler(w http.ResponseWriter, r *http.Request) { http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed) return } - body := io.LimitReader(r.Body, 8192) + body := http.MaxBytesReader(w, r.Body, 8192) msg, err := ioutil.ReadAll(body) if err != nil { http.Error(w, http.StatusText(http.StatusRequestEntityTooLarge), http.StatusRequestEntityTooLarge) @@ -93,7 +121,7 @@ func (cs *chatServer) subscribe(ctx context.Context, c *websocket.Conn) error { ctx = c.CloseRead(ctx) s := &subscriber{ - msgs: make(chan []byte, 16), + msgs: make(chan []byte, cs.subscriberMessageBuffer), closeSlow: func() { c.Close(websocket.StatusPolicyViolation, "connection too slow to keep up with messages") }, @@ -118,8 +146,10 @@ func (cs *chatServer) subscribe(ctx context.Context, c *websocket.Conn) error { // It never blocks and so messages to slow subscribers // are dropped. func (cs *chatServer) publish(msg []byte) { - cs.subscribersMu.RLock() - defer cs.subscribersMu.RUnlock() + cs.subscribersMu.Lock() + defer cs.subscribersMu.Unlock() + + cs.publishLimiter.Wait(context.Background()) for s := range cs.subscribers { select { @@ -133,9 +163,6 @@ func (cs *chatServer) publish(msg []byte) { // addSubscriber registers a subscriber. func (cs *chatServer) addSubscriber(s *subscriber) { cs.subscribersMu.Lock() - if cs.subscribers == nil { - cs.subscribers = make(map[*subscriber]struct{}) - } cs.subscribers[s] = struct{}{} cs.subscribersMu.Unlock() } diff --git a/chat-example/chat_test.go b/chat-example/chat_test.go index d1772381..491499cc 100644 --- a/chat-example/chat_test.go +++ b/chat-example/chat_test.go @@ -4,104 +4,214 @@ package main import ( "context" - "errors" + "crypto/rand" "fmt" + "math/big" "net/http" "net/http/httptest" "strings" + "sync" "testing" "time" + "golang.org/x/time/rate" + "nhooyr.io/websocket" ) -func TestGrace(t *testing.T) { +func Test_chatServer(t *testing.T) { t.Parallel() - var cs chatServer + // This is a simple echo test with a single client. + // The client sends a message and ensures it receives + // it on its WebSocket. + t.Run("simple", func(t *testing.T) { + t.Parallel() + + url, closeFn := setupTest(t) + defer closeFn() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + cl, err := newClient(ctx, url) + assertSuccess(t, err) + defer cl.Close() + + expMsg := randString(512) + err = cl.publish(ctx, expMsg) + assertSuccess(t, err) + + msg, err := cl.nextMessage() + assertSuccess(t, err) + + if expMsg != msg { + t.Fatalf("expected %v but got %v", expMsg, msg) + } + }) + + // This test is a complex concurrency test. + // 10 clients are started that send 128 different + // messages of max 128 bytes concurrently. + // + // The test verifies that every message is seen by ever client + // and no errors occur anywhere. + t.Run("concurrency", func(t *testing.T) { + t.Parallel() + + const nmessages = 128 + const maxMessageSize = 128 + const nclients = 10 + + url, closeFn := setupTest(t) + defer closeFn() + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + var clients []*client + var clientMsgs []map[string]struct{} + for i := 0; i < nclients; i++ { + cl, err := newClient(ctx, url) + assertSuccess(t, err) + defer cl.Close() + + clients = append(clients, cl) + clientMsgs = append(clientMsgs, randMessages(nmessages, maxMessageSize)) + } + + allMessages := make(map[string]struct{}) + for _, msgs := range clientMsgs { + for m := range msgs { + allMessages[m] = struct{}{} + } + } + + var wg sync.WaitGroup + for i, cl := range clients { + i := i + cl := cl + + wg.Add(1) + go func() { + defer wg.Done() + err := cl.publishMsgs(ctx, clientMsgs[i]) + if err != nil { + t.Errorf("client %d failed to publish all messages: %v", i, err) + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + err := testAllMessagesReceived(cl, nclients*nmessages, allMessages) + if err != nil { + t.Errorf("client %d failed to receive all messages: %v", i, err) + } + }() + } + + wg.Wait() + }) +} + +// setupTest sets up chatServer that can be used +// via the returned url. +// +// Defer closeFn to ensure everything is cleaned up at +// the end of the test. +// +// chatServer logs will be logged via t.Logf. +func setupTest(t *testing.T) (url string, closeFn func()) { + cs := newChatServer() + cs.logf = t.Logf + + // To ensure tests run quickly under even -race. + cs.subscriberMessageBuffer = 4096 + cs.publishLimiter.SetLimit(rate.Inf) + var g websocket.Grace - s := httptest.NewServer(g.Handler(&cs)) - defer s.Close() - defer g.Close() + s := httptest.NewServer(g.Handler(cs)) + return s.URL, func() { + s.Close() + g.Close() + } +} - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - defer cancel() +// testAllMessagesReceived ensures that after n reads, all msgs in msgs +// have been read. +func testAllMessagesReceived(cl *client, n int, msgs map[string]struct{}) error { + msgs = cloneMessages(msgs) - cl1, err := newClient(ctx, s.URL) - assertSuccess(t, err) - defer cl1.Close() + for i := 0; i < n; i++ { + msg, err := cl.nextMessage() + if err != nil { + return err + } + delete(msgs, msg) + } - cl2, err := newClient(ctx, s.URL) - assertSuccess(t, err) - defer cl2.Close() + if len(msgs) != 0 { + return fmt.Errorf("did not receive all expected messages: %q", msgs) + } + return nil +} - err = cl1.publish(ctx, "hello") - assertSuccess(t, err) +func cloneMessages(msgs map[string]struct{}) map[string]struct{} { + msgs2 := make(map[string]struct{}, len(msgs)) + for m := range msgs { + msgs2[m] = struct{}{} + } + return msgs2 +} - assertReceivedMessage(ctx, cl1, "hello") - assertReceivedMessage(ctx, cl2, "hello") +func randMessages(n, maxMessageLength int) map[string]struct{} { + msgs := make(map[string]struct{}) + for i := 0; i < n; i++ { + m := randString(randInt(maxMessageLength)) + if _, ok := msgs[m]; ok { + i-- + continue + } + msgs[m] = struct{}{} + } + return msgs +} + +func assertSuccess(t *testing.T, err error) { + t.Helper() + if err != nil { + t.Fatal(err) + } } type client struct { - msgs chan string - url string - c *websocket.Conn + url string + c *websocket.Conn } func newClient(ctx context.Context, url string) (*client, error) { - wsURL := strings.ReplaceAll(url, "http://", "ws://") + wsURL := strings.Replace(url, "http://", "ws://", 1) c, _, err := websocket.Dial(ctx, wsURL+"/subscribe", nil) if err != nil { return nil, err } cl := &client{ - msgs: make(chan string, 16), - url: url, - c: c, + url: url, + c: c, } - go cl.readLoop() return cl, nil } -func (cl *client) readLoop() { - defer cl.c.Close(websocket.StatusInternalError, "") - defer close(cl.msgs) - - for { - typ, b, err := cl.c.Read(context.Background()) +func (cl *client) publish(ctx context.Context, msg string) (err error) { + defer func() { if err != nil { - return + cl.c.Close(websocket.StatusInternalError, "publish failed") } + }() - if typ != websocket.MessageText { - cl.c.Close(websocket.StatusUnsupportedData, "expected text message") - return - } - - select { - case cl.msgs <- string(b): - default: - cl.c.Close(websocket.StatusInternalError, "messages coming in too fast to handle") - return - } - } -} - -func (cl *client) receive(ctx context.Context) (string, error) { - select { - case msg, ok := <-cl.msgs: - if !ok { - return "", errors.New("client closed") - } - return msg, nil - case <-ctx.Done(): - return "", ctx.Err() - } -} - -func (cl *client) publish(ctx context.Context, msg string) error { req, _ := http.NewRequestWithContext(ctx, http.MethodPost, cl.url+"/publish", strings.NewReader(msg)) resp, err := http.DefaultClient.Do(req) if err != nil { @@ -114,24 +224,59 @@ func (cl *client) publish(ctx context.Context, msg string) error { return nil } +func (cl *client) publishMsgs(ctx context.Context, msgs map[string]struct{}) error { + for m := range msgs { + err := cl.publish(ctx, m) + if err != nil { + return err + } + } + return nil +} + +func (cl *client) nextMessage() (string, error) { + typ, b, err := cl.c.Read(context.Background()) + if err != nil { + return "", err + } + + if typ != websocket.MessageText { + cl.c.Close(websocket.StatusUnsupportedData, "expected text message") + return "", fmt.Errorf("expected text message but got %v", typ) + } + return string(b), nil +} + func (cl *client) Close() error { return cl.c.Close(websocket.StatusNormalClosure, "") } -func assertSuccess(t *testing.T, err error) { - t.Helper() +// randString generates a random string with length n. +func randString(n int) string { + b := make([]byte, n) + _, err := rand.Reader.Read(b) if err != nil { - t.Fatal(err) + panic(fmt.Sprintf("failed to generate rand bytes: %v", err)) + } + + s := strings.ToValidUTF8(string(b), "_") + s = strings.ReplaceAll(s, "\x00", "_") + if len(s) > n { + return s[:n] } + if len(s) < n { + // Pad with = + extra := n - len(s) + return s + strings.Repeat("=", extra) + } + return s } -func assertReceivedMessage(ctx context.Context, cl *client, msg string) error { - msg, err := cl.receive(ctx) +// randInt returns a randomly generated integer between [0, max). +func randInt(max int) int { + x, err := rand.Int(rand.Reader, big.NewInt(int64(max))) if err != nil { - return err + panic(fmt.Sprintf("failed to get random int: %v", err)) } - if msg != "hello" { - return fmt.Errorf("expected hello but got %q", msg) - } - return nil + return int(x.Int64()) } diff --git a/chat-example/index.js b/chat-example/index.js index a42c2d30..5868e7ca 100644 --- a/chat-example/index.js +++ b/chat-example/index.js @@ -51,7 +51,7 @@ appendLog("Submit a message to get started!") // onsubmit publishes the message from the user when the form is submitted. - publishForm.onsubmit = ev => { + publishForm.onsubmit = async ev => { ev.preventDefault() const msg = messageInput.value @@ -61,9 +61,16 @@ messageInput.value = "" expectingMessage = true - fetch("/publish", { - method: "POST", - body: msg, - }) + try { + const resp = await fetch("/publish", { + method: "POST", + body: msg, + }) + if (resp.status !== 202) { + throw new Error(`Unexpected HTTP Status ${resp.status} ${resp.statusText}`) + } + } catch (err) { + appendLog(`Publish failed: ${err.message}`, true) + } } })() diff --git a/chat-example/main.go b/chat-example/main.go index a265f60c..1b6f3266 100644 --- a/chat-example/main.go +++ b/chat-example/main.go @@ -35,10 +35,10 @@ func run() error { } log.Printf("listening on http://%v", l.Addr()) - var cs chatServer + cs := newChatServer() var g websocket.Grace s := http.Server{ - Handler: g.Handler(&cs), + Handler: g.Handler(cs), ReadTimeout: time.Second * 10, WriteTimeout: time.Second * 10, } diff --git a/ci/test.mk b/ci/test.mk index c62a25b6..291d6beb 100644 --- a/ci/test.mk +++ b/ci/test.mk @@ -11,6 +11,7 @@ coveralls: gotest goveralls -coverprofile=ci/out/coverage.prof gotest: - go test -timeout=30m -covermode=count -coverprofile=ci/out/coverage.prof -coverpkg=./... $${GOTESTFLAGS-} ./... + go test -timeout=30m -covermode=atomic -coverprofile=ci/out/coverage.prof -coverpkg=./... $${GOTESTFLAGS-} ./... sed -i '/stringer\.go/d' ci/out/coverage.prof sed -i '/nhooyr.io\/websocket\/internal\/test/d' ci/out/coverage.prof + sed -i '/chat-example/d' ci/out/coverage.prof From ff876f6d14d96bf1de6094887d6bbf167991b046 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 27 Feb 2020 16:09:06 -0500 Subject: [PATCH 296/519] Make sure to release lock when acquiring and connection is closed. Closes #205 --- conn_notjs.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/conn_notjs.go b/conn_notjs.go index 2ec5f5bf..bb2eb22f 100644 --- a/conn_notjs.go +++ b/conn_notjs.go @@ -245,9 +245,11 @@ func (m *mu) lock(ctx context.Context) error { case m.ch <- struct{}{}: // To make sure the connection is certainly alive. // As it's possible the send on m.ch was selected - // the receive on closed. + // over the receive on closed. select { case <-m.c.closed: + // Make sure to release. + m.unlock() return m.c.closeErr default: } From 66eb672e854683f7295d1c068b1d725acba4beca Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 22 Mar 2020 12:34:23 -0400 Subject: [PATCH 297/519] Re-enable Wasm test on CI --- conn_test.go | 4 ---- go.mod | 4 ++-- go.sum | 4 ++++ 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/conn_test.go b/conn_test.go index 535afe24..64e6736f 100644 --- a/conn_test.go +++ b/conn_test.go @@ -268,10 +268,6 @@ func TestConn(t *testing.T) { func TestWasm(t *testing.T) { t.Parallel() - if os.Getenv("CI") != "" { - t.Skip("skipping on CI") - } - var wg sync.WaitGroup s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { wg.Add(1) diff --git a/go.mod b/go.mod index 801d6be6..60377823 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,9 @@ require ( github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee // indirect github.com/gobwas/pool v0.2.0 // indirect github.com/gobwas/ws v1.0.2 - github.com/golang/protobuf v1.3.3 + github.com/golang/protobuf v1.3.5 github.com/google/go-cmp v0.4.0 github.com/gorilla/websocket v1.4.1 - github.com/klauspost/compress v1.10.0 + github.com/klauspost/compress v1.10.3 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 ) diff --git a/go.sum b/go.sum index e4bbd62d..dac1ed3a 100644 --- a/go.sum +++ b/go.sum @@ -6,12 +6,16 @@ github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/klauspost/compress v1.10.0 h1:92XGj1AcYzA6UrVdd4qIIBrT8OroryvRvdmg/IfmC7Y= github.com/klauspost/compress v1.10.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= From 07343c2a717904b31466177f5dfeefb5ef5ab687 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 27 Feb 2020 21:03:35 -0500 Subject: [PATCH 298/519] Allow passing http:// and https:// URLS to Dial --- README.md | 8 ++++---- accept_js.go | 1 + chat-example/chat_test.go | 5 ++--- close_notjs.go | 17 +++++++++-------- conn_test.go | 2 +- dial.go | 3 +++ example_test.go | 7 +++++-- internal/test/wstest/url.go | 11 ----------- ws_js.go | 4 ++++ 9 files changed, 29 insertions(+), 29 deletions(-) delete mode 100644 internal/test/wstest/url.go diff --git a/README.md b/README.md index e967cd8a..2d71ce0b 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # websocket -[![godoc](https://godoc.org/nhooyr.io/websocket?status.svg)](https://godoc.org/nhooyr.io/websocket) +[![godoc](https://godoc.org/nhooyr.io/websocket?status.svg)](https://pkg.go.dev/nhooyr.io/websocket) websocket is a minimal and idiomatic WebSocket library for Go. @@ -16,8 +16,8 @@ go get nhooyr.io/websocket - First class [context.Context](https://blog.golang.org/context) support - Fully passes the WebSocket [autobahn-testsuite](https://github.com/crossbario/autobahn-testsuite) - Thorough unit tests with [90% coverage](https://coveralls.io/github/nhooyr/websocket) -- [Minimal dependencies](https://godoc.org/nhooyr.io/websocket?imports) -- JSON and protobuf helpers in the [wsjson](https://godoc.org/nhooyr.io/websocket/wsjson) and [wspb](https://godoc.org/nhooyr.io/websocket/wspb) subpackages +- [Minimal dependencies](https://pkg.go.dev/nhooyr.io/websocket?tab=imports) +- JSON and protobuf helpers in the [wsjson](https://pkg.go.dev/nhooyr.io/websocket/wsjson?tab=doc) and [wspb](https://pkg.go.dev/nhooyr.io/websocket/wspb?tab=doc) subpackages - Zero alloc reads and writes - Concurrent writes - [Close handshake](https://godoc.org/nhooyr.io/websocket#Conn.Close) @@ -98,7 +98,7 @@ Advantages of nhooyr.io/websocket: - [net.Conn](https://godoc.org/nhooyr.io/websocket#NetConn) wrapper - Zero alloc reads and writes ([gorilla/websocket#535](https://github.com/gorilla/websocket/issues/535)) - Full [context.Context](https://blog.golang.org/context) support -- Dial uses [net/http.Client](https://golang.org/pkg/net/http/#Client) +- Dials use [net/http.Client](https://golang.org/pkg/net/http/#Client) - Will enable easy HTTP/2 support in the future - Gorilla writes directly to a net.Conn and so duplicates features of net/http.Client. - Concurrent writes diff --git a/accept_js.go b/accept_js.go index 724b35b5..daad4b79 100644 --- a/accept_js.go +++ b/accept_js.go @@ -9,6 +9,7 @@ import ( type AcceptOptions struct { Subprotocols []string InsecureSkipVerify bool + OriginPatterns []string CompressionMode CompressionMode CompressionThreshold int } diff --git a/chat-example/chat_test.go b/chat-example/chat_test.go index 491499cc..2cbc995e 100644 --- a/chat-example/chat_test.go +++ b/chat-example/chat_test.go @@ -61,7 +61,7 @@ func Test_chatServer(t *testing.T) { const nmessages = 128 const maxMessageSize = 128 - const nclients = 10 + const nclients = 16 url, closeFn := setupTest(t) defer closeFn() @@ -191,8 +191,7 @@ type client struct { } func newClient(ctx context.Context, url string) (*client, error) { - wsURL := strings.Replace(url, "http://", "ws://", 1) - c, _, err := websocket.Dial(ctx, wsURL+"/subscribe", nil) + c, _, err := websocket.Dial(ctx, url+"/subscribe", nil) if err != nil { return nil, err } diff --git a/close_notjs.go b/close_notjs.go index 25372995..4f1cebcb 100644 --- a/close_notjs.go +++ b/close_notjs.go @@ -34,14 +34,15 @@ func (c *Conn) Close(code StatusCode, reason string) error { func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { defer errd.Wrap(&err, "failed to close WebSocket") - err = c.writeClose(code, reason) - if err != nil && CloseStatus(err) == -1 && err != errAlreadyWroteClose { - return err + writeErr := c.writeClose(code, reason) + closeHandshakeErr := c.waitCloseHandshake() + + if writeErr != nil { + return writeErr } - err = c.waitCloseHandshake() - if CloseStatus(err) == -1 { - return err + if CloseStatus(closeHandshakeErr) == -1 { + return closeHandshakeErr } return nil } @@ -50,10 +51,10 @@ var errAlreadyWroteClose = errors.New("already wrote close") func (c *Conn) writeClose(code StatusCode, reason string) error { c.closeMu.Lock() - closing := c.wroteClose + wroteClose := c.wroteClose c.wroteClose = true c.closeMu.Unlock() - if closing { + if wroteClose { return errAlreadyWroteClose } diff --git a/conn_test.go b/conn_test.go index af4fa4c0..7514540d 100644 --- a/conn_test.go +++ b/conn_test.go @@ -298,7 +298,7 @@ func TestWasm(t *testing.T) { defer cancel() cmd := exec.CommandContext(ctx, "go", "test", "-exec=wasmbrowsertest", "./...") - cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm", fmt.Sprintf("WS_ECHO_SERVER_URL=%v", wstest.URL(s))) + cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm", fmt.Sprintf("WS_ECHO_SERVER_URL=%v", s.URL)) b, err := cmd.CombinedOutput() if err != nil { diff --git a/dial.go b/dial.go index 50a0ecce..9ab680eb 100644 --- a/dial.go +++ b/dial.go @@ -58,6 +58,8 @@ type DialOptions struct { // This function requires at least Go 1.12 as it uses a new feature // in net/http to perform WebSocket handshakes. // See docs on the HTTPClient option and https://github.com/golang/go/issues/26937#issuecomment-415855861 +// +// URLs with http/https schemes will work and translated into ws/wss. func Dial(ctx context.Context, u string, opts *DialOptions) (*Conn, *http.Response, error) { return dial(ctx, u, opts, nil) } @@ -145,6 +147,7 @@ func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, copts u.Scheme = "http" case "wss": u.Scheme = "https" + case "http", "https": default: return nil, fmt.Errorf("unexpected url scheme: %q", u.Scheme) } diff --git a/example_test.go b/example_test.go index 462de376..39de0b80 100644 --- a/example_test.go +++ b/example_test.go @@ -1,5 +1,3 @@ -// +build !js - package websocket_test import ( @@ -187,3 +185,8 @@ func ExampleGrace() { s.Shutdown(ctx) g.Shutdown(ctx) } + +// This example demonstrates full stack chat with an automated test. +func Example_fullStackChat() { + // https://github.com/nhooyr/websocket/tree/master/chat-example +} diff --git a/internal/test/wstest/url.go b/internal/test/wstest/url.go deleted file mode 100644 index a11c61b4..00000000 --- a/internal/test/wstest/url.go +++ /dev/null @@ -1,11 +0,0 @@ -package wstest - -import ( - "net/http/httptest" - "strings" -) - -// URL returns the ws url for s. -func URL(s *httptest.Server) string { - return strings.Replace(s.URL, "http", "ws", 1) -} diff --git a/ws_js.go b/ws_js.go index a8c8b771..69019e61 100644 --- a/ws_js.go +++ b/ws_js.go @@ -9,6 +9,7 @@ import ( "net/http" "reflect" "runtime" + "strings" "sync" "syscall/js" @@ -257,6 +258,9 @@ func dial(ctx context.Context, url string, opts *DialOptions) (*Conn, *http.Resp opts = &DialOptions{} } + url = strings.Replace(url, "http://", "ws://", 1) + url = strings.Replace(url, "https://", "wss://", 1) + ws, err := wsjs.New(url, opts.Subprotocols) if err != nil { return nil, nil, err From 008b61622b1cb1774a35191d35b102139b02f321 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 22 Mar 2020 12:33:24 -0400 Subject: [PATCH 299/519] Remove Grace partially --- accept.go | 14 ------ conn_notjs.go | 5 --- example_echo_test.go | 5 +++ grace.go | 105 ++++++++++++++++++++----------------------- 4 files changed, 54 insertions(+), 75 deletions(-) diff --git a/accept.go b/accept.go index dd96c9bd..a583f232 100644 --- a/accept.go +++ b/accept.go @@ -75,13 +75,6 @@ func Accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Conn, err error) { defer errd.Wrap(&err, "failed to accept WebSocket connection") - g := graceFromRequest(r) - if g != nil && g.isShuttingdown() { - err := errors.New("server shutting down") - http.Error(w, err.Error(), http.StatusServiceUnavailable) - return nil, err - } - if opts == nil { opts = &AcceptOptions{} } @@ -152,13 +145,6 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con bw: brw.Writer, }) - if g != nil { - err = g.addConn(c) - if err != nil { - return nil, err - } - } - return c, nil } diff --git a/conn_notjs.go b/conn_notjs.go index f604898e..bb2eb22f 100644 --- a/conn_notjs.go +++ b/conn_notjs.go @@ -33,7 +33,6 @@ type Conn struct { flateThreshold int br *bufio.Reader bw *bufio.Writer - g *Grace readTimeout chan context.Context writeTimeout chan context.Context @@ -139,10 +138,6 @@ func (c *Conn) close(err error) { // closeErr. c.rwc.Close() - if c.g != nil { - c.g.delConn(c) - } - go func() { c.msgWriterState.close() diff --git a/example_echo_test.go b/example_echo_test.go index 0c0b84ea..fb212c1f 100644 --- a/example_echo_test.go +++ b/example_echo_test.go @@ -18,6 +18,11 @@ import ( "nhooyr.io/websocket/wsjson" ) +// TODO IMPROVE CANCELLATION AND SHUTDOWN +// TODO on context cancel send websocket going away and fix the read timeout error to be dependant on context deadline reached. +// TODO this way you cancel your context and the right message automatically gets sent. Furthrmore, then u can just use a simple waitgroup to wait for connections. +// TODO grace is wrong as it doesn't wait for the individual goroutines. + // This example starts a WebSocket echo server, // dials the server and then sends 5 different messages // and prints out the server's responses. diff --git a/grace.go b/grace.go index c53cd40b..a0ec8969 100644 --- a/grace.go +++ b/grace.go @@ -2,7 +2,6 @@ package websocket import ( "context" - "errors" "fmt" "net/http" "sync" @@ -17,79 +16,75 @@ import ( // Grace is intended to be used in harmony with net/http.Server's Shutdown and Close methods. // It's required as net/http's Shutdown and Close methods do not keep track of WebSocket // connections. +// +// Make sure to Close or Shutdown the *http.Server first as you don't want to accept +// any new connections while the existing websockets are being shut down. type Grace struct { - mu sync.Mutex - closed bool - shuttingDown bool - conns map[*Conn]struct{} + handlersMu sync.Mutex + closing bool + handlers map[context.Context]context.CancelFunc } // Handler returns a handler that wraps around h to record // all WebSocket connections accepted. // // Use Close or Shutdown to gracefully close recorded connections. +// Make sure to Close or Shutdown the *http.Server first. func (g *Grace) Handler(h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx := context.WithValue(r.Context(), gracefulContextKey{}, g) + ctx, cancel := context.WithCancel(r.Context()) + defer cancel() + r = r.WithContext(ctx) + + ok := g.add(w, ctx, cancel) + if !ok { + return + } + defer g.del(ctx) + h.ServeHTTP(w, r) }) } -func (g *Grace) isShuttingdown() bool { - g.mu.Lock() - defer g.mu.Unlock() - return g.shuttingDown -} - -func graceFromRequest(r *http.Request) *Grace { - g, _ := r.Context().Value(gracefulContextKey{}).(*Grace) - return g -} +func (g *Grace) add(w http.ResponseWriter, ctx context.Context, cancel context.CancelFunc) bool { + g.handlersMu.Lock() + defer g.handlersMu.Unlock() -func (g *Grace) addConn(c *Conn) error { - g.mu.Lock() - defer g.mu.Unlock() - if g.closed { - c.Close(StatusGoingAway, "server shutting down") - return errors.New("server shutting down") + if g.closing { + http.Error(w, "shutting down", http.StatusServiceUnavailable) + return false } - if g.conns == nil { - g.conns = make(map[*Conn]struct{}) + + if g.handlers == nil { + g.handlers = make(map[context.Context]context.CancelFunc) } - g.conns[c] = struct{}{} - c.g = g - return nil -} + g.handlers[ctx] = cancel -func (g *Grace) delConn(c *Conn) { - g.mu.Lock() - defer g.mu.Unlock() - delete(g.conns, c) + return true } -type gracefulContextKey struct{} +func (g *Grace) del(ctx context.Context) { + g.handlersMu.Lock() + defer g.handlersMu.Unlock() + + delete(g.handlers, ctx) +} // Close prevents the acceptance of new connections with // http.StatusServiceUnavailable and closes all accepted // connections with StatusGoingAway. +// +// Make sure to Close or Shutdown the *http.Server first. func (g *Grace) Close() error { - g.mu.Lock() - g.shuttingDown = true - g.closed = true - var wg sync.WaitGroup - for c := range g.conns { - wg.Add(1) - go func(c *Conn) { - defer wg.Done() - c.Close(StatusGoingAway, "server shutting down") - }(c) - - delete(g.conns, c) + g.handlersMu.Lock() + for _, cancel := range g.handlers { + cancel() } - g.mu.Unlock() + g.handlersMu.Unlock() - wg.Wait() + // Wait for all goroutines to exit. + g.Shutdown(context.Background()) return nil } @@ -97,18 +92,16 @@ func (g *Grace) Close() error { // Shutdown prevents the acceptance of new connections and waits until // all connections close. If the context is cancelled before that, it // calls Close to close all connections immediately. +// +// Make sure to Close or Shutdown the *http.Server first. func (g *Grace) Shutdown(ctx context.Context) error { defer g.Close() - g.mu.Lock() - g.shuttingDown = true - g.mu.Unlock() - // Same poll period used by net/http. t := time.NewTicker(500 * time.Millisecond) defer t.Stop() for { - if g.zeroConns() { + if g.zeroHandlers() { return nil } @@ -120,8 +113,8 @@ func (g *Grace) Shutdown(ctx context.Context) error { } } -func (g *Grace) zeroConns() bool { - g.mu.Lock() - defer g.mu.Unlock() - return len(g.conns) == 0 +func (g *Grace) zeroHandlers() bool { + g.handlersMu.Lock() + defer g.handlersMu.Unlock() + return len(g.handlers) == 0 } From b307b475131604cad9f43f1f30cc757725eca80e Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 13 Apr 2020 21:22:14 -0400 Subject: [PATCH 300/519] Improve docs and fix examples Closes #207 --- README.md | 17 +-- ci/test.mk | 2 +- example_test.go | 120 +++++++++--------- examples/README.md | 4 + {chat-example => examples/chat}/README.md | 0 {chat-example => examples/chat}/chat.go | 0 {chat-example => examples/chat}/chat_test.go | 0 {chat-example => examples/chat}/go.sum | 0 {chat-example => examples/chat}/index.css | 0 {chat-example => examples/chat}/index.html | 0 {chat-example => examples/chat}/index.js | 0 {chat-example => examples/chat}/main.go | 0 example_echo_test.go => examples/echo/echo.go | 4 +- 13 files changed, 78 insertions(+), 69 deletions(-) create mode 100644 examples/README.md rename {chat-example => examples/chat}/README.md (100%) rename {chat-example => examples/chat}/chat.go (100%) rename {chat-example => examples/chat}/chat_test.go (100%) rename {chat-example => examples/chat}/go.sum (100%) rename {chat-example => examples/chat}/index.css (100%) rename {chat-example => examples/chat}/index.html (100%) rename {chat-example => examples/chat}/index.js (100%) rename {chat-example => examples/chat}/main.go (100%) rename example_echo_test.go => examples/echo/echo.go (99%) diff --git a/README.md b/README.md index 2d71ce0b..3debf2f8 100644 --- a/README.md +++ b/README.md @@ -15,9 +15,9 @@ go get nhooyr.io/websocket - Minimal and idiomatic API - First class [context.Context](https://blog.golang.org/context) support - Fully passes the WebSocket [autobahn-testsuite](https://github.com/crossbario/autobahn-testsuite) -- Thorough unit tests with [90% coverage](https://coveralls.io/github/nhooyr/websocket) -- [Minimal dependencies](https://pkg.go.dev/nhooyr.io/websocket?tab=imports) -- JSON and protobuf helpers in the [wsjson](https://pkg.go.dev/nhooyr.io/websocket/wsjson?tab=doc) and [wspb](https://pkg.go.dev/nhooyr.io/websocket/wspb?tab=doc) subpackages +- Thorough tests with [90% coverage](https://coveralls.io/github/nhooyr/websocket) +- [Zero dependencies](https://pkg.go.dev/nhooyr.io/websocket?tab=imports) +- 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 - Zero alloc reads and writes - Concurrent writes - [Close handshake](https://godoc.org/nhooyr.io/websocket#Conn.Close) @@ -32,9 +32,10 @@ go get nhooyr.io/websocket ## Examples -For a production quality example that demonstrates the complete API, see the [echo example](https://godoc.org/nhooyr.io/websocket#example-package--Echo). +For a production quality example that demonstrates the complete API, see the +[echo example](./examples/echo). -For a full stack example, see [./chat-example](./chat-example). +For a full stack example, see the [chat example](./examples/chat). ### Server @@ -98,7 +99,7 @@ Advantages of nhooyr.io/websocket: - [net.Conn](https://godoc.org/nhooyr.io/websocket#NetConn) wrapper - Zero alloc reads and writes ([gorilla/websocket#535](https://github.com/gorilla/websocket/issues/535)) - Full [context.Context](https://blog.golang.org/context) support -- Dials use [net/http.Client](https://golang.org/pkg/net/http/#Client) +- Dial uses [net/http.Client](https://golang.org/pkg/net/http/#Client) - Will enable easy HTTP/2 support in the future - Gorilla writes directly to a net.Conn and so duplicates features of net/http.Client. - Concurrent writes @@ -111,7 +112,7 @@ Advantages of nhooyr.io/websocket: - Gorilla's implementation is slower and uses [unsafe](https://golang.org/pkg/unsafe/). - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support - Gorilla only supports no context takeover mode - - We use [klauspost/compress](https://github.com/klauspost/compress) for much lower memory usage ([gorilla/websocket#203](https://github.com/gorilla/websocket/issues/203)) + - 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)) - [CloseRead](https://godoc.org/nhooyr.io/websocket#Conn.CloseRead) helper ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492)) - Actively maintained ([gorilla/websocket#370](https://github.com/gorilla/websocket/issues/370)) @@ -120,7 +121,7 @@ Advantages of nhooyr.io/websocket: [golang.org/x/net/websocket](https://godoc.org/golang.org/x/net/websocket) is deprecated. See [golang/go/issues/18152](https://github.com/golang/go/issues/18152). -The [net.Conn](https://godoc.org/nhooyr.io/websocket#NetConn) wrapper will ease in transitioning +The [net.Conn](https://godoc.org/nhooyr.io/websocket#NetConn) can help in transitioning to nhooyr.io/websocket. #### gobwas/ws diff --git a/ci/test.mk b/ci/test.mk index 291d6beb..b2f92b7c 100644 --- a/ci/test.mk +++ b/ci/test.mk @@ -14,4 +14,4 @@ gotest: go test -timeout=30m -covermode=atomic -coverprofile=ci/out/coverage.prof -coverpkg=./... $${GOTESTFLAGS-} ./... sed -i '/stringer\.go/d' ci/out/coverage.prof sed -i '/nhooyr.io\/websocket\/internal\/test/d' ci/out/coverage.prof - sed -i '/chat-example/d' ci/out/coverage.prof + sed -i '/example/d' ci/out/coverage.prof diff --git a/example_test.go b/example_test.go index 39de0b80..632c4d6e 100644 --- a/example_test.go +++ b/example_test.go @@ -4,17 +4,16 @@ import ( "context" "log" "net/http" - "os" - "os/signal" "time" "nhooyr.io/websocket" "nhooyr.io/websocket/wsjson" ) -// This example accepts a WebSocket connection, reads a single JSON -// message from the client and then closes the connection. func ExampleAccept() { + // This handler accepts a WebSocket connection, reads a single JSON + // message from the client and then closes the connection. + fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c, err := websocket.Accept(w, r, nil) if err != nil { @@ -40,9 +39,10 @@ func ExampleAccept() { log.Fatal(err) } -// This example dials a server, writes a single JSON message and then -// closes the connection. func ExampleDial() { + // Dials a server, writes a single JSON message and then + // closes the connection. + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() @@ -60,9 +60,10 @@ func ExampleDial() { c.Close(websocket.StatusNormalClosure, "") } -// This example dials a server and then expects to be disconnected with status code -// websocket.StatusNormalClosure. func ExampleCloseStatus() { + // Dials a server and then expects to be disconnected with status code + // websocket.StatusNormalClosure. + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() @@ -78,9 +79,9 @@ func ExampleCloseStatus() { } } -// This example shows how to correctly handle a WebSocket connection -// on which you will only write and do not expect to read data messages. func Example_writeOnly() { + // This handler demonstrates how to correctly handle a write only WebSocket connection. + // i.e you only expect to write messages and do not expect to read any messages. fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c, err := websocket.Accept(w, r, nil) if err != nil { @@ -116,9 +117,9 @@ func Example_writeOnly() { log.Fatal(err) } -// This example demonstrates how to safely accept cross origin WebSockets -// from the origin example.com. func Example_crossOrigin() { + // This handler demonstrates how to safely accept cross origin WebSockets + // from the origin example.com. fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ OriginPatterns: []string{"example.com"}, @@ -141,52 +142,57 @@ func Example_crossOrigin() { // for 10 seconds. // If you CTRL+C while a connection is open, it will wait at most 30s // for all connections to terminate before shutting down. -func ExampleGrace() { - fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c, err := websocket.Accept(w, r, nil) - if err != nil { - log.Println(err) - return - } - defer c.Close(websocket.StatusInternalError, "the sky is falling") - - ctx := c.CloseRead(r.Context()) - select { - case <-ctx.Done(): - case <-time.After(time.Second * 10): - } - - c.Close(websocket.StatusNormalClosure, "") - }) - - var g websocket.Grace - s := &http.Server{ - Handler: g.Handler(fn), - ReadTimeout: time.Second * 15, - WriteTimeout: time.Second * 15, - } - - errc := make(chan error, 1) - go func() { - errc <- s.ListenAndServe() - }() - - sigs := make(chan os.Signal, 1) - signal.Notify(sigs, os.Interrupt) - select { - case err := <-errc: - log.Printf("failed to listen and serve: %v", err) - case sig := <-sigs: - log.Printf("terminating: %v", sig) - } - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) - defer cancel() - s.Shutdown(ctx) - g.Shutdown(ctx) -} +// func ExampleGrace() { +// fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { +// c, err := websocket.Accept(w, r, nil) +// if err != nil { +// log.Println(err) +// return +// } +// defer c.Close(websocket.StatusInternalError, "the sky is falling") +// +// ctx := c.CloseRead(r.Context()) +// select { +// case <-ctx.Done(): +// case <-time.After(time.Second * 10): +// } +// +// c.Close(websocket.StatusNormalClosure, "") +// }) +// +// var g websocket.Grace +// s := &http.Server{ +// Handler: g.Handler(fn), +// ReadTimeout: time.Second * 15, +// WriteTimeout: time.Second * 15, +// } +// +// errc := make(chan error, 1) +// go func() { +// errc <- s.ListenAndServe() +// }() +// +// sigs := make(chan os.Signal, 1) +// signal.Notify(sigs, os.Interrupt) +// select { +// case err := <-errc: +// log.Printf("failed to listen and serve: %v", err) +// case sig := <-sigs: +// log.Printf("terminating: %v", sig) +// } +// +// ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) +// defer cancel() +// s.Shutdown(ctx) +// g.Shutdown(ctx) +// } // This example demonstrates full stack chat with an automated test. func Example_fullStackChat() { - // https://github.com/nhooyr/websocket/tree/master/chat-example + // https://github.com/nhooyr/websocket/tree/master/examples/chat +} + +// This example demonstrates a echo server. +func Example_echo() { + // https://github.com/nhooyr/websocket/tree/master/examples/echo } diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 00000000..3cb437ae --- /dev/null +++ b/examples/README.md @@ -0,0 +1,4 @@ +# Examples + +This directory contains more involved examples unsuitable +for display with godoc. diff --git a/chat-example/README.md b/examples/chat/README.md similarity index 100% rename from chat-example/README.md rename to examples/chat/README.md diff --git a/chat-example/chat.go b/examples/chat/chat.go similarity index 100% rename from chat-example/chat.go rename to examples/chat/chat.go diff --git a/chat-example/chat_test.go b/examples/chat/chat_test.go similarity index 100% rename from chat-example/chat_test.go rename to examples/chat/chat_test.go diff --git a/chat-example/go.sum b/examples/chat/go.sum similarity index 100% rename from chat-example/go.sum rename to examples/chat/go.sum diff --git a/chat-example/index.css b/examples/chat/index.css similarity index 100% rename from chat-example/index.css rename to examples/chat/index.css diff --git a/chat-example/index.html b/examples/chat/index.html similarity index 100% rename from chat-example/index.html rename to examples/chat/index.html diff --git a/chat-example/index.js b/examples/chat/index.js similarity index 100% rename from chat-example/index.js rename to examples/chat/index.js diff --git a/chat-example/main.go b/examples/chat/main.go similarity index 100% rename from chat-example/main.go rename to examples/chat/main.go diff --git a/example_echo_test.go b/examples/echo/echo.go similarity index 99% rename from example_echo_test.go rename to examples/echo/echo.go index fb212c1f..0f31235d 100644 --- a/example_echo_test.go +++ b/examples/echo/echo.go @@ -1,6 +1,4 @@ -// +build !js - -package websocket_test +package main import ( "context" From f7ef6b827324774cae310d49aa5f2482cd953a4c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 13 Apr 2020 21:22:18 -0400 Subject: [PATCH 301/519] Remove grace for now Updates #209 --- conn_test.go | 11 ++- examples/chat/chat_test.go | 5 +- examples/chat/main.go | 11 +-- examples/echo/{echo.go => main.go} | 10 +-- grace.go | 120 ----------------------------- ws_js.go | 2 - 6 files changed, 15 insertions(+), 144 deletions(-) rename examples/echo/{echo.go => main.go} (96%) delete mode 100644 grace.go diff --git a/conn_test.go b/conn_test.go index 7514540d..68dc837d 100644 --- a/conn_test.go +++ b/conn_test.go @@ -271,12 +271,11 @@ func TestWasm(t *testing.T) { t.Skip("skipping on CI") } - var g websocket.Grace - defer g.Close() - s := httptest.NewServer(g.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // TODO grace + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ - Subprotocols: []string{"echo"}, - InsecureSkipVerify: true, + Subprotocols: []string{"echo"}, + OriginPatterns: []string{"*"}, }) if err != nil { t.Errorf("echo server failed: %v", err) @@ -291,7 +290,7 @@ func TestWasm(t *testing.T) { t.Errorf("echo server failed: %v", err) return } - }))) + })) defer s.Close() ctx, cancel := context.WithTimeout(context.Background(), time.Minute) diff --git a/examples/chat/chat_test.go b/examples/chat/chat_test.go index 2cbc995e..79523d2a 100644 --- a/examples/chat/chat_test.go +++ b/examples/chat/chat_test.go @@ -130,11 +130,10 @@ func setupTest(t *testing.T) (url string, closeFn func()) { cs.subscriberMessageBuffer = 4096 cs.publishLimiter.SetLimit(rate.Inf) - var g websocket.Grace - s := httptest.NewServer(g.Handler(cs)) + // TODO grace + s := httptest.NewServer(cs) return s.URL, func() { s.Close() - g.Close() } } diff --git a/examples/chat/main.go b/examples/chat/main.go index 1b6f3266..cc2d01e8 100644 --- a/examples/chat/main.go +++ b/examples/chat/main.go @@ -9,8 +9,6 @@ import ( "os" "os/signal" "time" - - "nhooyr.io/websocket" ) func main() { @@ -36,9 +34,9 @@ func run() error { log.Printf("listening on http://%v", l.Addr()) cs := newChatServer() - var g websocket.Grace + // TODO grace s := http.Server{ - Handler: g.Handler(cs), + Handler: cs, ReadTimeout: time.Second * 10, WriteTimeout: time.Second * 10, } @@ -59,8 +57,5 @@ func run() error { ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() - s.Shutdown(ctx) - g.Shutdown(ctx) - - return nil + return s.Shutdown(ctx) } diff --git a/examples/echo/echo.go b/examples/echo/main.go similarity index 96% rename from examples/echo/echo.go rename to examples/echo/main.go index 0f31235d..db2d06c9 100644 --- a/examples/echo/echo.go +++ b/examples/echo/main.go @@ -24,7 +24,7 @@ import ( // This example starts a WebSocket echo server, // dials the server and then sends 5 different messages // and prints out the server's responses. -func Example_echo() { +func main() { // First we listen on port 0 which means the OS will // assign us a random free port. This is the listener // the server will serve on and the client will connect to. @@ -34,15 +34,14 @@ func Example_echo() { } defer l.Close() - var g websocket.Grace - defer g.Close() + // TODO grace s := &http.Server{ - Handler: g.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := echoServer(w, r) if err != nil { log.Printf("echo server: %v", err) } - })), + }), ReadTimeout: time.Second * 15, WriteTimeout: time.Second * 15, } @@ -61,6 +60,7 @@ func Example_echo() { if err != nil { log.Fatalf("client failed: %v", err) } + // Output: // received: map[i:0] // received: map[i:1] diff --git a/grace.go b/grace.go deleted file mode 100644 index a0ec8969..00000000 --- a/grace.go +++ /dev/null @@ -1,120 +0,0 @@ -package websocket - -import ( - "context" - "fmt" - "net/http" - "sync" - "time" -) - -// Grace enables graceful shutdown of accepted WebSocket connections. -// -// Use Handler to wrap WebSocket handlers to record accepted connections -// and then use Close or Shutdown to gracefully close these connections. -// -// Grace is intended to be used in harmony with net/http.Server's Shutdown and Close methods. -// It's required as net/http's Shutdown and Close methods do not keep track of WebSocket -// connections. -// -// Make sure to Close or Shutdown the *http.Server first as you don't want to accept -// any new connections while the existing websockets are being shut down. -type Grace struct { - handlersMu sync.Mutex - closing bool - handlers map[context.Context]context.CancelFunc -} - -// Handler returns a handler that wraps around h to record -// all WebSocket connections accepted. -// -// Use Close or Shutdown to gracefully close recorded connections. -// Make sure to Close or Shutdown the *http.Server first. -func (g *Grace) Handler(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - ctx, cancel := context.WithCancel(r.Context()) - defer cancel() - - r = r.WithContext(ctx) - - ok := g.add(w, ctx, cancel) - if !ok { - return - } - defer g.del(ctx) - - h.ServeHTTP(w, r) - }) -} - -func (g *Grace) add(w http.ResponseWriter, ctx context.Context, cancel context.CancelFunc) bool { - g.handlersMu.Lock() - defer g.handlersMu.Unlock() - - if g.closing { - http.Error(w, "shutting down", http.StatusServiceUnavailable) - return false - } - - if g.handlers == nil { - g.handlers = make(map[context.Context]context.CancelFunc) - } - g.handlers[ctx] = cancel - - return true -} - -func (g *Grace) del(ctx context.Context) { - g.handlersMu.Lock() - defer g.handlersMu.Unlock() - - delete(g.handlers, ctx) -} - -// Close prevents the acceptance of new connections with -// http.StatusServiceUnavailable and closes all accepted -// connections with StatusGoingAway. -// -// Make sure to Close or Shutdown the *http.Server first. -func (g *Grace) Close() error { - g.handlersMu.Lock() - for _, cancel := range g.handlers { - cancel() - } - g.handlersMu.Unlock() - - // Wait for all goroutines to exit. - g.Shutdown(context.Background()) - - return nil -} - -// Shutdown prevents the acceptance of new connections and waits until -// all connections close. If the context is cancelled before that, it -// calls Close to close all connections immediately. -// -// Make sure to Close or Shutdown the *http.Server first. -func (g *Grace) Shutdown(ctx context.Context) error { - defer g.Close() - - // Same poll period used by net/http. - t := time.NewTicker(500 * time.Millisecond) - defer t.Stop() - for { - if g.zeroHandlers() { - return nil - } - - select { - case <-t.C: - case <-ctx.Done(): - return fmt.Errorf("failed to shutdown WebSockets: %w", ctx.Err()) - } - } -} - -func (g *Grace) zeroHandlers() bool { - g.handlersMu.Lock() - defer g.handlersMu.Unlock() - return len(g.handlers) == 0 -} diff --git a/ws_js.go b/ws_js.go index 69019e61..b87e32cd 100644 --- a/ws_js.go +++ b/ws_js.go @@ -39,8 +39,6 @@ type Conn struct { readSignal chan struct{} readBufMu sync.Mutex readBuf []wsjs.MessageEvent - - g *Grace } func (c *Conn) close(err error, wasClean bool) { From 98779ee0af50df3c774208c733ae18572ef409b6 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 13 Apr 2020 21:22:21 -0400 Subject: [PATCH 302/519] Fix outdated close handshake docs Closes #212 --- close_notjs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/close_notjs.go b/close_notjs.go index 4f1cebcb..fd9262b5 100644 --- a/close_notjs.go +++ b/close_notjs.go @@ -84,7 +84,7 @@ func (c *Conn) writeClose(code StatusCode, reason string) error { func (c *Conn) waitCloseHandshake() error { defer c.close(nil) - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() err := c.readMu.lock(ctx) From 5db7b716a02b179de735256d5502a4ac360a7942 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 13 Apr 2020 21:22:23 -0400 Subject: [PATCH 303/519] Clarify CloseRead docs Closes #208 --- read.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/read.go b/read.go index 381cea3d..afd08cc7 100644 --- a/read.go +++ b/read.go @@ -52,7 +52,7 @@ func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { // // Call CloseRead when you do not expect to read any more messages. // Since it actively reads from the connection, it will ensure that ping, pong and close -// frames are responded to. +// frames are responded to. This means c.Ping and c.Close will still work as expected. func (c *Conn) CloseRead(ctx context.Context) context.Context { ctx, cancel := context.WithCancel(ctx) go func() { From d0fa6bf84a14583ed29ded6f11d1e8665363a8b3 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 13 Apr 2020 21:36:00 -0400 Subject: [PATCH 304/519] Update prettier invocation for v2.0.0 --- ci/fmt.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/fmt.mk b/ci/fmt.mk index 3512d02f..1ed2920f 100644 --- a/ci/fmt.mk +++ b/ci/fmt.mk @@ -13,7 +13,7 @@ goimports: gen goimports -w "-local=$$(go list -m)" . prettier: - prettier --write --print-width=120 --no-semi --trailing-comma=all --loglevel=warn $$(git ls-files "*.yml" "*.md" "*.js" "*.css" "*.html") + prettier --write --print-width=120 --no-semi --trailing-comma=all --loglevel=warn --arrow-parens=avoid $$(git ls-files "*.yml" "*.md" "*.js" "*.css" "*.html") gen: stringer -type=opcode,MessageType,StatusCode -output=stringer.go From ba35516b80c8f8474024174e0899b557310b8f14 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 13 Apr 2020 21:45:23 -0400 Subject: [PATCH 305/519] Doc fixes --- README.md | 26 +++++++++++++------------- close_notjs.go | 1 + dial.go | 2 +- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 3debf2f8..60967789 100644 --- a/README.md +++ b/README.md @@ -20,11 +20,11 @@ go get nhooyr.io/websocket - 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 - Zero alloc reads and writes - Concurrent writes -- [Close handshake](https://godoc.org/nhooyr.io/websocket#Conn.Close) -- [net.Conn](https://godoc.org/nhooyr.io/websocket#NetConn) wrapper -- [Ping pong](https://godoc.org/nhooyr.io/websocket#Conn.Ping) API +- [Close handshake](https://pkg.go.dev/nhooyr.io/websocket#Conn.Close) +- [net.Conn](https://pkg.go.dev/nhooyr.io/websocket#NetConn) wrapper +- [Ping pong](https://pkg.go.dev/nhooyr.io/websocket#Conn.Ping) API - [RFC 7692](https://tools.ietf.org/html/rfc7692) permessage-deflate compression -- Compile to [Wasm](https://godoc.org/nhooyr.io/websocket#hdr-Wasm) +- Compile to [Wasm](https://pkg.go.dev/nhooyr.io/websocket#hdr-Wasm) ## Roadmap @@ -89,14 +89,14 @@ c.Close(websocket.StatusNormalClosure, "") Advantages of [gorilla/websocket](https://github.com/gorilla/websocket): - Mature and widely used -- [Prepared writes](https://godoc.org/github.com/gorilla/websocket#PreparedMessage) -- Configurable [buffer sizes](https://godoc.org/github.com/gorilla/websocket#hdr-Buffers) +- [Prepared writes](https://pkg.go.dev/github.com/gorilla/websocket#PreparedMessage) +- Configurable [buffer sizes](https://pkg.go.dev/github.com/gorilla/websocket#hdr-Buffers) Advantages of nhooyr.io/websocket: - Minimal and idiomatic API - - 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. -- [net.Conn](https://godoc.org/nhooyr.io/websocket#NetConn) wrapper + - 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. +- [net.Conn](https://pkg.go.dev/nhooyr.io/websocket#NetConn) wrapper - Zero alloc reads and writes ([gorilla/websocket#535](https://github.com/gorilla/websocket/issues/535)) - Full [context.Context](https://blog.golang.org/context) support - Dial uses [net/http.Client](https://golang.org/pkg/net/http/#Client) @@ -104,24 +104,24 @@ Advantages of nhooyr.io/websocket: - Gorilla writes directly to a net.Conn and so duplicates features of net/http.Client. - Concurrent writes - Close handshake ([gorilla/websocket#448](https://github.com/gorilla/websocket/issues/448)) -- Idiomatic [ping pong](https://godoc.org/nhooyr.io/websocket#Conn.Ping) API +- Idiomatic [ping pong](https://pkg.go.dev/nhooyr.io/websocket#Conn.Ping) API - Gorilla requires registering a pong callback before sending a Ping - Can target Wasm ([gorilla/websocket#432](https://github.com/gorilla/websocket/issues/432)) -- Transparent message buffer reuse with [wsjson](https://godoc.org/nhooyr.io/websocket/wsjson) and [wspb](https://godoc.org/nhooyr.io/websocket/wspb) subpackages +- 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 - [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster WebSocket masking implementation in pure Go - Gorilla's implementation is slower and uses [unsafe](https://golang.org/pkg/unsafe/). - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support - Gorilla only supports no context takeover mode - 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)) -- [CloseRead](https://godoc.org/nhooyr.io/websocket#Conn.CloseRead) helper ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492)) +- [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492)) - Actively maintained ([gorilla/websocket#370](https://github.com/gorilla/websocket/issues/370)) #### golang.org/x/net/websocket -[golang.org/x/net/websocket](https://godoc.org/golang.org/x/net/websocket) is deprecated. +[golang.org/x/net/websocket](https://pkg.go.dev/golang.org/x/net/websocket) is deprecated. See [golang/go/issues/18152](https://github.com/golang/go/issues/18152). -The [net.Conn](https://godoc.org/nhooyr.io/websocket#NetConn) can help in transitioning +The [net.Conn](https://pkg.go.dev/nhooyr.io/websocket#NetConn) can help in transitioning to nhooyr.io/websocket. #### gobwas/ws diff --git a/close_notjs.go b/close_notjs.go index fd9262b5..c30ac87a 100644 --- a/close_notjs.go +++ b/close_notjs.go @@ -63,6 +63,7 @@ func (c *Conn) writeClose(code StatusCode, reason string) error { Reason: reason, } + // TODO one problem with this is that if the connection is actually closed in the meantime, the error returned below will be this one lol. c.setCloseErr(fmt.Errorf("sent close frame: %w", ce)) var p []byte diff --git a/dial.go b/dial.go index 9ab680eb..2b25e351 100644 --- a/dial.go +++ b/dial.go @@ -59,7 +59,7 @@ type DialOptions struct { // in net/http to perform WebSocket handshakes. // See docs on the HTTPClient option and https://github.com/golang/go/issues/26937#issuecomment-415855861 // -// URLs with http/https schemes will work and translated into ws/wss. +// URLs with http/https schemes will work and are interpreted as ws/wss. func Dial(ctx context.Context, u string, opts *DialOptions) (*Conn, *http.Response, error) { return dial(ctx, u, opts, nil) } From c4d4650128fa3c5a2e2da7c41b7749d62e53fae7 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 13 Apr 2020 22:27:02 -0400 Subject: [PATCH 306/519] Fix bad close handshake logic This doesn't affect real world applications due to buffering but the testss would occasionally fail on CI due to the code not handling an immediate close after writing the close frame while resetting the write timeout. --- close_notjs.go | 28 +++++++++++++++++----------- go.sum | 4 ---- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/close_notjs.go b/close_notjs.go index c30ac87a..4251311d 100644 --- a/close_notjs.go +++ b/close_notjs.go @@ -44,6 +44,7 @@ func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { if CloseStatus(closeHandshakeErr) == -1 { return closeHandshakeErr } + return nil } @@ -63,23 +64,28 @@ func (c *Conn) writeClose(code StatusCode, reason string) error { Reason: reason, } - // TODO one problem with this is that if the connection is actually closed in the meantime, the error returned below will be this one lol. - c.setCloseErr(fmt.Errorf("sent close frame: %w", ce)) - var p []byte - var err error + var marshalErr error if ce.Code != StatusNoStatusRcvd { - p, err = ce.bytes() - if err != nil { - log.Printf("websocket: %v", err) + p, marshalErr = ce.bytes() + if marshalErr != nil { + log.Printf("websocket: %v", marshalErr) } } - werr := c.writeControl(context.Background(), opClose, p) - if err != nil { - return err + writeErr := c.writeControl(context.Background(), opClose, p) + if CloseStatus(writeErr) != -1 { + // Not a real error if it's due to a close frame being received. + writeErr = nil + } + + // We do this after in case there was an error writing the close frame. + c.setCloseErr(fmt.Errorf("sent close frame: %w", ce)) + + if marshalErr != nil { + return marshalErr } - return werr + return writeErr } func (c *Conn) waitCloseHandshake() error { diff --git a/go.sum b/go.sum index dac1ed3a..736df430 100644 --- a/go.sum +++ b/go.sum @@ -4,16 +4,12 @@ github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/klauspost/compress v1.10.0 h1:92XGj1AcYzA6UrVdd4qIIBrT8OroryvRvdmg/IfmC7Y= -github.com/klauspost/compress v1.10.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= From d34c89a325afbef0a8c4d23cece6fb4e4cc2b1a5 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 13 Apr 2020 22:31:35 -0400 Subject: [PATCH 307/519] Prevent all writes after close Closes #213 --- write.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/write.go b/write.go index baa5e6e2..2d439de7 100644 --- a/write.go +++ b/write.go @@ -246,7 +246,13 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco if err != nil { return 0, err } - defer c.writeFrameMu.unlock() + defer func() { + // We leave it locked when writing the close frame to avoid + // any other goroutine writing any other frame. + if opcode != opClose { + c.writeFrameMu.unlock() + } + }() select { case <-c.closed: From 2dc66c3f143f34f669248262380fd7c38eba107e Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 13 Apr 2020 22:34:47 -0400 Subject: [PATCH 308/519] Check whether the connection is closed before returning a write IO error Closes #215 --- write.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/write.go b/write.go index 2d439de7..60a4fba0 100644 --- a/write.go +++ b/write.go @@ -262,8 +262,14 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco defer func() { if err != nil { - err = fmt.Errorf("failed to write frame: %w", err) + select { + case <-c.closed: + err = c.closeErr + case <-ctx.Done(): + err = ctx.Err() + } c.close(err) + err = fmt.Errorf("failed to write frame: %w", err) } }() From 1d80cf339293725be66c457a5caa0a136ec743c5 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 13 Apr 2020 22:45:59 -0400 Subject: [PATCH 309/519] Final doc fixes --- README.md | 2 +- accept.go | 6 ++---- conn_test.go | 1 - examples/chat/chat_test.go | 1 - examples/chat/main.go | 3 +-- examples/echo/main.go | 6 ------ 6 files changed, 4 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 60967789..1f1ca46d 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ go get nhooyr.io/websocket - First class [context.Context](https://blog.golang.org/context) support - Fully passes the WebSocket [autobahn-testsuite](https://github.com/crossbario/autobahn-testsuite) - Thorough tests with [90% coverage](https://coveralls.io/github/nhooyr/websocket) -- [Zero dependencies](https://pkg.go.dev/nhooyr.io/websocket?tab=imports) +- [Minimal dependencies](https://pkg.go.dev/nhooyr.io/websocket?tab=imports) - 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 - Zero alloc reads and writes - Concurrent writes diff --git a/accept.go b/accept.go index a583f232..47e20b52 100644 --- a/accept.go +++ b/accept.go @@ -134,7 +134,7 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con b, _ := brw.Reader.Peek(brw.Reader.Buffered()) brw.Reader.Reset(io.MultiReader(bytes.NewReader(b), netConn)) - c := newConn(connConfig{ + return newConn(connConfig{ subprotocol: w.Header().Get("Sec-WebSocket-Protocol"), rwc: netConn, client: false, @@ -143,9 +143,7 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con br: brw.Reader, bw: brw.Writer, - }) - - return c, nil + }), nil } func verifyClientRequest(w http.ResponseWriter, r *http.Request) (errCode int, _ error) { diff --git a/conn_test.go b/conn_test.go index 68dc837d..451d093a 100644 --- a/conn_test.go +++ b/conn_test.go @@ -271,7 +271,6 @@ func TestWasm(t *testing.T) { t.Skip("skipping on CI") } - // TODO grace s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ Subprotocols: []string{"echo"}, diff --git a/examples/chat/chat_test.go b/examples/chat/chat_test.go index 79523d2a..eae18580 100644 --- a/examples/chat/chat_test.go +++ b/examples/chat/chat_test.go @@ -130,7 +130,6 @@ func setupTest(t *testing.T) (url string, closeFn func()) { cs.subscriberMessageBuffer = 4096 cs.publishLimiter.SetLimit(rate.Inf) - // TODO grace s := httptest.NewServer(cs) return s.URL, func() { s.Close() diff --git a/examples/chat/main.go b/examples/chat/main.go index cc2d01e8..7f3cf6f3 100644 --- a/examples/chat/main.go +++ b/examples/chat/main.go @@ -34,8 +34,7 @@ func run() error { log.Printf("listening on http://%v", l.Addr()) cs := newChatServer() - // TODO grace - s := http.Server{ + s := &http.Server{ Handler: cs, ReadTimeout: time.Second * 10, WriteTimeout: time.Second * 10, diff --git a/examples/echo/main.go b/examples/echo/main.go index db2d06c9..f1771752 100644 --- a/examples/echo/main.go +++ b/examples/echo/main.go @@ -16,11 +16,6 @@ import ( "nhooyr.io/websocket/wsjson" ) -// TODO IMPROVE CANCELLATION AND SHUTDOWN -// TODO on context cancel send websocket going away and fix the read timeout error to be dependant on context deadline reached. -// TODO this way you cancel your context and the right message automatically gets sent. Furthrmore, then u can just use a simple waitgroup to wait for connections. -// TODO grace is wrong as it doesn't wait for the individual goroutines. - // This example starts a WebSocket echo server, // dials the server and then sends 5 different messages // and prints out the server's responses. @@ -34,7 +29,6 @@ func main() { } defer l.Close() - // TODO grace s := &http.Server{ Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := echoServer(w, r) From 7157c051de3fd1bf675b387ebc5db574634629fd Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 13 Apr 2020 22:49:04 -0400 Subject: [PATCH 310/519] Clarify dependency situation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1f1ca46d..14c39293 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ go get nhooyr.io/websocket - First class [context.Context](https://blog.golang.org/context) support - Fully passes the WebSocket [autobahn-testsuite](https://github.com/crossbario/autobahn-testsuite) - Thorough tests with [90% coverage](https://coveralls.io/github/nhooyr/websocket) -- [Minimal dependencies](https://pkg.go.dev/nhooyr.io/websocket?tab=imports) +- [Single dependency](https://pkg.go.dev/nhooyr.io/websocket?tab=imports) - 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 - Zero alloc reads and writes - Concurrent writes From 4edcada120234320ad58e8b6cd82b823aae4326f Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 13 Apr 2020 22:56:56 -0400 Subject: [PATCH 311/519] Disable safari compression compatability layer Closes #218 --- accept.go | 5 +++-- compress.go | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/accept.go b/accept.go index 47e20b52..6bed54da 100644 --- a/accept.go +++ b/accept.go @@ -231,8 +231,9 @@ func acceptCompression(r *http.Request, w http.ResponseWriter, mode CompressionM switch ext.name { case "permessage-deflate": return acceptDeflate(w, ext, mode) - case "x-webkit-deflate-frame": - return acceptWebkitDeflate(w, ext, mode) + // Disabled for now, see https://github.com/nhooyr/websocket/issues/218 + // case "x-webkit-deflate-frame": + // return acceptWebkitDeflate(w, ext, mode) } } return nil, nil diff --git a/compress.go b/compress.go index 57446d01..80b46d1c 100644 --- a/compress.go +++ b/compress.go @@ -7,6 +7,7 @@ package websocket // by safari. See https://tools.ietf.org/html/draft-tyoshino-hybi-websocket-perframe-deflate-06 // It will work the same in every way except that we cannot signal to the peer we // want to use no context takeover on our side, we can only signal that they should. +// It is however currently disabled due to Safari bugs. See https://github.com/nhooyr/websocket/issues/218 type CompressionMode int const ( From 71a12fb8e2f0d73b6711919125f7375e344bc739 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 13 Apr 2020 23:08:30 -0400 Subject: [PATCH 312/519] Fix for CI --- accept.go | 6 +++--- accept_test.go | 32 ++++++++++++++++---------------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/accept.go b/accept.go index 6bed54da..c43d9616 100644 --- a/accept.go +++ b/accept.go @@ -231,9 +231,9 @@ func acceptCompression(r *http.Request, w http.ResponseWriter, mode CompressionM switch ext.name { case "permessage-deflate": return acceptDeflate(w, ext, mode) - // Disabled for now, see https://github.com/nhooyr/websocket/issues/218 - // case "x-webkit-deflate-frame": - // return acceptWebkitDeflate(w, ext, mode) + // Disabled for now, see https://github.com/nhooyr/websocket/issues/218 + // case "x-webkit-deflate-frame": + // return acceptWebkitDeflate(w, ext, mode) } } return nil, nil diff --git a/accept_test.go b/accept_test.go index 40a7b40c..9b18d8e1 100644 --- a/accept_test.go +++ b/accept_test.go @@ -356,22 +356,22 @@ func Test_acceptCompression(t *testing.T) { reqSecWebSocketExtensions: "permessage-deflate; meow", error: true, }, - { - name: "x-webkit-deflate-frame", - mode: CompressionNoContextTakeover, - reqSecWebSocketExtensions: "x-webkit-deflate-frame; no_context_takeover", - respSecWebSocketExtensions: "x-webkit-deflate-frame; no_context_takeover", - expCopts: &compressionOptions{ - clientNoContextTakeover: true, - serverNoContextTakeover: true, - }, - }, - { - name: "x-webkit-deflate/error", - mode: CompressionNoContextTakeover, - reqSecWebSocketExtensions: "x-webkit-deflate-frame; max_window_bits", - error: true, - }, + // { + // name: "x-webkit-deflate-frame", + // mode: CompressionNoContextTakeover, + // reqSecWebSocketExtensions: "x-webkit-deflate-frame; no_context_takeover", + // respSecWebSocketExtensions: "x-webkit-deflate-frame; no_context_takeover", + // expCopts: &compressionOptions{ + // clientNoContextTakeover: true, + // serverNoContextTakeover: true, + // }, + // }, + // { + // name: "x-webkit-deflate/error", + // mode: CompressionNoContextTakeover, + // reqSecWebSocketExtensions: "x-webkit-deflate-frame; max_window_bits", + // error: true, + // }, } for _, tc := range testCases { From c40b7029ac3087d516721fa1379be2844024ccac Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 14 Apr 2020 02:04:34 -0400 Subject: [PATCH 313/519] Add automated test and documentation for echo example Closes #223 --- ci/test.mk | 2 +- conn_test.go | 2 +- examples/chat/README.md | 2 +- examples/chat/chat_test.go | 2 - examples/chat/go.sum | 18 ---- examples/chat/main.go | 2 +- examples/echo/README.md | 21 +++++ examples/echo/main.go | 161 +++++++---------------------------- examples/echo/server.go | 81 ++++++++++++++++++ examples/echo/server_test.go | 54 ++++++++++++ 10 files changed, 191 insertions(+), 154 deletions(-) delete mode 100644 examples/chat/go.sum create mode 100644 examples/echo/README.md create mode 100644 examples/echo/server.go create mode 100644 examples/echo/server_test.go diff --git a/ci/test.mk b/ci/test.mk index b2f92b7c..553a05c5 100644 --- a/ci/test.mk +++ b/ci/test.mk @@ -14,4 +14,4 @@ gotest: go test -timeout=30m -covermode=atomic -coverprofile=ci/out/coverage.prof -coverpkg=./... $${GOTESTFLAGS-} ./... sed -i '/stringer\.go/d' ci/out/coverage.prof sed -i '/nhooyr.io\/websocket\/internal\/test/d' ci/out/coverage.prof - sed -i '/example/d' ci/out/coverage.prof + sed -i '/examples/d' ci/out/coverage.prof diff --git a/conn_test.go b/conn_test.go index 451d093a..28e8d59d 100644 --- a/conn_test.go +++ b/conn_test.go @@ -295,7 +295,7 @@ func TestWasm(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - cmd := exec.CommandContext(ctx, "go", "test", "-exec=wasmbrowsertest", "./...") + cmd := exec.CommandContext(ctx, "go", "test", "-exec=wasmbrowsertest", ".") cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm", fmt.Sprintf("WS_ECHO_SERVER_URL=%v", s.URL)) b, err := cmd.CombinedOutput() diff --git a/examples/chat/README.md b/examples/chat/README.md index a4c99a93..57424220 100644 --- a/examples/chat/README.md +++ b/examples/chat/README.md @@ -3,7 +3,7 @@ This directory contains a full stack example of a simple chat webapp using nhooyr.io/websocket. ```bash -$ cd chat-example +$ cd examples/chat $ go run . localhost:0 listening on http://127.0.0.1:51055 ``` diff --git a/examples/chat/chat_test.go b/examples/chat/chat_test.go index eae18580..f80f1de1 100644 --- a/examples/chat/chat_test.go +++ b/examples/chat/chat_test.go @@ -1,5 +1,3 @@ -// +build !js - package main import ( diff --git a/examples/chat/go.sum b/examples/chat/go.sum deleted file mode 100644 index e4bbd62d..00000000 --- a/examples/chat/go.sum +++ /dev/null @@ -1,18 +0,0 @@ -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= -github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= -github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= -github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/klauspost/compress v1.10.0 h1:92XGj1AcYzA6UrVdd4qIIBrT8OroryvRvdmg/IfmC7Y= -github.com/klauspost/compress v1.10.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/examples/chat/main.go b/examples/chat/main.go index 7f3cf6f3..3fcec6be 100644 --- a/examples/chat/main.go +++ b/examples/chat/main.go @@ -20,7 +20,7 @@ func main() { } } -// run initializes the chatServer and routes and then +// run initializes the chatServer and then // starts a http.Server for the passed in address. func run() error { if len(os.Args) < 2 { diff --git a/examples/echo/README.md b/examples/echo/README.md new file mode 100644 index 00000000..7f42c3c5 --- /dev/null +++ b/examples/echo/README.md @@ -0,0 +1,21 @@ +# Echo Example + +This directory contains a echo server example using nhooyr.io/websocket. + +```bash +$ cd examples/echo +$ go run . localhost:0 +listening on http://127.0.0.1:51055 +``` + +You can use a WebSocket client like https://github.com/hashrocket/ws to connect. All messages +written will be echoed back. + +## Structure + +The server is in `server.go` and is implemented as a `http.HandlerFunc` that accepts the WebSocket +and then reads all messages and writes them exactly as is back to the connection. + +`server_test.go` contains a small unit test to verify it works correctly. + +`main.go` brings it all together so that you can run it and play around with it. diff --git a/examples/echo/main.go b/examples/echo/main.go index f1771752..16d78a79 100644 --- a/examples/echo/main.go +++ b/examples/echo/main.go @@ -3,158 +3,59 @@ package main import ( "context" "errors" - "fmt" - "io" "log" "net" "net/http" + "os" + "os/signal" "time" - - "golang.org/x/time/rate" - - "nhooyr.io/websocket" - "nhooyr.io/websocket/wsjson" ) -// This example starts a WebSocket echo server, -// dials the server and then sends 5 different messages -// and prints out the server's responses. func main() { - // First we listen on port 0 which means the OS will - // assign us a random free port. This is the listener - // the server will serve on and the client will connect to. - l, err := net.Listen("tcp", "localhost:0") - if err != nil { - log.Fatalf("failed to listen: %v", err) - } - defer l.Close() - - s := &http.Server{ - Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - err := echoServer(w, r) - if err != nil { - log.Printf("echo server: %v", err) - } - }), - ReadTimeout: time.Second * 15, - WriteTimeout: time.Second * 15, - } - defer s.Close() - - // This starts the echo server on the listener. - go func() { - err := s.Serve(l) - if err != http.ErrServerClosed { - log.Fatalf("failed to listen and serve: %v", err) - } - }() + log.SetFlags(0) - // Now we dial the server, send the messages and echo the responses. - err = client("ws://" + l.Addr().String()) + err := run() if err != nil { - log.Fatalf("client failed: %v", err) - } - - // Output: - // received: map[i:0] - // received: map[i:1] - // received: map[i:2] - // received: map[i:3] - // received: map[i:4] -} - -// echoServer is the WebSocket echo server implementation. -// It ensures the client speaks the echo subprotocol and -// only allows one message every 100ms with a 10 message burst. -func echoServer(w http.ResponseWriter, r *http.Request) error { - c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ - Subprotocols: []string{"echo"}, - }) - if err != nil { - return err - } - defer c.Close(websocket.StatusInternalError, "the sky is falling") - - if c.Subprotocol() != "echo" { - c.Close(websocket.StatusPolicyViolation, "client must speak the echo subprotocol") - return errors.New("client does not speak echo sub protocol") - } - - l := rate.NewLimiter(rate.Every(time.Millisecond*100), 10) - for { - err = echo(r.Context(), c, l) - if websocket.CloseStatus(err) == websocket.StatusNormalClosure { - return nil - } - if err != nil { - return fmt.Errorf("failed to echo with %v: %w", r.RemoteAddr, err) - } + log.Fatal(err) } } -// echo reads from the WebSocket connection and then writes -// the received message back to it. -// The entire function has 10s to complete. -func echo(ctx context.Context, c *websocket.Conn, l *rate.Limiter) error { - ctx, cancel := context.WithTimeout(ctx, time.Second*10) - defer cancel() - - err := l.Wait(ctx) - if err != nil { - return err +// run starts a http.Server for the passed in address +// with all requests handled by echoServer. +func run() error { + if len(os.Args) < 2 { + return errors.New("please provide an address to listen on as the first argument") } - typ, r, err := c.Reader(ctx) + l, err := net.Listen("tcp", os.Args[1]) if err != nil { return err } + log.Printf("listening on http://%v", l.Addr()) - w, err := c.Writer(ctx, typ) - if err != nil { - return err + s := &http.Server{ + Handler: echoServer{ + logf: log.Printf, + }, + ReadTimeout: time.Second * 10, + WriteTimeout: time.Second * 10, } + errc := make(chan error, 1) + go func() { + errc <- s.Serve(l) + }() - _, err = io.Copy(w, r) - if err != nil { - return fmt.Errorf("failed to io.Copy: %w", err) + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, os.Interrupt) + select { + case err := <-errc: + log.Printf("failed to serve: %v", err) + case sig := <-sigs: + log.Printf("terminating: %v", sig) } - err = w.Close() - return err -} - -// client dials the WebSocket echo server at the given url. -// It then sends it 5 different messages and echo's the server's -// response to each. -func client(url string) error { - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() - c, _, err := websocket.Dial(ctx, url, &websocket.DialOptions{ - Subprotocols: []string{"echo"}, - }) - if err != nil { - return err - } - defer c.Close(websocket.StatusInternalError, "the sky is falling") - - for i := 0; i < 5; i++ { - err = wsjson.Write(ctx, c, map[string]int{ - "i": i, - }) - if err != nil { - return err - } - - v := map[string]int{} - err = wsjson.Read(ctx, c, &v) - if err != nil { - return err - } - - fmt.Printf("received: %v\n", v) - } - - c.Close(websocket.StatusNormalClosure, "") - return nil + return s.Shutdown(ctx) } diff --git a/examples/echo/server.go b/examples/echo/server.go new file mode 100644 index 00000000..308c4a5e --- /dev/null +++ b/examples/echo/server.go @@ -0,0 +1,81 @@ +package main + +import ( + "context" + "fmt" + "io" + "net/http" + "time" + + "golang.org/x/time/rate" + + "nhooyr.io/websocket" +) + +// echoServer is the WebSocket echo server implementation. +// It ensures the client speaks the echo subprotocol and +// only allows one message every 100ms with a 10 message burst. +type echoServer struct { + + // logf controls where logs are sent. + logf func(f string, v ...interface{}) +} + +func (s echoServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + Subprotocols: []string{"echo"}, + }) + if err != nil { + s.logf("%v", err) + return + } + defer c.Close(websocket.StatusInternalError, "the sky is falling") + + if c.Subprotocol() != "echo" { + c.Close(websocket.StatusPolicyViolation, "client must speak the echo subprotocol") + return + } + + l := rate.NewLimiter(rate.Every(time.Millisecond*100), 10) + for { + err = echo(r.Context(), c, l) + if websocket.CloseStatus(err) == websocket.StatusNormalClosure { + return + } + if err != nil { + s.logf("failed to echo with %v: %v", r.RemoteAddr, err) + return + } + } +} + +// echo reads from the WebSocket connection and then writes +// the received message back to it. +// The entire function has 10s to complete. +func echo(ctx context.Context, c *websocket.Conn, l *rate.Limiter) error { + ctx, cancel := context.WithTimeout(ctx, time.Second*10) + defer cancel() + + err := l.Wait(ctx) + if err != nil { + return err + } + + typ, r, err := c.Reader(ctx) + if err != nil { + return err + } + + w, err := c.Writer(ctx, typ) + if err != nil { + return err + } + + _, err = io.Copy(w, r) + if err != nil { + return fmt.Errorf("failed to io.Copy: %w", err) + } + + err = w.Close() + return err +} diff --git a/examples/echo/server_test.go b/examples/echo/server_test.go new file mode 100644 index 00000000..9b608301 --- /dev/null +++ b/examples/echo/server_test.go @@ -0,0 +1,54 @@ +package main + +import ( + "context" + "net/http/httptest" + "testing" + "time" + + "nhooyr.io/websocket" + "nhooyr.io/websocket/wsjson" +) + +// Test_echoServer tests the echoServer by sending it 5 different messages +// and ensuring the responses all match. +func Test_echoServer(t *testing.T) { + t.Parallel() + + s := httptest.NewServer(echoServer{ + logf: t.Logf, + }) + defer s.Close() + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + c, _, err := websocket.Dial(ctx, s.URL, &websocket.DialOptions{ + Subprotocols: []string{"echo"}, + }) + if err != nil { + t.Fatal(err) + } + defer c.Close(websocket.StatusInternalError, "the sky is falling") + + for i := 0; i < 5; i++ { + err = wsjson.Write(ctx, c, map[string]int{ + "i": i, + }) + if err != nil { + t.Fatal(err) + } + + v := map[string]int{} + err = wsjson.Read(ctx, c, &v) + if err != nil { + t.Fatal(err) + } + + if v["i"] != i { + t.Fatalf("expected %v but got %v", i, v) + } + } + + c.Close(websocket.StatusNormalClosure, "") +} From f074be25a16fb07b08f48fefac19a294130caad8 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 14 Apr 2020 02:12:41 -0400 Subject: [PATCH 314/519] Undeprecate InsecureSkipVerify It's more clear than * as an origin pattern. --- accept.go | 6 ++++-- conn_test.go | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/accept.go b/accept.go index c43d9616..e4109c57 100644 --- a/accept.go +++ b/accept.go @@ -28,8 +28,7 @@ type AcceptOptions struct { // InsecureSkipVerify is used to disable Accept's origin verification behaviour. // - // Deprecated: Use OriginPatterns with a match all pattern of * instead to control - // origin authorization yourself. + // You probably want to use OriginPatterns instead. InsecureSkipVerify bool // OriginPatterns lists the host patterns for authorized origins. @@ -46,6 +45,9 @@ type AcceptOptions struct { // // Please ensure you understand the ramifications of enabling this. // If used incorrectly your WebSocket server will be open to CSRF attacks. + // + // Do not use * as a pattern to allow any origin, prefer to use InsecureSkipVerify instead + // to bring attention to the danger of such a setting. OriginPatterns []string // CompressionMode controls the compression mode. diff --git a/conn_test.go b/conn_test.go index 451d093a..6c52121a 100644 --- a/conn_test.go +++ b/conn_test.go @@ -273,8 +273,8 @@ func TestWasm(t *testing.T) { s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ - Subprotocols: []string{"echo"}, - OriginPatterns: []string{"*"}, + Subprotocols: []string{"echo"}, + InsecureSkipVerify: true, }) if err != nil { t.Errorf("echo server failed: %v", err) From 181f9432d723191e71c8334273084723e5074830 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 14 Apr 2020 16:53:40 -0400 Subject: [PATCH 315/519] Fix mention of compress library in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 14c39293..930c3c73 100644 --- a/README.md +++ b/README.md @@ -112,7 +112,7 @@ Advantages of nhooyr.io/websocket: - Gorilla's implementation is slower and uses [unsafe](https://golang.org/pkg/unsafe/). - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support - Gorilla only supports no context takeover mode - - 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)) + - We use [klauspost/compress](https://github.com/klauspost/compress) for much lower memory usage ([gorilla/websocket#203](https://github.com/gorilla/websocket/issues/203)) - [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492)) - Actively maintained ([gorilla/websocket#370](https://github.com/gorilla/websocket/issues/370)) From 5e8fc375fb70c508b0fff7795030c23d0d7bce07 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 14 Apr 2020 22:00:10 -0400 Subject: [PATCH 316/519] Document why publish endpoint and not sending messages over WebSocket See https://github.com/nhooyr/websocket/issues/174#issuecomment-613403182 --- examples/chat/README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/examples/chat/README.md b/examples/chat/README.md index a4c99a93..fc7df5d0 100644 --- a/examples/chat/README.md +++ b/examples/chat/README.md @@ -17,8 +17,10 @@ Visit the printed URL to submit and view broadcasted messages in a browser. The frontend is contained in `index.html`, `index.js` and `index.css`. It sets up the DOM with a scrollable div at the top that is populated with new messages as they are broadcast. At the bottom it adds a form to submit messages. + The messages are received via the WebSocket `/subscribe` endpoint and published via -the HTTP POST `/publish` endpoint. +the HTTP POST `/publish` endpoint. The reason for not publishing messages over the WebSocket +is so that you can easily publish a message with curl. The server portion is `main.go` and `chat.go` and implements serving the static frontend assets, the `/subscribe` WebSocket endpoint and the HTTP POST `/publish` endpoint. From 86e98b36a2a04323711d9d94a419c682cd715795 Mon Sep 17 00:00:00 2001 From: Mura Li Date: Tue, 5 May 2020 11:37:28 +0800 Subject: [PATCH 317/519] Less strict about message type of JSON payloads --- wsjson/wsjson.go | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/wsjson/wsjson.go b/wsjson/wsjson.go index 99996a69..2000a77a 100644 --- a/wsjson/wsjson.go +++ b/wsjson/wsjson.go @@ -20,16 +20,11 @@ func Read(ctx context.Context, c *websocket.Conn, v interface{}) error { func read(ctx context.Context, c *websocket.Conn, v interface{}) (err error) { defer errd.Wrap(&err, "failed to read JSON message") - typ, r, err := c.Reader(ctx) + _, r, err := c.Reader(ctx) if err != nil { return err } - if typ != websocket.MessageText { - c.Close(websocket.StatusUnsupportedData, "expected text message") - return fmt.Errorf("expected text message for JSON but got: %v", typ) - } - b := bpool.Get() defer bpool.Put(b) From 2a1e717bff943c60f775b34b0bf6f5154be11e7a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 10 May 2020 04:50:07 -0400 Subject: [PATCH 318/519] Mention OriginPatterns in Accept docs Closes #235 --- accept.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accept.go b/accept.go index e4109c57..8979614b 100644 --- a/accept.go +++ b/accept.go @@ -67,7 +67,7 @@ type AcceptOptions struct { // the connection to a WebSocket. // // Accept will not allow cross origin requests by default. -// See the InsecureSkipVerify option to allow cross origin requests. +// See the InsecureSkipVerify and OriginPatterns options to allow cross origin requests. // // Accept will write a response to w on all errors. func Accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, error) { From 4e5177fc250090cfc61494b1ec94884a30f51778 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 10 May 2020 05:03:40 -0400 Subject: [PATCH 319/519] Back to GH Actions See https://github.com/agnivade/wasmbrowsertest/issues/15 --- .github/workflows/ci.yaml | 39 ++++++++++++++++++++++++++++++++++++++ .travis.yml | 40 --------------------------------------- Makefile | 7 ------- README.md | 4 ++-- ci/container/Dockerfile | 14 ++++++++++++++ ci/ensure_fmt.sh | 23 ---------------------- ci/fmt.mk | 22 --------------------- ci/fmt.sh | 38 +++++++++++++++++++++++++++++++++++++ ci/lint.mk | 16 ---------------- ci/lint.sh | 16 ++++++++++++++++ ci/test.mk | 17 ----------------- ci/test.sh | 25 ++++++++++++++++++++++++ conn_test.go | 4 ---- 13 files changed, 134 insertions(+), 131 deletions(-) create mode 100644 .github/workflows/ci.yaml delete mode 100644 .travis.yml delete mode 100644 Makefile create mode 100644 ci/container/Dockerfile delete mode 100755 ci/ensure_fmt.sh delete mode 100644 ci/fmt.mk create mode 100755 ci/fmt.sh delete mode 100644 ci/lint.mk create mode 100755 ci/lint.sh delete mode 100644 ci/test.mk create mode 100755 ci/test.sh diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 00000000..3d9829ef --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,39 @@ +name: ci + +on: [push, pull_request] + +jobs: + fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Run ./ci/fmt.sh + uses: ./ci/container + with: + args: ./ci/fmt.sh + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Run ./ci/lint.sh + uses: ./ci/container + with: + args: ./ci/lint.sh + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v1 + - name: Run ./ci/test.sh + uses: ./ci/container + with: + args: ./ci/test.sh + env: + NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} + NETLIFY_SITE_ID: 9b3ee4dc-8297-4774-b4b9-a61561fbbce7 + - name: Upload coverage.html + uses: actions/upload-artifact@v2 + with: + name: coverage.html + path: ./ci/out/coverage.html diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 41d3c201..00000000 --- a/.travis.yml +++ /dev/null @@ -1,40 +0,0 @@ -language: go -go: 1.x -dist: bionic - -env: - global: - - SHFMT_URL=https://github.com/mvdan/sh/releases/download/v3.0.1/shfmt_v3.0.1_linux_amd64 - - GOFLAGS="-mod=readonly" - -jobs: - include: - - name: Format - before_script: - - sudo apt-get install -y npm - - sudo npm install -g prettier - - sudo curl -L "$SHFMT_URL" > /usr/local/bin/shfmt && sudo chmod +x /usr/local/bin/shfmt - - go get golang.org/x/tools/cmd/stringer - - go get golang.org/x/tools/cmd/goimports - script: make -j16 fmt - - name: Lint - before_script: - - sudo apt-get install -y shellcheck - - go get golang.org/x/lint/golint - script: make -j16 lint - - name: Test - before_script: - - sudo apt-get install -y chromium-browser - - go get github.com/agnivade/wasmbrowsertest - - go get github.com/mattn/goveralls - script: make -j16 test - -addons: - apt: - update: true - -cache: - npm: true - directories: - - ~/.cache - - ~/gopath/pkg diff --git a/Makefile b/Makefile deleted file mode 100644 index f9f31c49..00000000 --- a/Makefile +++ /dev/null @@ -1,7 +0,0 @@ -all: fmt lint test - -.SILENT: - -include ci/fmt.mk -include ci/lint.mk -include ci/test.mk diff --git a/README.md b/README.md index 14c39293..a88d26c7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # websocket [![godoc](https://godoc.org/nhooyr.io/websocket?status.svg)](https://pkg.go.dev/nhooyr.io/websocket) +[![coverage](https://img.shields.io/badge/coverage-88%25-success)](https://nhooyrio-websocket-coverage.netlify.app) websocket is a minimal and idiomatic WebSocket library for Go. @@ -10,12 +11,11 @@ websocket is a minimal and idiomatic WebSocket library for Go. go get nhooyr.io/websocket ``` -## Features +## Highlights - Minimal and idiomatic API - First class [context.Context](https://blog.golang.org/context) support - Fully passes the WebSocket [autobahn-testsuite](https://github.com/crossbario/autobahn-testsuite) -- Thorough tests with [90% coverage](https://coveralls.io/github/nhooyr/websocket) - [Single dependency](https://pkg.go.dev/nhooyr.io/websocket?tab=imports) - 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 - Zero alloc reads and writes diff --git a/ci/container/Dockerfile b/ci/container/Dockerfile new file mode 100644 index 00000000..fd008788 --- /dev/null +++ b/ci/container/Dockerfile @@ -0,0 +1,14 @@ +FROM golang + +RUN apt-get update +RUN apt-get install -y npm shellcheck chromium + +ENV GO111MODULE=on +RUN go get golang.org/x/tools/cmd/goimports +RUN go get mvdan.cc/sh/v3/cmd/shfmt +RUN go get golang.org/x/tools/cmd/stringer +RUN go get golang.org/x/lint/golint +RUN go get github.com/agnivade/wasmbrowsertest + +RUN npm install -g prettier +RUN npm install -g netlify-cli diff --git a/ci/ensure_fmt.sh b/ci/ensure_fmt.sh deleted file mode 100755 index 6fe9cb18..00000000 --- a/ci/ensure_fmt.sh +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/env bash - -set -euo pipefail - -main() { - local files - mapfile -t files < <(git ls-files --other --modified --exclude-standard) - if [[ ${files[*]} == "" ]]; then - return - fi - - echo "Files need generation or are formatted incorrectly:" - for f in "${files[@]}"; do - echo " $f" - done - - echo - echo "Please run the following locally:" - echo " make fmt" - exit 1 -} - -main "$@" diff --git a/ci/fmt.mk b/ci/fmt.mk deleted file mode 100644 index 1ed2920f..00000000 --- a/ci/fmt.mk +++ /dev/null @@ -1,22 +0,0 @@ -fmt: modtidy gofmt goimports prettier shfmt -ifdef CI - ./ci/ensure_fmt.sh -endif - -modtidy: gen - go mod tidy - -gofmt: gen - gofmt -w -s . - -goimports: gen - goimports -w "-local=$$(go list -m)" . - -prettier: - prettier --write --print-width=120 --no-semi --trailing-comma=all --loglevel=warn --arrow-parens=avoid $$(git ls-files "*.yml" "*.md" "*.js" "*.css" "*.html") - -gen: - stringer -type=opcode,MessageType,StatusCode -output=stringer.go - -shfmt: - shfmt -i 2 -w -s -sr $$(git ls-files "*.sh") diff --git a/ci/fmt.sh b/ci/fmt.sh new file mode 100755 index 00000000..e6a2d689 --- /dev/null +++ b/ci/fmt.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +set -euo pipefail + +main() { + cd "$(dirname "$0")/.." + + go mod tidy + gofmt -w -s . + goimports -w "-local=$(go list -m)" . + + prettier \ + --write \ + --print-width=120 \ + --no-semi \ + --trailing-comma=all \ + --loglevel=warn \ + --arrow-parens=avoid \ + $(git ls-files "*.yml" "*.md" "*.js" "*.css" "*.html") + shfmt -i 2 -w -s -sr $(git ls-files "*.sh") + + stringer -type=opcode,MessageType,StatusCode -output=stringer.go + + if [[ ${CI-} ]]; then + ensure_fmt + fi +} + +ensure_fmt() { + if [[ $(git ls-files --other --modified --exclude-standard) ]]; then + git -c color.ui=always --no-pager diff + echo + echo "Please run the following locally:" + echo " ./ci/fmt.sh" + exit 1 + fi +} + +main "$@" diff --git a/ci/lint.mk b/ci/lint.mk deleted file mode 100644 index 4335e7b1..00000000 --- a/ci/lint.mk +++ /dev/null @@ -1,16 +0,0 @@ -lint: govet golint govet-wasm golint-wasm shellcheck - -govet: - go vet ./... - -govet-wasm: - GOOS=js GOARCH=wasm go vet ./... - -golint: - golint -set_exit_status ./... - -golint-wasm: - GOOS=js GOARCH=wasm golint -set_exit_status ./... - -shellcheck: - shellcheck $$(git ls-files "*.sh") diff --git a/ci/lint.sh b/ci/lint.sh new file mode 100755 index 00000000..e1053d13 --- /dev/null +++ b/ci/lint.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +set -euo pipefail + +main() { + cd "$(dirname "$0")/.." + + go vet ./... + GOOS=js GOARCH=wasm go vet ./... + + golint -set_exit_status ./... + GOOS=js GOARCH=wasm golint -set_exit_status ./... + + shellcheck --exclude=SC2046 $(git ls-files "*.sh") +} + +main "$@" diff --git a/ci/test.mk b/ci/test.mk deleted file mode 100644 index 553a05c5..00000000 --- a/ci/test.mk +++ /dev/null @@ -1,17 +0,0 @@ -test: ci/out/coverage.html -ifdef CI -test: coveralls -endif - -ci/out/coverage.html: gotest - go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html - -coveralls: gotest - echo "--- coveralls" - goveralls -coverprofile=ci/out/coverage.prof - -gotest: - go test -timeout=30m -covermode=atomic -coverprofile=ci/out/coverage.prof -coverpkg=./... $${GOTESTFLAGS-} ./... - sed -i '/stringer\.go/d' ci/out/coverage.prof - sed -i '/nhooyr.io\/websocket\/internal\/test/d' ci/out/coverage.prof - sed -i '/examples/d' ci/out/coverage.prof diff --git a/ci/test.sh b/ci/test.sh new file mode 100755 index 00000000..64aa7728 --- /dev/null +++ b/ci/test.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +set -euo pipefail + +main() { + cd "$(dirname "$0")/.." + + go test -timeout=30m -covermode=atomic -coverprofile=ci/out/coverage.prof -coverpkg=./... "$@" ./... + sed -i '/stringer\.go/d' ci/out/coverage.prof + sed -i '/nhooyr.io\/websocket\/internal\/test/d' ci/out/coverage.prof + sed -i '/examples/d' ci/out/coverage.prof + + # Last line is the total coverage. + go tool cover -func ci/out/coverage.prof | tail -n1 + + go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html + + if [[ ${CI} && ${GITHUB_REF-} == *master ]]; then + local deployDir + deployDir="$(mktemp -d)" + cp ci/out/coverage.html "$deployDir/index.html" + netlify deploy --prod "--dir=$deployDir" + fi +} + +main "$@" diff --git a/conn_test.go b/conn_test.go index 28e8d59d..e1990a52 100644 --- a/conn_test.go +++ b/conn_test.go @@ -267,10 +267,6 @@ func TestConn(t *testing.T) { func TestWasm(t *testing.T) { t.Parallel() - if os.Getenv("CI") != "" { - t.Skip("skipping on CI") - } - s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ Subprotocols: []string{"echo"}, From 6a060b213f04097849a7e73d6095e96e05748471 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 10 May 2020 06:23:20 -0400 Subject: [PATCH 320/519] Fix typo in test.sh --- ci/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/test.sh b/ci/test.sh index 64aa7728..95ef7101 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -14,7 +14,7 @@ main() { go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html - if [[ ${CI} && ${GITHUB_REF-} == *master ]]; then + if [[ ${CI-} && ${GITHUB_REF-} == *master ]]; then local deployDir deployDir="$(mktemp -d)" cp ci/out/coverage.html "$deployDir/index.html" From bdac8884f43e864753dd21481810dc5259866d71 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 10 May 2020 06:26:40 -0400 Subject: [PATCH 321/519] Add ./ci/all.sh --- ci/all.sh | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100755 ci/all.sh diff --git a/ci/all.sh b/ci/all.sh new file mode 100755 index 00000000..efd56b61 --- /dev/null +++ b/ci/all.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash +set -euo pipefail + +main() { + cd "$(dirname "$0")/.." + + ./ci/fmt.sh + ./ci/lint.sh + ./ci/test.sh +} + +main "$@" From bf79bdd8c657bfde6615cffa69dfa5cf12c4473e Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 10 May 2020 04:46:31 -0400 Subject: [PATCH 322/519] Patch Accept for Gin Closes #166 --- accept.go | 6 ++++++ conn_test.go | 60 ++++++++++++++++++++++++++++++++++++++++------------ go.mod | 1 + go.sum | 46 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 13 deletions(-) diff --git a/accept.go b/accept.go index 8979614b..66379b5d 100644 --- a/accept.go +++ b/accept.go @@ -124,6 +124,12 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con } w.WriteHeader(http.StatusSwitchingProtocols) + // See https://github.com/nhooyr/websocket/issues/166 + if ginWriter, ok := w.(interface { + WriteHeaderNow() + }); ok { + ginWriter.WriteHeaderNow() + } netConn, brw, err := hj.Hijack() if err != nil { diff --git a/conn_test.go b/conn_test.go index 0da7e1d8..c2c41292 100644 --- a/conn_test.go +++ b/conn_test.go @@ -16,10 +16,12 @@ import ( "testing" "time" + "github.com/gin-gonic/gin" "github.com/golang/protobuf/ptypes" "github.com/golang/protobuf/ptypes/duration" "nhooyr.io/websocket" + "nhooyr.io/websocket/internal/errd" "nhooyr.io/websocket/internal/test/assert" "nhooyr.io/websocket/internal/test/wstest" "nhooyr.io/websocket/internal/test/xrand" @@ -268,22 +270,12 @@ func TestWasm(t *testing.T) { t.Parallel() s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - c, err := websocket.Accept(w, r, &websocket.AcceptOptions{ + err := echoServer(w, r, &websocket.AcceptOptions{ Subprotocols: []string{"echo"}, InsecureSkipVerify: true, }) if err != nil { - t.Errorf("echo server failed: %v", err) - return - } - defer c.Close(websocket.StatusInternalError, "") - - err = wstest.EchoLoop(r.Context(), c) - - err = assertCloseStatus(websocket.StatusNormalClosure, err) - if err != nil { - t.Errorf("echo server failed: %v", err) - return + t.Error(err) } })) defer s.Close() @@ -489,7 +481,49 @@ func BenchmarkConn(b *testing.B) { } } -func TestCompression(t *testing.T) { +func echoServer(w http.ResponseWriter, r *http.Request, opts *websocket.AcceptOptions) (err error) { + defer errd.Wrap(&err, "echo server failed") + + c, err := websocket.Accept(w, r, opts) + if err != nil { + return err + } + defer c.Close(websocket.StatusInternalError, "") + + err = wstest.EchoLoop(r.Context(), c) + return assertCloseStatus(websocket.StatusNormalClosure, err) +} + +func TestGin(t *testing.T) { t.Parallel() + gin.SetMode(gin.ReleaseMode) + r := gin.New() + r.GET("/", func(ginCtx *gin.Context) { + err := echoServer(ginCtx.Writer, ginCtx.Request, nil) + if err != nil { + t.Error(err) + } + }) + + s := httptest.NewServer(r) + defer s.Close() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + + c, _, err := websocket.Dial(ctx, s.URL, nil) + assert.Success(t, err) + defer c.Close(websocket.StatusInternalError, "") + + err = wsjson.Write(ctx, c, "hello") + assert.Success(t, err) + + var v interface{} + err = wsjson.Read(ctx, c, &v) + assert.Success(t, err) + assert.Equal(t, "read msg", "hello", v) + + err = c.Close(websocket.StatusNormalClosure, "") + assert.Success(t, err) } diff --git a/go.mod b/go.mod index 60377823..c5f1a20f 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module nhooyr.io/websocket go 1.13 require ( + github.com/gin-gonic/gin v1.6.3 github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee // indirect github.com/gobwas/pool v0.2.0 // indirect github.com/gobwas/ws v1.0.2 diff --git a/go.sum b/go.sum index 736df430..155c3013 100644 --- a/go.sum +++ b/go.sum @@ -1,18 +1,64 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= From 03cca81f294e0386e8364ed68c397c3f7163ced4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 10 May 2020 08:41:38 -0400 Subject: [PATCH 323/519] Fix deadlock introduced in v1.8.5 Closes #231 --- ci/all.sh | 2 +- write.go | 23 +++++++++++++++++------ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/ci/all.sh b/ci/all.sh index efd56b61..1ee7640f 100755 --- a/ci/all.sh +++ b/ci/all.sh @@ -6,7 +6,7 @@ main() { ./ci/fmt.sh ./ci/lint.sh - ./ci/test.sh + ./ci/test.sh "$@" } main "$@" diff --git a/write.go b/write.go index 60a4fba0..2210cf81 100644 --- a/write.go +++ b/write.go @@ -246,13 +246,24 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco if err != nil { return 0, err } - defer func() { - // We leave it locked when writing the close frame to avoid - // any other goroutine writing any other frame. - if opcode != opClose { - c.writeFrameMu.unlock() + defer c.writeFrameMu.unlock() + + // If the state says a close has already been written, we wait until + // the connection is closed and return that error. + // + // However, if the frame being written is a close, that means its the close from + // the state being set so we let it go through. + c.closeMu.Lock() + wroteClose := c.wroteClose + c.closeMu.Unlock() + if wroteClose && opcode != opClose { + select { + case <-ctx.Done(): + return 0, ctx.Err() + case <-c.closed: + return 0, c.closeErr } - }() + } select { case <-c.closed: From b453d3ee377e513da7fdf5f9241e0be088489717 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 18 May 2020 02:47:53 -0400 Subject: [PATCH 324/519] Move all Wasm related code into ws_js.go This way we don't pollute the directory tree. --- accept_js.go | 20 ---- close.go | 205 +++++++++++++++++++++++++++++++++++ close_notjs.go | 211 ------------------------------------ compress.go | 180 +++++++++++++++++++++++++++++++ compress_notjs.go | 181 ------------------------------- conn.go | 264 +++++++++++++++++++++++++++++++++++++++++++++ conn_notjs.go | 265 ---------------------------------------------- ws_js.go | 134 +++++++++++++++++++++++ 8 files changed, 783 insertions(+), 677 deletions(-) delete mode 100644 accept_js.go delete mode 100644 close_notjs.go delete mode 100644 compress_notjs.go delete mode 100644 conn_notjs.go diff --git a/accept_js.go b/accept_js.go deleted file mode 100644 index daad4b79..00000000 --- a/accept_js.go +++ /dev/null @@ -1,20 +0,0 @@ -package websocket - -import ( - "errors" - "net/http" -) - -// AcceptOptions represents Accept's options. -type AcceptOptions struct { - Subprotocols []string - InsecureSkipVerify bool - OriginPatterns []string - CompressionMode CompressionMode - CompressionThreshold int -} - -// Accept is stubbed out for Wasm. -func Accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, error) { - return nil, errors.New("unimplemented") -} diff --git a/close.go b/close.go index 7cbc19e9..d76dc2f4 100644 --- a/close.go +++ b/close.go @@ -1,8 +1,16 @@ +// +build !js + package websocket import ( + "context" + "encoding/binary" "errors" "fmt" + "log" + "time" + + "nhooyr.io/websocket/internal/errd" ) // StatusCode represents a WebSocket status code. @@ -74,3 +82,200 @@ func CloseStatus(err error) StatusCode { } return -1 } + +// Close performs the WebSocket close handshake with the given status code and reason. +// +// It will write a WebSocket close frame with a timeout of 5s and then wait 5s for +// the peer to send a close frame. +// All data messages received from the peer during the close handshake will be discarded. +// +// The connection can only be closed once. Additional calls to Close +// are no-ops. +// +// The maximum length of reason must be 125 bytes. Avoid +// sending a dynamic reason. +// +// Close will unblock all goroutines interacting with the connection once +// complete. +func (c *Conn) Close(code StatusCode, reason string) error { + return c.closeHandshake(code, reason) +} + +func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { + defer errd.Wrap(&err, "failed to close WebSocket") + + writeErr := c.writeClose(code, reason) + closeHandshakeErr := c.waitCloseHandshake() + + if writeErr != nil { + return writeErr + } + + if CloseStatus(closeHandshakeErr) == -1 { + return closeHandshakeErr + } + + return nil +} + +var errAlreadyWroteClose = errors.New("already wrote close") + +func (c *Conn) writeClose(code StatusCode, reason string) error { + c.closeMu.Lock() + wroteClose := c.wroteClose + c.wroteClose = true + c.closeMu.Unlock() + if wroteClose { + return errAlreadyWroteClose + } + + ce := CloseError{ + Code: code, + Reason: reason, + } + + var p []byte + var marshalErr error + if ce.Code != StatusNoStatusRcvd { + p, marshalErr = ce.bytes() + if marshalErr != nil { + log.Printf("websocket: %v", marshalErr) + } + } + + writeErr := c.writeControl(context.Background(), opClose, p) + if CloseStatus(writeErr) != -1 { + // Not a real error if it's due to a close frame being received. + writeErr = nil + } + + // We do this after in case there was an error writing the close frame. + c.setCloseErr(fmt.Errorf("sent close frame: %w", ce)) + + if marshalErr != nil { + return marshalErr + } + return writeErr +} + +func (c *Conn) waitCloseHandshake() error { + defer c.close(nil) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + err := c.readMu.lock(ctx) + if err != nil { + return err + } + defer c.readMu.unlock() + + if c.readCloseFrameErr != nil { + return c.readCloseFrameErr + } + + for { + h, err := c.readLoop(ctx) + if err != nil { + return err + } + + for i := int64(0); i < h.payloadLength; i++ { + _, err := c.br.ReadByte() + if err != nil { + return err + } + } + } +} + +func parseClosePayload(p []byte) (CloseError, error) { + if len(p) == 0 { + return CloseError{ + Code: StatusNoStatusRcvd, + }, nil + } + + if len(p) < 2 { + return CloseError{}, fmt.Errorf("close payload %q too small, cannot even contain the 2 byte status code", p) + } + + ce := CloseError{ + Code: StatusCode(binary.BigEndian.Uint16(p)), + Reason: string(p[2:]), + } + + if !validWireCloseCode(ce.Code) { + return CloseError{}, fmt.Errorf("invalid status code %v", ce.Code) + } + + return ce, nil +} + +// See http://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number +// and https://tools.ietf.org/html/rfc6455#section-7.4.1 +func validWireCloseCode(code StatusCode) bool { + switch code { + case statusReserved, StatusNoStatusRcvd, StatusAbnormalClosure, StatusTLSHandshake: + return false + } + + if code >= StatusNormalClosure && code <= StatusBadGateway { + return true + } + if code >= 3000 && code <= 4999 { + return true + } + + return false +} + +func (ce CloseError) bytes() ([]byte, error) { + p, err := ce.bytesErr() + if err != nil { + err = fmt.Errorf("failed to marshal close frame: %w", err) + ce = CloseError{ + Code: StatusInternalError, + } + p, _ = ce.bytesErr() + } + return p, err +} + +const maxCloseReason = maxControlPayload - 2 + +func (ce CloseError) bytesErr() ([]byte, error) { + if len(ce.Reason) > maxCloseReason { + return nil, fmt.Errorf("reason string max is %v but got %q with length %v", maxCloseReason, ce.Reason, len(ce.Reason)) + } + + if !validWireCloseCode(ce.Code) { + return nil, fmt.Errorf("status code %v cannot be set", ce.Code) + } + + buf := make([]byte, 2+len(ce.Reason)) + binary.BigEndian.PutUint16(buf, uint16(ce.Code)) + copy(buf[2:], ce.Reason) + return buf, nil +} + +func (c *Conn) setCloseErr(err error) { + c.closeMu.Lock() + c.setCloseErrLocked(err) + c.closeMu.Unlock() +} + +func (c *Conn) setCloseErrLocked(err error) { + if c.closeErr == nil { + c.closeErr = fmt.Errorf("WebSocket closed: %w", err) + } +} + +func (c *Conn) isClosed() bool { + select { + case <-c.closed: + return true + default: + return false + } +} diff --git a/close_notjs.go b/close_notjs.go deleted file mode 100644 index 4251311d..00000000 --- a/close_notjs.go +++ /dev/null @@ -1,211 +0,0 @@ -// +build !js - -package websocket - -import ( - "context" - "encoding/binary" - "errors" - "fmt" - "log" - "time" - - "nhooyr.io/websocket/internal/errd" -) - -// Close performs the WebSocket close handshake with the given status code and reason. -// -// It will write a WebSocket close frame with a timeout of 5s and then wait 5s for -// the peer to send a close frame. -// All data messages received from the peer during the close handshake will be discarded. -// -// The connection can only be closed once. Additional calls to Close -// are no-ops. -// -// The maximum length of reason must be 125 bytes. Avoid -// sending a dynamic reason. -// -// Close will unblock all goroutines interacting with the connection once -// complete. -func (c *Conn) Close(code StatusCode, reason string) error { - return c.closeHandshake(code, reason) -} - -func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { - defer errd.Wrap(&err, "failed to close WebSocket") - - writeErr := c.writeClose(code, reason) - closeHandshakeErr := c.waitCloseHandshake() - - if writeErr != nil { - return writeErr - } - - if CloseStatus(closeHandshakeErr) == -1 { - return closeHandshakeErr - } - - return nil -} - -var errAlreadyWroteClose = errors.New("already wrote close") - -func (c *Conn) writeClose(code StatusCode, reason string) error { - c.closeMu.Lock() - wroteClose := c.wroteClose - c.wroteClose = true - c.closeMu.Unlock() - if wroteClose { - return errAlreadyWroteClose - } - - ce := CloseError{ - Code: code, - Reason: reason, - } - - var p []byte - var marshalErr error - if ce.Code != StatusNoStatusRcvd { - p, marshalErr = ce.bytes() - if marshalErr != nil { - log.Printf("websocket: %v", marshalErr) - } - } - - writeErr := c.writeControl(context.Background(), opClose, p) - if CloseStatus(writeErr) != -1 { - // Not a real error if it's due to a close frame being received. - writeErr = nil - } - - // We do this after in case there was an error writing the close frame. - c.setCloseErr(fmt.Errorf("sent close frame: %w", ce)) - - if marshalErr != nil { - return marshalErr - } - return writeErr -} - -func (c *Conn) waitCloseHandshake() error { - defer c.close(nil) - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - defer cancel() - - err := c.readMu.lock(ctx) - if err != nil { - return err - } - defer c.readMu.unlock() - - if c.readCloseFrameErr != nil { - return c.readCloseFrameErr - } - - for { - h, err := c.readLoop(ctx) - if err != nil { - return err - } - - for i := int64(0); i < h.payloadLength; i++ { - _, err := c.br.ReadByte() - if err != nil { - return err - } - } - } -} - -func parseClosePayload(p []byte) (CloseError, error) { - if len(p) == 0 { - return CloseError{ - Code: StatusNoStatusRcvd, - }, nil - } - - if len(p) < 2 { - return CloseError{}, fmt.Errorf("close payload %q too small, cannot even contain the 2 byte status code", p) - } - - ce := CloseError{ - Code: StatusCode(binary.BigEndian.Uint16(p)), - Reason: string(p[2:]), - } - - if !validWireCloseCode(ce.Code) { - return CloseError{}, fmt.Errorf("invalid status code %v", ce.Code) - } - - return ce, nil -} - -// See http://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number -// and https://tools.ietf.org/html/rfc6455#section-7.4.1 -func validWireCloseCode(code StatusCode) bool { - switch code { - case statusReserved, StatusNoStatusRcvd, StatusAbnormalClosure, StatusTLSHandshake: - return false - } - - if code >= StatusNormalClosure && code <= StatusBadGateway { - return true - } - if code >= 3000 && code <= 4999 { - return true - } - - return false -} - -func (ce CloseError) bytes() ([]byte, error) { - p, err := ce.bytesErr() - if err != nil { - err = fmt.Errorf("failed to marshal close frame: %w", err) - ce = CloseError{ - Code: StatusInternalError, - } - p, _ = ce.bytesErr() - } - return p, err -} - -const maxCloseReason = maxControlPayload - 2 - -func (ce CloseError) bytesErr() ([]byte, error) { - if len(ce.Reason) > maxCloseReason { - return nil, fmt.Errorf("reason string max is %v but got %q with length %v", maxCloseReason, ce.Reason, len(ce.Reason)) - } - - if !validWireCloseCode(ce.Code) { - return nil, fmt.Errorf("status code %v cannot be set", ce.Code) - } - - buf := make([]byte, 2+len(ce.Reason)) - binary.BigEndian.PutUint16(buf, uint16(ce.Code)) - copy(buf[2:], ce.Reason) - return buf, nil -} - -func (c *Conn) setCloseErr(err error) { - c.closeMu.Lock() - c.setCloseErrLocked(err) - c.closeMu.Unlock() -} - -func (c *Conn) setCloseErrLocked(err error) { - if c.closeErr == nil { - c.closeErr = fmt.Errorf("WebSocket closed: %w", err) - } -} - -func (c *Conn) isClosed() bool { - select { - case <-c.closed: - return true - default: - return false - } -} diff --git a/compress.go b/compress.go index 80b46d1c..63d961b4 100644 --- a/compress.go +++ b/compress.go @@ -1,5 +1,15 @@ +// +build !js + package websocket +import ( + "io" + "net/http" + "sync" + + "github.com/klauspost/compress/flate" +) + // CompressionMode represents the modes available to the deflate extension. // See https://tools.ietf.org/html/rfc7692 // @@ -37,3 +47,173 @@ const ( // important than bandwidth. CompressionDisabled ) + +func (m CompressionMode) opts() *compressionOptions { + return &compressionOptions{ + clientNoContextTakeover: m == CompressionNoContextTakeover, + serverNoContextTakeover: m == CompressionNoContextTakeover, + } +} + +type compressionOptions struct { + clientNoContextTakeover bool + serverNoContextTakeover bool +} + +func (copts *compressionOptions) setHeader(h http.Header) { + s := "permessage-deflate" + if copts.clientNoContextTakeover { + s += "; client_no_context_takeover" + } + if copts.serverNoContextTakeover { + s += "; server_no_context_takeover" + } + h.Set("Sec-WebSocket-Extensions", s) +} + +// These bytes are required to get flate.Reader to return. +// They are removed when sending to avoid the overhead as +// WebSocket framing tell's when the message has ended but then +// we need to add them back otherwise flate.Reader keeps +// trying to return more bytes. +const deflateMessageTail = "\x00\x00\xff\xff" + +type trimLastFourBytesWriter struct { + w io.Writer + tail []byte +} + +func (tw *trimLastFourBytesWriter) reset() { + if tw != nil && tw.tail != nil { + tw.tail = tw.tail[:0] + } +} + +func (tw *trimLastFourBytesWriter) Write(p []byte) (int, error) { + if tw.tail == nil { + tw.tail = make([]byte, 0, 4) + } + + extra := len(tw.tail) + len(p) - 4 + + if extra <= 0 { + tw.tail = append(tw.tail, p...) + return len(p), nil + } + + // Now we need to write as many extra bytes as we can from the previous tail. + if extra > len(tw.tail) { + extra = len(tw.tail) + } + if extra > 0 { + _, err := tw.w.Write(tw.tail[:extra]) + if err != nil { + return 0, err + } + + // Shift remaining bytes in tail over. + n := copy(tw.tail, tw.tail[extra:]) + tw.tail = tw.tail[:n] + } + + // If p is less than or equal to 4 bytes, + // all of it is is part of the tail. + if len(p) <= 4 { + tw.tail = append(tw.tail, p...) + return len(p), nil + } + + // Otherwise, only the last 4 bytes are. + tw.tail = append(tw.tail, p[len(p)-4:]...) + + p = p[:len(p)-4] + n, err := tw.w.Write(p) + return n + 4, err +} + +var flateReaderPool sync.Pool + +func getFlateReader(r io.Reader, dict []byte) io.Reader { + fr, ok := flateReaderPool.Get().(io.Reader) + if !ok { + return flate.NewReaderDict(r, dict) + } + fr.(flate.Resetter).Reset(r, dict) + return fr +} + +func putFlateReader(fr io.Reader) { + flateReaderPool.Put(fr) +} + +type slidingWindow struct { + buf []byte +} + +var swPoolMu sync.RWMutex +var swPool = map[int]*sync.Pool{} + +func slidingWindowPool(n int) *sync.Pool { + swPoolMu.RLock() + p, ok := swPool[n] + swPoolMu.RUnlock() + if ok { + return p + } + + p = &sync.Pool{} + + swPoolMu.Lock() + swPool[n] = p + swPoolMu.Unlock() + + return p +} + +func (sw *slidingWindow) init(n int) { + if sw.buf != nil { + return + } + + if n == 0 { + n = 32768 + } + + p := slidingWindowPool(n) + buf, ok := p.Get().([]byte) + if ok { + sw.buf = buf[:0] + } else { + sw.buf = make([]byte, 0, n) + } +} + +func (sw *slidingWindow) close() { + if sw.buf == nil { + return + } + + swPoolMu.Lock() + swPool[cap(sw.buf)].Put(sw.buf) + swPoolMu.Unlock() + sw.buf = nil +} + +func (sw *slidingWindow) write(p []byte) { + if len(p) >= cap(sw.buf) { + sw.buf = sw.buf[:cap(sw.buf)] + p = p[len(p)-cap(sw.buf):] + copy(sw.buf, p) + return + } + + left := cap(sw.buf) - len(sw.buf) + if left < len(p) { + // We need to shift spaceNeeded bytes from the end to make room for p at the end. + spaceNeeded := len(p) - left + copy(sw.buf, sw.buf[spaceNeeded:]) + sw.buf = sw.buf[:len(sw.buf)-spaceNeeded] + } + + sw.buf = append(sw.buf, p...) +} diff --git a/compress_notjs.go b/compress_notjs.go deleted file mode 100644 index 809a272c..00000000 --- a/compress_notjs.go +++ /dev/null @@ -1,181 +0,0 @@ -// +build !js - -package websocket - -import ( - "io" - "net/http" - "sync" - - "github.com/klauspost/compress/flate" -) - -func (m CompressionMode) opts() *compressionOptions { - return &compressionOptions{ - clientNoContextTakeover: m == CompressionNoContextTakeover, - serverNoContextTakeover: m == CompressionNoContextTakeover, - } -} - -type compressionOptions struct { - clientNoContextTakeover bool - serverNoContextTakeover bool -} - -func (copts *compressionOptions) setHeader(h http.Header) { - s := "permessage-deflate" - if copts.clientNoContextTakeover { - s += "; client_no_context_takeover" - } - if copts.serverNoContextTakeover { - s += "; server_no_context_takeover" - } - h.Set("Sec-WebSocket-Extensions", s) -} - -// These bytes are required to get flate.Reader to return. -// They are removed when sending to avoid the overhead as -// WebSocket framing tell's when the message has ended but then -// we need to add them back otherwise flate.Reader keeps -// trying to return more bytes. -const deflateMessageTail = "\x00\x00\xff\xff" - -type trimLastFourBytesWriter struct { - w io.Writer - tail []byte -} - -func (tw *trimLastFourBytesWriter) reset() { - if tw != nil && tw.tail != nil { - tw.tail = tw.tail[:0] - } -} - -func (tw *trimLastFourBytesWriter) Write(p []byte) (int, error) { - if tw.tail == nil { - tw.tail = make([]byte, 0, 4) - } - - extra := len(tw.tail) + len(p) - 4 - - if extra <= 0 { - tw.tail = append(tw.tail, p...) - return len(p), nil - } - - // Now we need to write as many extra bytes as we can from the previous tail. - if extra > len(tw.tail) { - extra = len(tw.tail) - } - if extra > 0 { - _, err := tw.w.Write(tw.tail[:extra]) - if err != nil { - return 0, err - } - - // Shift remaining bytes in tail over. - n := copy(tw.tail, tw.tail[extra:]) - tw.tail = tw.tail[:n] - } - - // If p is less than or equal to 4 bytes, - // all of it is is part of the tail. - if len(p) <= 4 { - tw.tail = append(tw.tail, p...) - return len(p), nil - } - - // Otherwise, only the last 4 bytes are. - tw.tail = append(tw.tail, p[len(p)-4:]...) - - p = p[:len(p)-4] - n, err := tw.w.Write(p) - return n + 4, err -} - -var flateReaderPool sync.Pool - -func getFlateReader(r io.Reader, dict []byte) io.Reader { - fr, ok := flateReaderPool.Get().(io.Reader) - if !ok { - return flate.NewReaderDict(r, dict) - } - fr.(flate.Resetter).Reset(r, dict) - return fr -} - -func putFlateReader(fr io.Reader) { - flateReaderPool.Put(fr) -} - -type slidingWindow struct { - buf []byte -} - -var swPoolMu sync.RWMutex -var swPool = map[int]*sync.Pool{} - -func slidingWindowPool(n int) *sync.Pool { - swPoolMu.RLock() - p, ok := swPool[n] - swPoolMu.RUnlock() - if ok { - return p - } - - p = &sync.Pool{} - - swPoolMu.Lock() - swPool[n] = p - swPoolMu.Unlock() - - return p -} - -func (sw *slidingWindow) init(n int) { - if sw.buf != nil { - return - } - - if n == 0 { - n = 32768 - } - - p := slidingWindowPool(n) - buf, ok := p.Get().([]byte) - if ok { - sw.buf = buf[:0] - } else { - sw.buf = make([]byte, 0, n) - } -} - -func (sw *slidingWindow) close() { - if sw.buf == nil { - return - } - - swPoolMu.Lock() - swPool[cap(sw.buf)].Put(sw.buf) - swPoolMu.Unlock() - sw.buf = nil -} - -func (sw *slidingWindow) write(p []byte) { - if len(p) >= cap(sw.buf) { - sw.buf = sw.buf[:cap(sw.buf)] - p = p[len(p)-cap(sw.buf):] - copy(sw.buf, p) - return - } - - left := cap(sw.buf) - len(sw.buf) - if left < len(p) { - // We need to shift spaceNeeded bytes from the end to make room for p at the end. - spaceNeeded := len(p) - left - copy(sw.buf, sw.buf[spaceNeeded:]) - sw.buf = sw.buf[:len(sw.buf)-spaceNeeded] - } - - sw.buf = append(sw.buf, p...) -} diff --git a/conn.go b/conn.go index a41808be..e208d116 100644 --- a/conn.go +++ b/conn.go @@ -1,5 +1,19 @@ +// +build !js + package websocket +import ( + "bufio" + "context" + "errors" + "fmt" + "io" + "runtime" + "strconv" + "sync" + "sync/atomic" +) + // MessageType represents the type of a WebSocket message. // See https://tools.ietf.org/html/rfc6455#section-5.6 type MessageType int @@ -11,3 +25,253 @@ const ( // MessageBinary is for binary messages like protobufs. MessageBinary ) + +// Conn represents a WebSocket connection. +// All methods may be called concurrently except for Reader and Read. +// +// You must always read from the connection. Otherwise control +// frames will not be handled. See Reader and CloseRead. +// +// Be sure to call Close on the connection when you +// are finished with it to release associated resources. +// +// On any error from any method, the connection is closed +// with an appropriate reason. +type Conn struct { + subprotocol string + rwc io.ReadWriteCloser + client bool + copts *compressionOptions + flateThreshold int + br *bufio.Reader + bw *bufio.Writer + + readTimeout chan context.Context + writeTimeout chan context.Context + + // Read state. + readMu *mu + readHeaderBuf [8]byte + readControlBuf [maxControlPayload]byte + msgReader *msgReader + readCloseFrameErr error + + // Write state. + msgWriterState *msgWriterState + writeFrameMu *mu + writeBuf []byte + writeHeaderBuf [8]byte + writeHeader header + + closed chan struct{} + closeMu sync.Mutex + closeErr error + wroteClose bool + + pingCounter int32 + activePingsMu sync.Mutex + activePings map[string]chan<- struct{} +} + +type connConfig struct { + subprotocol string + rwc io.ReadWriteCloser + client bool + copts *compressionOptions + flateThreshold int + + br *bufio.Reader + bw *bufio.Writer +} + +func newConn(cfg connConfig) *Conn { + c := &Conn{ + subprotocol: cfg.subprotocol, + rwc: cfg.rwc, + client: cfg.client, + copts: cfg.copts, + flateThreshold: cfg.flateThreshold, + + br: cfg.br, + bw: cfg.bw, + + readTimeout: make(chan context.Context), + writeTimeout: make(chan context.Context), + + closed: make(chan struct{}), + activePings: make(map[string]chan<- struct{}), + } + + c.readMu = newMu(c) + c.writeFrameMu = newMu(c) + + c.msgReader = newMsgReader(c) + + c.msgWriterState = newMsgWriterState(c) + if c.client { + c.writeBuf = extractBufioWriterBuf(c.bw, c.rwc) + } + + if c.flate() && c.flateThreshold == 0 { + c.flateThreshold = 128 + if !c.msgWriterState.flateContextTakeover() { + c.flateThreshold = 512 + } + } + + runtime.SetFinalizer(c, func(c *Conn) { + c.close(errors.New("connection garbage collected")) + }) + + go c.timeoutLoop() + + return c +} + +// Subprotocol returns the negotiated subprotocol. +// An empty string means the default protocol. +func (c *Conn) Subprotocol() string { + return c.subprotocol +} + +func (c *Conn) close(err error) { + c.closeMu.Lock() + defer c.closeMu.Unlock() + + if c.isClosed() { + return + } + c.setCloseErrLocked(err) + close(c.closed) + runtime.SetFinalizer(c, nil) + + // Have to close after c.closed is closed to ensure any goroutine that wakes up + // from the connection being closed also sees that c.closed is closed and returns + // closeErr. + c.rwc.Close() + + go func() { + c.msgWriterState.close() + + c.msgReader.close() + }() +} + +func (c *Conn) timeoutLoop() { + readCtx := context.Background() + writeCtx := context.Background() + + for { + select { + case <-c.closed: + return + + case writeCtx = <-c.writeTimeout: + case readCtx = <-c.readTimeout: + + case <-readCtx.Done(): + c.setCloseErr(fmt.Errorf("read timed out: %w", readCtx.Err())) + go c.writeError(StatusPolicyViolation, errors.New("timed out")) + case <-writeCtx.Done(): + c.close(fmt.Errorf("write timed out: %w", writeCtx.Err())) + return + } + } +} + +func (c *Conn) flate() bool { + return c.copts != nil +} + +// Ping sends a ping to the peer and waits for a pong. +// Use this to measure latency or ensure the peer is responsive. +// Ping must be called concurrently with Reader as it does +// not read from the connection but instead waits for a Reader call +// to read the pong. +// +// TCP Keepalives should suffice for most use cases. +func (c *Conn) Ping(ctx context.Context) error { + p := atomic.AddInt32(&c.pingCounter, 1) + + err := c.ping(ctx, strconv.Itoa(int(p))) + if err != nil { + return fmt.Errorf("failed to ping: %w", err) + } + return nil +} + +func (c *Conn) ping(ctx context.Context, p string) error { + pong := make(chan struct{}) + + c.activePingsMu.Lock() + c.activePings[p] = pong + c.activePingsMu.Unlock() + + defer func() { + c.activePingsMu.Lock() + delete(c.activePings, p) + c.activePingsMu.Unlock() + }() + + err := c.writeControl(ctx, opPing, []byte(p)) + if err != nil { + return err + } + + select { + case <-c.closed: + return c.closeErr + case <-ctx.Done(): + err := fmt.Errorf("failed to wait for pong: %w", ctx.Err()) + c.close(err) + return err + case <-pong: + return nil + } +} + +type mu struct { + c *Conn + ch chan struct{} +} + +func newMu(c *Conn) *mu { + return &mu{ + c: c, + ch: make(chan struct{}, 1), + } +} + +func (m *mu) forceLock() { + m.ch <- struct{}{} +} + +func (m *mu) lock(ctx context.Context) error { + select { + case <-m.c.closed: + return m.c.closeErr + case <-ctx.Done(): + err := fmt.Errorf("failed to acquire lock: %w", ctx.Err()) + m.c.close(err) + return err + case m.ch <- struct{}{}: + // To make sure the connection is certainly alive. + // As it's possible the send on m.ch was selected + // over the receive on closed. + select { + case <-m.c.closed: + // Make sure to release. + m.unlock() + return m.c.closeErr + default: + } + return nil + } +} + +func (m *mu) unlock() { + select { + case <-m.ch: + default: + } +} diff --git a/conn_notjs.go b/conn_notjs.go deleted file mode 100644 index bb2eb22f..00000000 --- a/conn_notjs.go +++ /dev/null @@ -1,265 +0,0 @@ -// +build !js - -package websocket - -import ( - "bufio" - "context" - "errors" - "fmt" - "io" - "runtime" - "strconv" - "sync" - "sync/atomic" -) - -// Conn represents a WebSocket connection. -// All methods may be called concurrently except for Reader and Read. -// -// You must always read from the connection. Otherwise control -// frames will not be handled. See Reader and CloseRead. -// -// Be sure to call Close on the connection when you -// are finished with it to release associated resources. -// -// On any error from any method, the connection is closed -// with an appropriate reason. -type Conn struct { - subprotocol string - rwc io.ReadWriteCloser - client bool - copts *compressionOptions - flateThreshold int - br *bufio.Reader - bw *bufio.Writer - - readTimeout chan context.Context - writeTimeout chan context.Context - - // Read state. - readMu *mu - readHeaderBuf [8]byte - readControlBuf [maxControlPayload]byte - msgReader *msgReader - readCloseFrameErr error - - // Write state. - msgWriterState *msgWriterState - writeFrameMu *mu - writeBuf []byte - writeHeaderBuf [8]byte - writeHeader header - - closed chan struct{} - closeMu sync.Mutex - closeErr error - wroteClose bool - - pingCounter int32 - activePingsMu sync.Mutex - activePings map[string]chan<- struct{} -} - -type connConfig struct { - subprotocol string - rwc io.ReadWriteCloser - client bool - copts *compressionOptions - flateThreshold int - - br *bufio.Reader - bw *bufio.Writer -} - -func newConn(cfg connConfig) *Conn { - c := &Conn{ - subprotocol: cfg.subprotocol, - rwc: cfg.rwc, - client: cfg.client, - copts: cfg.copts, - flateThreshold: cfg.flateThreshold, - - br: cfg.br, - bw: cfg.bw, - - readTimeout: make(chan context.Context), - writeTimeout: make(chan context.Context), - - closed: make(chan struct{}), - activePings: make(map[string]chan<- struct{}), - } - - c.readMu = newMu(c) - c.writeFrameMu = newMu(c) - - c.msgReader = newMsgReader(c) - - c.msgWriterState = newMsgWriterState(c) - if c.client { - c.writeBuf = extractBufioWriterBuf(c.bw, c.rwc) - } - - if c.flate() && c.flateThreshold == 0 { - c.flateThreshold = 128 - if !c.msgWriterState.flateContextTakeover() { - c.flateThreshold = 512 - } - } - - runtime.SetFinalizer(c, func(c *Conn) { - c.close(errors.New("connection garbage collected")) - }) - - go c.timeoutLoop() - - return c -} - -// Subprotocol returns the negotiated subprotocol. -// An empty string means the default protocol. -func (c *Conn) Subprotocol() string { - return c.subprotocol -} - -func (c *Conn) close(err error) { - c.closeMu.Lock() - defer c.closeMu.Unlock() - - if c.isClosed() { - return - } - c.setCloseErrLocked(err) - close(c.closed) - runtime.SetFinalizer(c, nil) - - // Have to close after c.closed is closed to ensure any goroutine that wakes up - // from the connection being closed also sees that c.closed is closed and returns - // closeErr. - c.rwc.Close() - - go func() { - c.msgWriterState.close() - - c.msgReader.close() - }() -} - -func (c *Conn) timeoutLoop() { - readCtx := context.Background() - writeCtx := context.Background() - - for { - select { - case <-c.closed: - return - - case writeCtx = <-c.writeTimeout: - case readCtx = <-c.readTimeout: - - case <-readCtx.Done(): - c.setCloseErr(fmt.Errorf("read timed out: %w", readCtx.Err())) - go c.writeError(StatusPolicyViolation, errors.New("timed out")) - case <-writeCtx.Done(): - c.close(fmt.Errorf("write timed out: %w", writeCtx.Err())) - return - } - } -} - -func (c *Conn) flate() bool { - return c.copts != nil -} - -// Ping sends a ping to the peer and waits for a pong. -// Use this to measure latency or ensure the peer is responsive. -// Ping must be called concurrently with Reader as it does -// not read from the connection but instead waits for a Reader call -// to read the pong. -// -// TCP Keepalives should suffice for most use cases. -func (c *Conn) Ping(ctx context.Context) error { - p := atomic.AddInt32(&c.pingCounter, 1) - - err := c.ping(ctx, strconv.Itoa(int(p))) - if err != nil { - return fmt.Errorf("failed to ping: %w", err) - } - return nil -} - -func (c *Conn) ping(ctx context.Context, p string) error { - pong := make(chan struct{}) - - c.activePingsMu.Lock() - c.activePings[p] = pong - c.activePingsMu.Unlock() - - defer func() { - c.activePingsMu.Lock() - delete(c.activePings, p) - c.activePingsMu.Unlock() - }() - - err := c.writeControl(ctx, opPing, []byte(p)) - if err != nil { - return err - } - - select { - case <-c.closed: - return c.closeErr - case <-ctx.Done(): - err := fmt.Errorf("failed to wait for pong: %w", ctx.Err()) - c.close(err) - return err - case <-pong: - return nil - } -} - -type mu struct { - c *Conn - ch chan struct{} -} - -func newMu(c *Conn) *mu { - return &mu{ - c: c, - ch: make(chan struct{}, 1), - } -} - -func (m *mu) forceLock() { - m.ch <- struct{}{} -} - -func (m *mu) lock(ctx context.Context) error { - select { - case <-m.c.closed: - return m.c.closeErr - case <-ctx.Done(): - err := fmt.Errorf("failed to acquire lock: %w", ctx.Err()) - m.c.close(err) - return err - case m.ch <- struct{}{}: - // To make sure the connection is certainly alive. - // As it's possible the send on m.ch was selected - // over the receive on closed. - select { - case <-m.c.closed: - // Make sure to release. - m.unlock() - return m.c.closeErr - default: - } - return nil - } -} - -func (m *mu) unlock() { - select { - case <-m.ch: - default: - } -} diff --git a/ws_js.go b/ws_js.go index b87e32cd..31e3c2f6 100644 --- a/ws_js.go +++ b/ws_js.go @@ -377,3 +377,137 @@ func (c *Conn) isClosed() bool { return false } } + +// AcceptOptions represents Accept's options. +type AcceptOptions struct { + Subprotocols []string + InsecureSkipVerify bool + OriginPatterns []string + CompressionMode CompressionMode + CompressionThreshold int +} + +// Accept is stubbed out for Wasm. +func Accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, error) { + return nil, errors.New("unimplemented") +} + +// StatusCode represents a WebSocket status code. +// https://tools.ietf.org/html/rfc6455#section-7.4 +type StatusCode int + +// https://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number +// +// These are only the status codes defined by the protocol. +// +// You can define custom codes in the 3000-4999 range. +// The 3000-3999 range is reserved for use by libraries, frameworks and applications. +// The 4000-4999 range is reserved for private use. +const ( + StatusNormalClosure StatusCode = 1000 + StatusGoingAway StatusCode = 1001 + StatusProtocolError StatusCode = 1002 + StatusUnsupportedData StatusCode = 1003 + + // 1004 is reserved and so unexported. + statusReserved StatusCode = 1004 + + // StatusNoStatusRcvd cannot be sent in a close message. + // It is reserved for when a close message is received without + // a status code. + StatusNoStatusRcvd StatusCode = 1005 + + // StatusAbnormalClosure is exported for use only with Wasm. + // In non Wasm Go, the returned error will indicate whether the + // connection was closed abnormally. + StatusAbnormalClosure StatusCode = 1006 + + StatusInvalidFramePayloadData StatusCode = 1007 + StatusPolicyViolation StatusCode = 1008 + StatusMessageTooBig StatusCode = 1009 + StatusMandatoryExtension StatusCode = 1010 + StatusInternalError StatusCode = 1011 + StatusServiceRestart StatusCode = 1012 + StatusTryAgainLater StatusCode = 1013 + StatusBadGateway StatusCode = 1014 + + // StatusTLSHandshake is only exported for use with Wasm. + // In non Wasm Go, the returned error will indicate whether there was + // a TLS handshake failure. + StatusTLSHandshake StatusCode = 1015 +) + +// CloseError is returned when the connection is closed with a status and reason. +// +// Use Go 1.13's errors.As to check for this error. +// Also see the CloseStatus helper. +type CloseError struct { + Code StatusCode + Reason string +} + +func (ce CloseError) Error() string { + return fmt.Sprintf("status = %v and reason = %q", ce.Code, ce.Reason) +} + +// CloseStatus is a convenience wrapper around Go 1.13's errors.As to grab +// the status code from a CloseError. +// +// -1 will be returned if the passed error is nil or not a CloseError. +func CloseStatus(err error) StatusCode { + var ce CloseError + if errors.As(err, &ce) { + return ce.Code + } + return -1 +} + +// CompressionMode represents the modes available to the deflate extension. +// See https://tools.ietf.org/html/rfc7692 +// +// A compatibility layer is implemented for the older deflate-frame extension used +// by safari. See https://tools.ietf.org/html/draft-tyoshino-hybi-websocket-perframe-deflate-06 +// It will work the same in every way except that we cannot signal to the peer we +// want to use no context takeover on our side, we can only signal that they should. +// It is however currently disabled due to Safari bugs. See https://github.com/nhooyr/websocket/issues/218 +type CompressionMode int + +const ( + // CompressionNoContextTakeover grabs a new flate.Reader and flate.Writer as needed + // for every message. This applies to both server and client side. + // + // This means less efficient compression as the sliding window from previous messages + // will not be used but the memory overhead will be lower if the connections + // are long lived and seldom used. + // + // The message will only be compressed if greater than 512 bytes. + CompressionNoContextTakeover CompressionMode = iota + + // CompressionContextTakeover uses a flate.Reader and flate.Writer per connection. + // This enables reusing the sliding window from previous messages. + // As most WebSocket protocols are repetitive, this can be very efficient. + // It carries an overhead of 8 kB for every connection compared to CompressionNoContextTakeover. + // + // If the peer negotiates NoContextTakeover on the client or server side, it will be + // used instead as this is required by the RFC. + CompressionContextTakeover + + // CompressionDisabled disables the deflate extension. + // + // Use this if you are using a predominantly binary protocol with very + // little duplication in between messages or CPU and memory are more + // important than bandwidth. + CompressionDisabled +) + +// MessageType represents the type of a WebSocket message. +// See https://tools.ietf.org/html/rfc6455#section-5.6 +type MessageType int + +// MessageType constants. +const ( + // MessageText is for UTF-8 encoded text messages like JSON. + MessageText MessageType = iota + 1 + // MessageBinary is for binary messages like protobufs. + MessageBinary +) From 17cf0fe86c9c23e64714986b266a15fd9a26142d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 18 May 2020 03:12:08 -0400 Subject: [PATCH 325/519] Disable compression by default Closes #220 and #230 --- README.md | 3 +-- accept.go | 2 +- accept_test.go | 4 +++- autobahn_test.go | 4 +++- compress.go | 60 +++++++++++++++++++++++++++++------------------- conn_test.go | 4 ++-- dial.go | 2 +- go.mod | 1 - go.sum | 2 -- write.go | 35 ++++++++++++++++++---------- 10 files changed, 71 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index df20c581..8420bdbd 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ go get nhooyr.io/websocket - Minimal and idiomatic API - First class [context.Context](https://blog.golang.org/context) support - Fully passes the WebSocket [autobahn-testsuite](https://github.com/crossbario/autobahn-testsuite) -- [Single dependency](https://pkg.go.dev/nhooyr.io/websocket?tab=imports) +- [Zero dependencies](https://pkg.go.dev/nhooyr.io/websocket?tab=imports) - 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 - Zero alloc reads and writes - Concurrent writes @@ -112,7 +112,6 @@ Advantages of nhooyr.io/websocket: - Gorilla's implementation is slower and uses [unsafe](https://golang.org/pkg/unsafe/). - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support - Gorilla only supports no context takeover mode - - We use [klauspost/compress](https://github.com/klauspost/compress) for much lower memory usage ([gorilla/websocket#203](https://github.com/gorilla/websocket/issues/203)) - [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492)) - Actively maintained ([gorilla/websocket#370](https://github.com/gorilla/websocket/issues/370)) diff --git a/accept.go b/accept.go index 66379b5d..f038dec9 100644 --- a/accept.go +++ b/accept.go @@ -51,7 +51,7 @@ type AcceptOptions struct { OriginPatterns []string // CompressionMode controls the compression mode. - // Defaults to CompressionNoContextTakeover. + // Defaults to CompressionDisabled. // // See docs on CompressionMode for details. CompressionMode CompressionMode diff --git a/accept_test.go b/accept_test.go index 9b18d8e1..f7bc6693 100644 --- a/accept_test.go +++ b/accept_test.go @@ -55,7 +55,9 @@ func TestAccept(t *testing.T) { r.Header.Set("Sec-WebSocket-Key", "meow123") r.Header.Set("Sec-WebSocket-Extensions", "permessage-deflate; harharhar") - _, err := Accept(w, r, nil) + _, err := Accept(w, r, &AcceptOptions{ + CompressionMode: CompressionContextTakeover, + }) assert.Contains(t, err, `unsupported permessage-deflate parameter`) }) diff --git a/autobahn_test.go b/autobahn_test.go index e56a4912..d53159a0 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -61,7 +61,9 @@ func TestAutobahn(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) defer cancel() - c, _, err := websocket.Dial(ctx, fmt.Sprintf(wstestURL+"/runCase?case=%v&agent=main", i), nil) + c, _, err := websocket.Dial(ctx, fmt.Sprintf(wstestURL+"/runCase?case=%v&agent=main", i), &websocket.DialOptions{ + CompressionMode: websocket.CompressionContextTakeover, + }) assert.Success(t, err) err = wstest.EchoLoop(ctx, c) t.Logf("echoLoop: %v", err) diff --git a/compress.go b/compress.go index 63d961b4..f49d9e5d 100644 --- a/compress.go +++ b/compress.go @@ -3,49 +3,47 @@ package websocket import ( + "compress/flate" "io" "net/http" "sync" - - "github.com/klauspost/compress/flate" ) // CompressionMode represents the modes available to the deflate extension. // See https://tools.ietf.org/html/rfc7692 -// -// A compatibility layer is implemented for the older deflate-frame extension used -// by safari. See https://tools.ietf.org/html/draft-tyoshino-hybi-websocket-perframe-deflate-06 -// It will work the same in every way except that we cannot signal to the peer we -// want to use no context takeover on our side, we can only signal that they should. -// It is however currently disabled due to Safari bugs. See https://github.com/nhooyr/websocket/issues/218 type CompressionMode int const ( - // CompressionNoContextTakeover grabs a new flate.Reader and flate.Writer as needed - // for every message. This applies to both server and client side. + // CompressionDisabled disables the deflate extension. // - // This means less efficient compression as the sliding window from previous messages - // will not be used but the memory overhead will be lower if the connections - // are long lived and seldom used. + // Use this if you are using a predominantly binary protocol with very + // little duplication in between messages or CPU and memory are more + // important than bandwidth. // - // The message will only be compressed if greater than 512 bytes. - CompressionNoContextTakeover CompressionMode = iota + // This is the default. + CompressionDisabled CompressionMode = iota - // CompressionContextTakeover uses a flate.Reader and flate.Writer per connection. - // This enables reusing the sliding window from previous messages. + // CompressionContextTakeover uses a 32 kB sliding window and flate.Writer per connection. + // It reusing the sliding window from previous messages. // As most WebSocket protocols are repetitive, this can be very efficient. - // It carries an overhead of 8 kB for every connection compared to CompressionNoContextTakeover. + // It carries an overhead of 32 kB + 1.2 MB for every connection compared to CompressionNoContextTakeover. + // + // Sometime in the future it will carry 65 kB overhead instead once https://github.com/golang/go/issues/36919 + // is fixed. // // If the peer negotiates NoContextTakeover on the client or server side, it will be // used instead as this is required by the RFC. CompressionContextTakeover - // CompressionDisabled disables the deflate extension. + // CompressionNoContextTakeover grabs a new flate.Reader and flate.Writer as needed + // for every message. This applies to both server and client side. // - // Use this if you are using a predominantly binary protocol with very - // little duplication in between messages or CPU and memory are more - // important than bandwidth. - CompressionDisabled + // This means less efficient compression as the sliding window from previous messages + // will not be used but the memory overhead will be lower if the connections + // are long lived and seldom used. + // + // The message will only be compressed if greater than 512 bytes. + CompressionNoContextTakeover ) func (m CompressionMode) opts() *compressionOptions { @@ -146,6 +144,22 @@ func putFlateReader(fr io.Reader) { flateReaderPool.Put(fr) } +var flateWriterPool sync.Pool + +func getFlateWriter(w io.Writer) *flate.Writer { + fw, ok := flateWriterPool.Get().(*flate.Writer) + if !ok { + fw, _ = flate.NewWriter(w, flate.BestSpeed) + return fw + } + fw.Reset(w) + return fw +} + +func putFlateWriter(w *flate.Writer) { + flateWriterPool.Put(w) +} + type slidingWindow struct { buf []byte } diff --git a/conn_test.go b/conn_test.go index c2c41292..4bab5adf 100644 --- a/conn_test.go +++ b/conn_test.go @@ -37,7 +37,7 @@ func TestConn(t *testing.T) { t.Parallel() compressionMode := func() websocket.CompressionMode { - return websocket.CompressionMode(xrand.Int(int(websocket.CompressionDisabled) + 1)) + return websocket.CompressionMode(xrand.Int(int(websocket.CompressionContextTakeover) + 1)) } for i := 0; i < 5; i++ { @@ -389,7 +389,7 @@ func BenchmarkConn(b *testing.B) { mode: websocket.CompressionDisabled, }, { - name: "compress", + name: "compressContextTakeover", mode: websocket.CompressionContextTakeover, }, { diff --git a/dial.go b/dial.go index 2b25e351..9ec90444 100644 --- a/dial.go +++ b/dial.go @@ -35,7 +35,7 @@ type DialOptions struct { Subprotocols []string // CompressionMode controls the compression mode. - // Defaults to CompressionNoContextTakeover. + // Defaults to CompressionDisabled. // // See docs on CompressionMode for details. CompressionMode CompressionMode diff --git a/go.mod b/go.mod index c5f1a20f..d4bca923 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,5 @@ require ( github.com/golang/protobuf v1.3.5 github.com/google/go-cmp v0.4.0 github.com/gorilla/websocket v1.4.1 - github.com/klauspost/compress v1.10.3 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 ) diff --git a/go.sum b/go.sum index 155c3013..1344e958 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,6 @@ github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvK github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= -github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= diff --git a/write.go b/write.go index 2210cf81..b1c57c1b 100644 --- a/write.go +++ b/write.go @@ -12,7 +12,7 @@ import ( "io" "time" - "github.com/klauspost/compress/flate" + "compress/flate" "nhooyr.io/websocket/internal/errd" ) @@ -76,8 +76,8 @@ type msgWriterState struct { opcode opcode flate bool - trimWriter *trimLastFourBytesWriter - dict slidingWindow + trimWriter *trimLastFourBytesWriter + flateWriter *flate.Writer } func newMsgWriterState(c *Conn) *msgWriterState { @@ -96,7 +96,9 @@ func (mw *msgWriterState) ensureFlate() { } } - mw.dict.init(8192) + if mw.flateWriter == nil { + mw.flateWriter = getFlateWriter(mw.trimWriter) + } mw.flate = true } @@ -153,6 +155,13 @@ func (mw *msgWriterState) reset(ctx context.Context, typ MessageType) error { return nil } +func (mw *msgWriterState) putFlateWriter() { + if mw.flateWriter != nil { + putFlateWriter(mw.flateWriter) + mw.flateWriter = nil + } +} + // Write writes the given bytes to the WebSocket connection. func (mw *msgWriterState) Write(p []byte) (_ int, err error) { err = mw.writeMu.lock(mw.ctx) @@ -177,12 +186,7 @@ func (mw *msgWriterState) Write(p []byte) (_ int, err error) { } if mw.flate { - err = flate.StatelessDeflate(mw.trimWriter, p, false, mw.dict.buf) - if err != nil { - return 0, err - } - mw.dict.write(p) - return len(p), nil + return mw.flateWriter.Write(p) } return mw.write(p) @@ -207,13 +211,20 @@ func (mw *msgWriterState) Close() (err error) { } defer mw.writeMu.unlock() + if mw.flate { + err = mw.flateWriter.Flush() + if err != nil { + return fmt.Errorf("failed to flush flate: %w", err) + } + } + _, err = mw.c.writeFrame(mw.ctx, true, mw.flate, mw.opcode, nil) if err != nil { return fmt.Errorf("failed to write fin frame: %w", err) } if mw.flate && !mw.flateContextTakeover() { - mw.dict.close() + mw.putFlateWriter() } mw.mu.unlock() return nil @@ -226,7 +237,7 @@ func (mw *msgWriterState) close() { } mw.writeMu.forceLock() - mw.dict.close() + mw.putFlateWriter() } func (c *Conn) writeControl(ctx context.Context, opcode opcode, p []byte) error { From de8e29bdb753bc55c8f742c664adb44833afbc50 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 18 May 2020 04:25:52 -0400 Subject: [PATCH 326/519] Fix tests taking too long and switch to t.Cleanup --- autobahn_test.go | 7 ++++++- conn_test.go | 47 +++++++++++++---------------------------------- 2 files changed, 19 insertions(+), 35 deletions(-) diff --git a/autobahn_test.go b/autobahn_test.go index d53159a0..5bf0062c 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -28,7 +28,6 @@ var excludedAutobahnCases = []string{ // We skip the tests related to requestMaxWindowBits as that is unimplemented due // to limitations in compress/flate. See https://github.com/golang/go/issues/3155 - // Same with klauspost/compress which doesn't allow adjusting the sliding window size. "13.3.*", "13.4.*", "13.5.*", "13.6.*", } @@ -41,6 +40,12 @@ func TestAutobahn(t *testing.T) { t.SkipNow() } + if os.Getenv("AUTOBAHN_FAST") != "" { + excludedAutobahnCases = append(excludedAutobahnCases, + "9.*", "13.*", "12.*", + ) + } + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*15) defer cancel() diff --git a/conn_test.go b/conn_test.go index 4bab5adf..9c85459e 100644 --- a/conn_test.go +++ b/conn_test.go @@ -49,7 +49,6 @@ func TestConn(t *testing.T) { CompressionMode: compressionMode(), CompressionThreshold: xrand.Int(9999), }) - defer tt.cleanup() tt.goEchoLoop(c2) @@ -67,8 +66,9 @@ func TestConn(t *testing.T) { }) t.Run("badClose", func(t *testing.T) { - tt, c1, _ := newConnTest(t, nil, nil) - defer tt.cleanup() + tt, c1, c2 := newConnTest(t, nil, nil) + + c2.CloseRead(tt.ctx) err := c1.Close(-1, "") assert.Contains(t, err, "failed to marshal close frame: status code StatusCode(-1) cannot be set") @@ -76,7 +76,6 @@ func TestConn(t *testing.T) { t.Run("ping", func(t *testing.T) { tt, c1, c2 := newConnTest(t, nil, nil) - defer tt.cleanup() c1.CloseRead(tt.ctx) c2.CloseRead(tt.ctx) @@ -92,7 +91,6 @@ func TestConn(t *testing.T) { t.Run("badPing", func(t *testing.T) { tt, c1, c2 := newConnTest(t, nil, nil) - defer tt.cleanup() c2.CloseRead(tt.ctx) @@ -105,7 +103,6 @@ func TestConn(t *testing.T) { t.Run("concurrentWrite", func(t *testing.T) { tt, c1, c2 := newConnTest(t, nil, nil) - defer tt.cleanup() tt.goDiscardLoop(c2) @@ -138,7 +135,6 @@ func TestConn(t *testing.T) { t.Run("concurrentWriteError", func(t *testing.T) { tt, c1, _ := newConnTest(t, nil, nil) - defer tt.cleanup() _, err := c1.Writer(tt.ctx, websocket.MessageText) assert.Success(t, err) @@ -152,7 +148,6 @@ func TestConn(t *testing.T) { t.Run("netConn", func(t *testing.T) { tt, c1, c2 := newConnTest(t, nil, nil) - defer tt.cleanup() n1 := websocket.NetConn(tt.ctx, c1, websocket.MessageBinary) n2 := websocket.NetConn(tt.ctx, c2, websocket.MessageBinary) @@ -192,17 +187,14 @@ func TestConn(t *testing.T) { t.Run("netConn/BadMsg", func(t *testing.T) { tt, c1, c2 := newConnTest(t, nil, nil) - defer tt.cleanup() n1 := websocket.NetConn(tt.ctx, c1, websocket.MessageBinary) n2 := websocket.NetConn(tt.ctx, c2, websocket.MessageText) + c2.CloseRead(tt.ctx) errs := xsync.Go(func() error { _, err := n2.Write([]byte("hello")) - if err != nil { - return err - } - return nil + return err }) _, err := ioutil.ReadAll(n1) @@ -218,7 +210,6 @@ func TestConn(t *testing.T) { t.Run("wsjson", func(t *testing.T) { tt, c1, c2 := newConnTest(t, nil, nil) - defer tt.cleanup() tt.goEchoLoop(c2) @@ -248,7 +239,6 @@ func TestConn(t *testing.T) { t.Run("wspb", func(t *testing.T) { tt, c1, c2 := newConnTest(t, nil, nil) - defer tt.cleanup() tt.goEchoLoop(c2) @@ -305,8 +295,6 @@ func assertCloseStatus(exp websocket.StatusCode, err error) error { type connTest struct { t testing.TB ctx context.Context - - doneFuncs []func() } func newConnTest(t testing.TB, dialOpts *websocket.DialOptions, acceptOpts *websocket.AcceptOptions) (tt *connTest, c1, c2 *websocket.Conn) { @@ -317,30 +305,22 @@ func newConnTest(t testing.TB, dialOpts *websocket.DialOptions, acceptOpts *webs ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) tt = &connTest{t: t, ctx: ctx} - tt.appendDone(cancel) + t.Cleanup(cancel) c1, c2 = wstest.Pipe(dialOpts, acceptOpts) if xrand.Bool() { c1, c2 = c2, c1 } - tt.appendDone(func() { - c2.Close(websocket.StatusInternalError, "") - c1.Close(websocket.StatusInternalError, "") + t.Cleanup(func() { + // We don't actually care whether this succeeds so we just run it in a separate goroutine to avoid + // blocking the test shutting down. + go c2.Close(websocket.StatusInternalError, "") + go c1.Close(websocket.StatusInternalError, "") }) return tt, c1, c2 } -func (tt *connTest) appendDone(f func()) { - tt.doneFuncs = append(tt.doneFuncs, f) -} - -func (tt *connTest) cleanup() { - for i := len(tt.doneFuncs) - 1; i >= 0; i-- { - tt.doneFuncs[i]() - } -} - func (tt *connTest) goEchoLoop(c *websocket.Conn) { ctx, cancel := context.WithCancel(tt.ctx) @@ -348,7 +328,7 @@ func (tt *connTest) goEchoLoop(c *websocket.Conn) { err := wstest.EchoLoop(ctx, c) return assertCloseStatus(websocket.StatusNormalClosure, err) }) - tt.appendDone(func() { + tt.t.Cleanup(func() { cancel() err := <-echoLoopErr if err != nil { @@ -370,7 +350,7 @@ func (tt *connTest) goDiscardLoop(c *websocket.Conn) { } } }) - tt.appendDone(func() { + tt.t.Cleanup(func() { cancel() err := <-discardLoopErr if err != nil { @@ -404,7 +384,6 @@ func BenchmarkConn(b *testing.B) { }, &websocket.AcceptOptions{ CompressionMode: bc.mode, }) - defer bb.cleanup() bb.goEchoLoop(c2) From 169521697c04f5b5a06b3da51bf4cad56884d2b6 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 18 May 2020 14:09:47 -0400 Subject: [PATCH 327/519] Add ping example Closes #227 --- autobahn_test.go | 5 +++-- example_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/autobahn_test.go b/autobahn_test.go index 5bf0062c..7c735a38 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -36,11 +36,12 @@ var autobahnCases = []string{"*"} func TestAutobahn(t *testing.T) { t.Parallel() - if os.Getenv("AUTOBAHN_TEST") == "" { + if os.Getenv("AUTOBAHN") == "" { t.SkipNow() } - if os.Getenv("AUTOBAHN_FAST") != "" { + if os.Getenv("AUTOBAHN") == "fast" { + // These are the slow tests. excludedAutobahnCases = append(excludedAutobahnCases, "9.*", "13.*", "12.*", ) diff --git a/example_test.go b/example_test.go index 632c4d6e..d44bd537 100644 --- a/example_test.go +++ b/example_test.go @@ -135,6 +135,31 @@ func Example_crossOrigin() { log.Fatal(err) } +func ExampleConn_Ping() { + // Dials a server and pings it 5 times. + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + c, _, err := websocket.Dial(ctx, "ws://localhost:8080", nil) + if err != nil { + log.Fatal(err) + } + defer c.Close(websocket.StatusInternalError, "the sky is falling") + + // Required to read the Pongs from the server. + ctx = c.CloseRead(ctx) + + for i := 0; i < 5; i++ { + err = c.Ping(ctx) + if err != nil { + log.Fatal(err) + } + } + + c.Close(websocket.StatusNormalClosure, "") +} + // This example demonstrates how to create a WebSocket server // that gracefully exits when sent a signal. // From 0a61ffe87a498f8ff9fef8020bee799cfa4f927f Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 18 May 2020 19:09:38 -0400 Subject: [PATCH 328/519] Make SetDeadline on NetConn not always close Conn NetConn has to close the connection to interrupt in progress reads and writes. However, it can block reads and writes that occur after the deadline instead of closing the connection. Closes #228 --- conn.go | 9 ++++ netconn.go | 128 +++++++++++++++++++++++++++++++++++------------------ ws_js.go | 32 ++++++++++++++ 3 files changed, 126 insertions(+), 43 deletions(-) diff --git a/conn.go b/conn.go index e208d116..1a57c656 100644 --- a/conn.go +++ b/conn.go @@ -246,6 +246,15 @@ func (m *mu) forceLock() { m.ch <- struct{}{} } +func (m *mu) tryLock() bool { + select { + case m.ch <- struct{}{}: + return true + default: + return false + } +} + func (m *mu) lock(ctx context.Context) error { select { case <-m.c.closed: diff --git a/netconn.go b/netconn.go index 64aadf0b..ae04b20a 100644 --- a/netconn.go +++ b/netconn.go @@ -6,7 +6,7 @@ import ( "io" "math" "net" - "sync" + "sync/atomic" "time" ) @@ -28,9 +28,10 @@ import ( // // Close will close the *websocket.Conn with StatusNormalClosure. // -// When a deadline is hit, the connection will be closed. This is -// different from most net.Conn implementations where only the -// reading/writing goroutines are interrupted but the connection is kept alive. +// When a deadline is hit and there is an active read or write goroutine, the +// connection will be closed. This is different from most net.Conn implementations +// where only the reading/writing goroutines are interrupted but the connection +// is kept alive. // // The Addr methods will return a mock net.Addr that returns "websocket" for Network // and "websocket/unknown-addr" for String. @@ -41,17 +42,43 @@ func NetConn(ctx context.Context, c *Conn, msgType MessageType) net.Conn { nc := &netConn{ c: c, msgType: msgType, + readMu: newMu(c), + writeMu: newMu(c), } - var cancel context.CancelFunc - nc.writeContext, cancel = context.WithCancel(ctx) - nc.writeTimer = time.AfterFunc(math.MaxInt64, cancel) + var writeCancel context.CancelFunc + nc.writeCtx, writeCancel = context.WithCancel(ctx) + var readCancel context.CancelFunc + nc.readCtx, readCancel = context.WithCancel(ctx) + + nc.writeTimer = time.AfterFunc(math.MaxInt64, func() { + if !nc.writeMu.tryLock() { + // If the lock cannot be acquired, then there is an + // active write goroutine and so we should cancel the context. + writeCancel() + return + } + defer nc.writeMu.unlock() + + // Prevents future writes from writing until the deadline is reset. + atomic.StoreInt64(&nc.writeExpired, 1) + }) if !nc.writeTimer.Stop() { <-nc.writeTimer.C } - nc.readContext, cancel = context.WithCancel(ctx) - nc.readTimer = time.AfterFunc(math.MaxInt64, cancel) + nc.readTimer = time.AfterFunc(math.MaxInt64, func() { + if !nc.readMu.tryLock() { + // If the lock cannot be acquired, then there is an + // active read goroutine and so we should cancel the context. + readCancel() + return + } + defer nc.readMu.unlock() + + // Prevents future reads from reading until the deadline is reset. + atomic.StoreInt64(&nc.readExpired, 1) + }) if !nc.readTimer.Stop() { <-nc.readTimer.C } @@ -64,59 +91,72 @@ type netConn struct { msgType MessageType writeTimer *time.Timer - writeContext context.Context + writeMu *mu + writeExpired int64 + writeCtx context.Context readTimer *time.Timer - readContext context.Context - - readMu sync.Mutex - eofed bool - reader io.Reader + readMu *mu + readExpired int64 + readCtx context.Context + readEOFed bool + reader io.Reader } var _ net.Conn = &netConn{} -func (c *netConn) Close() error { - return c.c.Close(StatusNormalClosure, "") +func (nc *netConn) Close() error { + return nc.c.Close(StatusNormalClosure, "") } -func (c *netConn) Write(p []byte) (int, error) { - err := c.c.Write(c.writeContext, c.msgType, p) +func (nc *netConn) Write(p []byte) (int, error) { + nc.writeMu.forceLock() + defer nc.writeMu.unlock() + + if atomic.LoadInt64(&nc.writeExpired) == 1 { + return 0, fmt.Errorf("failed to write: %w", context.DeadlineExceeded) + } + + err := nc.c.Write(nc.writeCtx, nc.msgType, p) if err != nil { return 0, err } return len(p), nil } -func (c *netConn) Read(p []byte) (int, error) { - c.readMu.Lock() - defer c.readMu.Unlock() +func (nc *netConn) Read(p []byte) (int, error) { + nc.readMu.forceLock() + defer nc.readMu.unlock() + + if atomic.LoadInt64(&nc.readExpired) == 1 { + return 0, fmt.Errorf("failed to read: %w", context.DeadlineExceeded) + } - if c.eofed { + if nc.readEOFed { return 0, io.EOF } - if c.reader == nil { - typ, r, err := c.c.Reader(c.readContext) + if nc.reader == nil { + typ, r, err := nc.c.Reader(nc.readCtx) if err != nil { switch CloseStatus(err) { case StatusNormalClosure, StatusGoingAway: - c.eofed = true + nc.readEOFed = true return 0, io.EOF } return 0, err } - if typ != c.msgType { - err := fmt.Errorf("unexpected frame type read (expected %v): %v", c.msgType, typ) - c.c.Close(StatusUnsupportedData, err.Error()) + if typ != nc.msgType { + err := fmt.Errorf("unexpected frame type read (expected %v): %v", nc.msgType, typ) + nc.c.Close(StatusUnsupportedData, err.Error()) return 0, err } - c.reader = r + nc.reader = r } - n, err := c.reader.Read(p) + n, err := nc.reader.Read(p) if err == io.EOF { - c.reader = nil + nc.reader = nil err = nil } return n, err @@ -133,34 +173,36 @@ func (a websocketAddr) String() string { return "websocket/unknown-addr" } -func (c *netConn) RemoteAddr() net.Addr { +func (nc *netConn) RemoteAddr() net.Addr { return websocketAddr{} } -func (c *netConn) LocalAddr() net.Addr { +func (nc *netConn) LocalAddr() net.Addr { return websocketAddr{} } -func (c *netConn) SetDeadline(t time.Time) error { - c.SetWriteDeadline(t) - c.SetReadDeadline(t) +func (nc *netConn) SetDeadline(t time.Time) error { + nc.SetWriteDeadline(t) + nc.SetReadDeadline(t) return nil } -func (c *netConn) SetWriteDeadline(t time.Time) error { +func (nc *netConn) SetWriteDeadline(t time.Time) error { + atomic.StoreInt64(&nc.writeExpired, 0) if t.IsZero() { - c.writeTimer.Stop() + nc.writeTimer.Stop() } else { - c.writeTimer.Reset(t.Sub(time.Now())) + nc.writeTimer.Reset(t.Sub(time.Now())) } return nil } -func (c *netConn) SetReadDeadline(t time.Time) error { +func (nc *netConn) SetReadDeadline(t time.Time) error { + atomic.StoreInt64(&nc.readExpired, 0) if t.IsZero() { - c.readTimer.Stop() + nc.readTimer.Stop() } else { - c.readTimer.Reset(t.Sub(time.Now())) + nc.readTimer.Reset(t.Sub(time.Now())) } return nil } diff --git a/ws_js.go b/ws_js.go index 31e3c2f6..d1361328 100644 --- a/ws_js.go +++ b/ws_js.go @@ -511,3 +511,35 @@ const ( // MessageBinary is for binary messages like protobufs. MessageBinary ) + +type mu struct { + c *Conn + ch chan struct{} +} + +func newMu(c *Conn) *mu { + return &mu{ + c: c, + ch: make(chan struct{}, 1), + } +} + +func (m *mu) forceLock() { + m.ch <- struct{}{} +} + +func (m *mu) tryLock() bool { + select { + case m.ch <- struct{}{}: + return true + default: + return false + } +} + +func (m *mu) unlock() { + select { + case <-m.ch: + default: + } +} From 15a152334e5aacc0158b541e135fe9f0834696dd Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 4 Jul 2020 19:21:25 -0400 Subject: [PATCH 329/519] ci/fmt.sh: Cleanup --- ci/fmt.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/fmt.sh b/ci/fmt.sh index e6a2d689..b34f1438 100755 --- a/ci/fmt.sh +++ b/ci/fmt.sh @@ -21,11 +21,11 @@ main() { stringer -type=opcode,MessageType,StatusCode -output=stringer.go if [[ ${CI-} ]]; then - ensure_fmt + assert_no_changes fi } -ensure_fmt() { +assert_no_changes() { if [[ $(git ls-files --other --modified --exclude-standard) ]]; then git -c color.ui=always --no-pager diff echo From 493ebbe9373d536b64e122b54dc2f56ad7b79b12 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 5 Jul 2020 17:01:55 -0400 Subject: [PATCH 330/519] netconn.go: Prevent timer leakage (#255) Closes #243 --- netconn.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netconn.go b/netconn.go index ae04b20a..1664e29b 100644 --- a/netconn.go +++ b/netconn.go @@ -106,6 +106,8 @@ type netConn struct { var _ net.Conn = &netConn{} func (nc *netConn) Close() error { + nc.writeTimer.Stop() + nc.readTimer.Stop() return nc.c.Close(StatusNormalClosure, "") } From 897a573291bed65c3528f779406add491d096a7f Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 5 Jul 2020 17:02:20 -0400 Subject: [PATCH 331/519] write.go: Fix deadlock in writeFrame (#253) Closes #248 Luckily, due to the 5s timeout on the close handshake, this would have had very minimal effects on anyone in production. --- write.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/write.go b/write.go index b1c57c1b..58bfdf9a 100644 --- a/write.go +++ b/write.go @@ -257,7 +257,6 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco if err != nil { return 0, err } - defer c.writeFrameMu.unlock() // If the state says a close has already been written, we wait until // the connection is closed and return that error. @@ -268,6 +267,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco wroteClose := c.wroteClose c.closeMu.Unlock() if wroteClose && opcode != opClose { + c.writeFrameMu.unlock() select { case <-ctx.Done(): return 0, ctx.Err() @@ -275,6 +275,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco return 0, c.closeErr } } + defer c.writeFrameMu.unlock() select { case <-c.closed: From 1b61eb0b3360ef904f841825492bd637189721cd Mon Sep 17 00:00:00 2001 From: swithek Date: Tue, 22 Sep 2020 13:55:08 +0300 Subject: [PATCH 332/519] Copy http client --- dial.go | 14 +++++++++----- dial_test.go | 9 --------- 2 files changed, 9 insertions(+), 14 deletions(-) diff --git a/dial.go b/dial.go index 2b25e351..35516943 100644 --- a/dial.go +++ b/dial.go @@ -8,7 +8,6 @@ import ( "context" "crypto/rand" "encoding/base64" - "errors" "fmt" "io" "io/ioutil" @@ -26,6 +25,7 @@ type DialOptions struct { // HTTPClient is used for the connection. // Its Transport must return writable bodies for WebSocket handshakes. // http.Transport does beginning with Go 1.12. + // Non-zero timeout will be ignored, see https://github.com/nhooyr/websocket/issues/67. HTTPClient *http.Client // HTTPHeader specifies the HTTP headers included in the handshake request. @@ -74,7 +74,15 @@ func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) ( opts = &*opts if opts.HTTPClient == nil { opts.HTTPClient = http.DefaultClient + } else if opts.HTTPClient.Timeout > 0 { + // remove timeout + opts.HTTPClient = &http.Client{ + Transport: opts.HTTPClient.Transport, + CheckRedirect: opts.HTTPClient.CheckRedirect, + Jar: opts.HTTPClient.Jar, + } } + if opts.HTTPHeader == nil { opts.HTTPHeader = http.Header{} } @@ -133,10 +141,6 @@ func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) ( } func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, copts *compressionOptions, secWebSocketKey string) (*http.Response, error) { - if opts.HTTPClient.Timeout > 0 { - return nil, errors.New("use context for cancellation instead of http.Client.Timeout; see https://github.com/nhooyr/websocket/issues/67") - } - u, err := url.Parse(urls) if err != nil { return nil, fmt.Errorf("failed to parse url: %w", err) diff --git a/dial_test.go b/dial_test.go index 7f13a934..28c255c6 100644 --- a/dial_test.go +++ b/dial_test.go @@ -36,15 +36,6 @@ func TestBadDials(t *testing.T) { name: "badURLScheme", url: "ftp://nhooyr.io", }, - { - name: "badHTTPClient", - url: "ws://nhooyr.io", - opts: &DialOptions{ - HTTPClient: &http.Client{ - Timeout: time.Minute, - }, - }, - }, { name: "badTLS", url: "wss://totallyfake.nhooyr.io", From fdc407913d18e6fff8feacf9bc50f8545c234d9d Mon Sep 17 00:00:00 2001 From: Andy Bursavich Date: Wed, 23 Sep 2020 23:38:22 -0700 Subject: [PATCH 333/519] Clone options (#259) See: https://staticcheck.io/docs/checks#SA4001 --- accept.go | 14 +++++++++----- dial.go | 26 +++++++++++++++----------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/accept.go b/accept.go index f038dec9..428abba4 100644 --- a/accept.go +++ b/accept.go @@ -63,6 +63,14 @@ type AcceptOptions struct { CompressionThreshold int } +func (opts *AcceptOptions) cloneWithDefaults() *AcceptOptions { + var o AcceptOptions + if opts != nil { + o = *opts + } + return &o +} + // Accept accepts a WebSocket handshake from a client and upgrades the // the connection to a WebSocket. // @@ -77,17 +85,13 @@ func Accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Conn, err error) { defer errd.Wrap(&err, "failed to accept WebSocket connection") - if opts == nil { - opts = &AcceptOptions{} - } - opts = &*opts - errCode, err := verifyClientRequest(w, r) if err != nil { http.Error(w, err.Error(), errCode) return nil, err } + opts = opts.cloneWithDefaults() if !opts.InsecureSkipVerify { err = authenticateOrigin(r, opts.OriginPatterns) if err != nil { diff --git a/dial.go b/dial.go index 9ec90444..d5d2266e 100644 --- a/dial.go +++ b/dial.go @@ -47,6 +47,20 @@ type DialOptions struct { CompressionThreshold int } +func (opts *DialOptions) cloneWithDefaults() *DialOptions { + var o DialOptions + if opts != nil { + o = *opts + } + if o.HTTPClient == nil { + o.HTTPClient = http.DefaultClient + } + if o.HTTPHeader == nil { + o.HTTPHeader = http.Header{} + } + return &o +} + // Dial performs a WebSocket handshake on url. // // The response is the WebSocket handshake response from the server. @@ -67,17 +81,7 @@ func Dial(ctx context.Context, u string, opts *DialOptions) (*Conn, *http.Respon func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) (_ *Conn, _ *http.Response, err error) { defer errd.Wrap(&err, "failed to WebSocket dial") - if opts == nil { - opts = &DialOptions{} - } - - opts = &*opts - if opts.HTTPClient == nil { - opts.HTTPClient = http.DefaultClient - } - if opts.HTTPHeader == nil { - opts.HTTPHeader = http.Header{} - } + opts = opts.cloneWithDefaults() secWebSocketKey, err := secWebSocketKey(rand) if err != nil { From dbaf6f8f37bc74b7176f9cada3cbf454e2fcf148 Mon Sep 17 00:00:00 2001 From: swithek Date: Thu, 24 Sep 2020 10:33:22 +0300 Subject: [PATCH 334/519] Create context with http client timeout --- dial.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/dial.go b/dial.go index 35516943..849515e8 100644 --- a/dial.go +++ b/dial.go @@ -25,7 +25,6 @@ type DialOptions struct { // HTTPClient is used for the connection. // Its Transport must return writable bodies for WebSocket handshakes. // http.Transport does beginning with Go 1.12. - // Non-zero timeout will be ignored, see https://github.com/nhooyr/websocket/issues/67. HTTPClient *http.Client // HTTPHeader specifies the HTTP headers included in the handshake request. @@ -75,7 +74,11 @@ func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) ( if opts.HTTPClient == nil { opts.HTTPClient = http.DefaultClient } else if opts.HTTPClient.Timeout > 0 { - // remove timeout + var cancel context.CancelFunc + + ctx, cancel = context.WithTimeout(ctx, opts.HTTPClient.Timeout) + defer cancel() + opts.HTTPClient = &http.Client{ Transport: opts.HTTPClient.Transport, CheckRedirect: opts.HTTPClient.CheckRedirect, From f67b03bef8172d988704e0a9fc15bd6415b3940b Mon Sep 17 00:00:00 2001 From: swithek Date: Fri, 25 Sep 2020 09:48:19 +0300 Subject: [PATCH 335/519] Improve http client copying --- dial.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/dial.go b/dial.go index 849515e8..509882e0 100644 --- a/dial.go +++ b/dial.go @@ -79,11 +79,9 @@ func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) ( ctx, cancel = context.WithTimeout(ctx, opts.HTTPClient.Timeout) defer cancel() - opts.HTTPClient = &http.Client{ - Transport: opts.HTTPClient.Transport, - CheckRedirect: opts.HTTPClient.CheckRedirect, - Jar: opts.HTTPClient.Jar, - } + newClient := *opts.HTTPClient + newClient.Timeout = 0 + opts.HTTPClient = &newClient } if opts.HTTPHeader == nil { From c9f314abd11b749d43bb61fd214171f8bb4e4173 Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Thu, 26 Nov 2020 17:52:31 -0500 Subject: [PATCH 336/519] fmt: Remove unnecessary lines --- examples/echo/server.go | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/echo/server.go b/examples/echo/server.go index 308c4a5e..e9f70f03 100644 --- a/examples/echo/server.go +++ b/examples/echo/server.go @@ -16,7 +16,6 @@ import ( // It ensures the client speaks the echo subprotocol and // only allows one message every 100ms with a 10 message burst. type echoServer struct { - // logf controls where logs are sent. logf func(f string, v ...interface{}) } From e4c3b0f8168d619c279822ab882b8f15717041af Mon Sep 17 00:00:00 2001 From: Egor Gorbunov Date: Wed, 23 Dec 2020 16:30:26 +0300 Subject: [PATCH 337/519] Do not lower header tokens in headerTokens() (#273) HTTP header values, as opposed to header keys, are case sensitive, but implementation of headerTokens() before this patch would return lowered values always. This old behavior could lead to chromium (v87) WebSocket rejecting connnection because negotiated subprotocol, returned in Sec-WebSocket-Protocol header (lowered be headerToken() function) would not match one sent by client, in case client specified value with capital letters. --- accept.go | 11 ++++------- accept_test.go | 6 ++++++ dial.go | 4 ++-- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/accept.go b/accept.go index 66379b5d..18536bdb 100644 --- a/accept.go +++ b/accept.go @@ -159,13 +159,13 @@ func verifyClientRequest(w http.ResponseWriter, r *http.Request) (errCode int, _ return http.StatusUpgradeRequired, fmt.Errorf("WebSocket protocol violation: handshake request must be at least HTTP/1.1: %q", r.Proto) } - if !headerContainsToken(r.Header, "Connection", "Upgrade") { + if !headerContainsTokenIgnoreCase(r.Header, "Connection", "Upgrade") { w.Header().Set("Connection", "Upgrade") w.Header().Set("Upgrade", "websocket") return http.StatusUpgradeRequired, fmt.Errorf("WebSocket protocol violation: Connection header %q does not contain Upgrade", r.Header.Get("Connection")) } - if !headerContainsToken(r.Header, "Upgrade", "websocket") { + if !headerContainsTokenIgnoreCase(r.Header, "Upgrade", "websocket") { w.Header().Set("Connection", "Upgrade") w.Header().Set("Upgrade", "websocket") return http.StatusUpgradeRequired, fmt.Errorf("WebSocket protocol violation: Upgrade header %q does not contain websocket", r.Header.Get("Upgrade")) @@ -309,11 +309,9 @@ func acceptWebkitDeflate(w http.ResponseWriter, ext websocketExtension, mode Com return copts, nil } -func headerContainsToken(h http.Header, key, token string) bool { - token = strings.ToLower(token) - +func headerContainsTokenIgnoreCase(h http.Header, key, token string) bool { for _, t := range headerTokens(h, key) { - if t == token { + if strings.EqualFold(t, token) { return true } } @@ -354,7 +352,6 @@ func headerTokens(h http.Header, key string) []string { for _, v := range h[key] { v = strings.TrimSpace(v) for _, t := range strings.Split(v, ",") { - t = strings.ToLower(t) t = strings.TrimSpace(t) tokens = append(tokens, t) } diff --git a/accept_test.go b/accept_test.go index 9b18d8e1..e114d1ad 100644 --- a/accept_test.go +++ b/accept_test.go @@ -224,6 +224,12 @@ func Test_selectSubprotocol(t *testing.T) { serverProtocols: []string{"echo2", "echo3"}, negotiated: "echo3", }, + { + name: "clientCasePresered", + clientProtocols: []string{"Echo1"}, + serverProtocols: []string{"echo1"}, + negotiated: "Echo1", + }, } for _, tc := range testCases { diff --git a/dial.go b/dial.go index 509882e0..7a7787ff 100644 --- a/dial.go +++ b/dial.go @@ -194,11 +194,11 @@ func verifyServerResponse(opts *DialOptions, copts *compressionOptions, secWebSo return nil, fmt.Errorf("expected handshake response status code %v but got %v", http.StatusSwitchingProtocols, resp.StatusCode) } - if !headerContainsToken(resp.Header, "Connection", "Upgrade") { + if !headerContainsTokenIgnoreCase(resp.Header, "Connection", "Upgrade") { return nil, fmt.Errorf("WebSocket protocol violation: Connection header %q does not contain Upgrade", resp.Header.Get("Connection")) } - if !headerContainsToken(resp.Header, "Upgrade", "WebSocket") { + if !headerContainsTokenIgnoreCase(resp.Header, "Upgrade", "WebSocket") { return nil, fmt.Errorf("WebSocket protocol violation: Upgrade header %q does not contain websocket", resp.Header.Get("Upgrade")) } From fe1020d9fa5d2a910ac04df301eec6fa1e9aab58 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 9 Jan 2021 07:33:26 -0500 Subject: [PATCH 338/519] Fix incorrect &*var clones Thank you @icholy for identifying these in https://github.com/nhooyr/websocket/pull/259#issuecomment-702279421 --- dial.go | 3 ++- internal/test/wstest/pipe.go | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/dial.go b/dial.go index 7c959bff..a79b55e6 100644 --- a/dial.go +++ b/dial.go @@ -250,7 +250,8 @@ func verifyServerExtensions(copts *compressionOptions, h http.Header) (*compress return nil, fmt.Errorf("WebSocket protcol violation: unsupported extensions from server: %+v", exts[1:]) } - copts = &*copts + _copts := *copts + copts = &_copts for _, p := range ext.params { switch p { diff --git a/internal/test/wstest/pipe.go b/internal/test/wstest/pipe.go index 1534f316..f3d4c517 100644 --- a/internal/test/wstest/pipe.go +++ b/internal/test/wstest/pipe.go @@ -24,7 +24,8 @@ func Pipe(dialOpts *websocket.DialOptions, acceptOpts *websocket.AcceptOptions) if dialOpts == nil { dialOpts = &websocket.DialOptions{} } - dialOpts = &*dialOpts + _dialOpts := *dialOpts + dialOpts = &_dialOpts dialOpts.HTTPClient = &http.Client{ Transport: tt, } From e4fee52874b402afcb4cc7aa5cebddc393618800 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 9 Jan 2021 07:30:29 -0500 Subject: [PATCH 339/519] ci/test.sh: Work with BSD sed --- ci/test.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ci/test.sh b/ci/test.sh index 95ef7101..bd68b80e 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -5,9 +5,9 @@ main() { cd "$(dirname "$0")/.." go test -timeout=30m -covermode=atomic -coverprofile=ci/out/coverage.prof -coverpkg=./... "$@" ./... - sed -i '/stringer\.go/d' ci/out/coverage.prof - sed -i '/nhooyr.io\/websocket\/internal\/test/d' ci/out/coverage.prof - sed -i '/examples/d' ci/out/coverage.prof + sed -i.bak '/stringer\.go/d' ci/out/coverage.prof + sed -i.bak '/nhooyr.io\/websocket\/internal\/test/d' ci/out/coverage.prof + sed -i.bak '/examples/d' ci/out/coverage.prof # Last line is the total coverage. go tool cover -func ci/out/coverage.prof | tail -n1 From 3b20a49a2c6fc9aa28be7d5296afe2079ff0e537 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 9 Jan 2021 08:01:54 -0500 Subject: [PATCH 340/519] Add back documentation on separate idle and read timeout Closes #87 --- read.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/read.go b/read.go index afd08cc7..87151dcb 100644 --- a/read.go +++ b/read.go @@ -26,6 +26,11 @@ import ( // Call CloseRead if you do not expect any data messages from the peer. // // Only one Reader may be open at a time. +// +// If you need a separate timeout on the Reader call and the Read itself, +// use time.AfterFunc to cancel the context passed in. +// See https://github.com/nhooyr/websocket/issues/87#issue-451703332 +// Most users should not need this. func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) { return c.reader(ctx) } From 29f527b17fdcba1ecd29b83f55dc7ff1114d2102 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 9 Jan 2021 08:03:11 -0500 Subject: [PATCH 341/519] Remove ExampleGrace for now to avoid confusion --- example_test.go | 52 ------------------------------------------------- 1 file changed, 52 deletions(-) diff --git a/example_test.go b/example_test.go index d44bd537..2e55eb96 100644 --- a/example_test.go +++ b/example_test.go @@ -160,58 +160,6 @@ func ExampleConn_Ping() { c.Close(websocket.StatusNormalClosure, "") } -// This example demonstrates how to create a WebSocket server -// that gracefully exits when sent a signal. -// -// It starts a WebSocket server that keeps every connection open -// for 10 seconds. -// If you CTRL+C while a connection is open, it will wait at most 30s -// for all connections to terminate before shutting down. -// func ExampleGrace() { -// fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { -// c, err := websocket.Accept(w, r, nil) -// if err != nil { -// log.Println(err) -// return -// } -// defer c.Close(websocket.StatusInternalError, "the sky is falling") -// -// ctx := c.CloseRead(r.Context()) -// select { -// case <-ctx.Done(): -// case <-time.After(time.Second * 10): -// } -// -// c.Close(websocket.StatusNormalClosure, "") -// }) -// -// var g websocket.Grace -// s := &http.Server{ -// Handler: g.Handler(fn), -// ReadTimeout: time.Second * 15, -// WriteTimeout: time.Second * 15, -// } -// -// errc := make(chan error, 1) -// go func() { -// errc <- s.ListenAndServe() -// }() -// -// sigs := make(chan os.Signal, 1) -// signal.Notify(sigs, os.Interrupt) -// select { -// case err := <-errc: -// log.Printf("failed to listen and serve: %v", err) -// case sig := <-sigs: -// log.Printf("terminating: %v", sig) -// } -// -// ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) -// defer cancel() -// s.Shutdown(ctx) -// g.Shutdown(ctx) -// } - // This example demonstrates full stack chat with an automated test. func Example_fullStackChat() { // https://github.com/nhooyr/websocket/tree/master/examples/chat From 085d46c46dde55c3ffe776ebb953ba5e93559c01 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 9 Jan 2021 08:09:58 -0500 Subject: [PATCH 342/519] Document context expirations wart Closes #242 --- conn.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/conn.go b/conn.go index 1a57c656..beb26cec 100644 --- a/conn.go +++ b/conn.go @@ -37,6 +37,9 @@ const ( // // On any error from any method, the connection is closed // with an appropriate reason. +// +// This applies to context expirations as well unfortunately. +// See https://github.com/nhooyr/websocket/issues/242#issuecomment-633182220 type Conn struct { subprotocol string rwc io.ReadWriteCloser From ea87744105d79f972e58404bb46791b97fc3f314 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 4 Jul 2020 19:34:24 -0400 Subject: [PATCH 343/519] netconn: Disable read limit on WebSocket Closes #245 --- netconn.go | 4 ++++ read.go | 21 +++++++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/netconn.go b/netconn.go index 1664e29b..c6f8dc13 100644 --- a/netconn.go +++ b/netconn.go @@ -38,7 +38,11 @@ import ( // // A received StatusNormalClosure or StatusGoingAway close frame will be translated to // io.EOF when reading. +// +// Furthermore, the ReadLimit is set to -1 to disable it. func NetConn(ctx context.Context, c *Conn, msgType MessageType) net.Conn { + c.SetReadLimit(-1) + nc := &netConn{ c: c, msgType: msgType, diff --git a/read.go b/read.go index 87151dcb..c4234f20 100644 --- a/read.go +++ b/read.go @@ -74,10 +74,16 @@ func (c *Conn) CloseRead(ctx context.Context) context.Context { // By default, the connection has a message read limit of 32768 bytes. // // When the limit is hit, the connection will be closed with StatusMessageTooBig. +// +// Set to -1 to disable. func (c *Conn) SetReadLimit(n int64) { - // We add read one more byte than the limit in case - // there is a fin frame that needs to be read. - c.msgReader.limitReader.limit.Store(n + 1) + if n >= 0 { + // We read one more byte than the limit in case + // there is a fin frame that needs to be read. + n++ + } + + c.msgReader.limitReader.limit.Store(n) } const defaultReadLimit = 32768 @@ -455,7 +461,11 @@ func (lr *limitReader) reset(r io.Reader) { } func (lr *limitReader) Read(p []byte) (int, error) { - if lr.n <= 0 { + if lr.n < 0 { + return lr.r.Read(p) + } + + if lr.n == 0 { err := fmt.Errorf("read limited at %v bytes", lr.limit.Load()) lr.c.writeError(StatusMessageTooBig, err) return 0, err @@ -466,6 +476,9 @@ func (lr *limitReader) Read(p []byte) (int, error) { } n, err := lr.r.Read(p) lr.n -= int64(n) + if lr.n < 0 { + lr.n = 0 + } return n, err } From 11af7f8bc0b3c3125a18ff0a0008b95e8a1e50e1 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 9 Jan 2021 08:14:49 -0500 Subject: [PATCH 344/519] netconn: Add test for disabled read limit --- conn_test.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/conn_test.go b/conn_test.go index 9c85459e..3ca810c5 100644 --- a/conn_test.go +++ b/conn_test.go @@ -208,6 +208,37 @@ func TestConn(t *testing.T) { } }) + t.Run("netConn/readLimit", func(t *testing.T) { + tt, c1, c2 := newConnTest(t, nil, nil) + + n1 := websocket.NetConn(tt.ctx, c1, websocket.MessageBinary) + n2 := websocket.NetConn(tt.ctx, c2, websocket.MessageBinary) + + s := strings.Repeat("papa", 1 << 20) + errs := xsync.Go(func() error { + _, err := n2.Write([]byte(s)) + if err != nil { + return err + } + return n2.Close() + }) + + b, err := ioutil.ReadAll(n1) + assert.Success(t, err) + + _, err = n1.Read(nil) + assert.Equal(t, "read error", err, io.EOF) + + select { + case err := <-errs: + assert.Success(t, err) + case <-tt.ctx.Done(): + t.Fatal(tt.ctx.Err()) + } + + assert.Equal(t, "read msg", s, string(b)) + }) + t.Run("wsjson", func(t *testing.T) { tt, c1, c2 := newConnTest(t, nil, nil) From 482f5845b6e345293575c727253dc7b46bce4905 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 9 Jan 2021 08:22:31 -0500 Subject: [PATCH 345/519] netconn.go: Cleanup contexts on close Updates #255 --- netconn.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/netconn.go b/netconn.go index c6f8dc13..aea1a02d 100644 --- a/netconn.go +++ b/netconn.go @@ -50,16 +50,14 @@ func NetConn(ctx context.Context, c *Conn, msgType MessageType) net.Conn { writeMu: newMu(c), } - var writeCancel context.CancelFunc - nc.writeCtx, writeCancel = context.WithCancel(ctx) - var readCancel context.CancelFunc - nc.readCtx, readCancel = context.WithCancel(ctx) + nc.writeCtx, nc.writeCancel = context.WithCancel(ctx) + nc.readCtx, nc.readCancel = context.WithCancel(ctx) nc.writeTimer = time.AfterFunc(math.MaxInt64, func() { if !nc.writeMu.tryLock() { // If the lock cannot be acquired, then there is an // active write goroutine and so we should cancel the context. - writeCancel() + nc.writeCancel() return } defer nc.writeMu.unlock() @@ -75,7 +73,7 @@ func NetConn(ctx context.Context, c *Conn, msgType MessageType) net.Conn { if !nc.readMu.tryLock() { // If the lock cannot be acquired, then there is an // active read goroutine and so we should cancel the context. - readCancel() + nc.readCancel() return } defer nc.readMu.unlock() @@ -98,11 +96,13 @@ type netConn struct { writeMu *mu writeExpired int64 writeCtx context.Context + writeCancel context.CancelFunc readTimer *time.Timer readMu *mu readExpired int64 readCtx context.Context + readCancel context.CancelFunc readEOFed bool reader io.Reader } @@ -111,7 +111,9 @@ var _ net.Conn = &netConn{} func (nc *netConn) Close() error { nc.writeTimer.Stop() + nc.writeCancel() nc.readTimer.Stop() + nc.readCancel() return nc.c.Close(StatusNormalClosure, "") } From 29251d03c03fc0f6cad649a50c31c608db8999ae Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 9 Jan 2021 08:35:37 -0500 Subject: [PATCH 346/519] accept.go: Improve unauthorized origin error message Closes #247 --- accept.go | 5 ++++- accept_test.go | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/accept.go b/accept.go index 6e1f494e..542b61e8 100644 --- a/accept.go +++ b/accept.go @@ -215,7 +215,10 @@ func authenticateOrigin(r *http.Request, originHosts []string) error { return nil } } - return fmt.Errorf("request Origin %q is not authorized for Host %q", origin, r.Host) + if u.Host == "" { + return fmt.Errorf("request Origin %q is not a valid URL with a host", origin) + } + return fmt.Errorf("request Origin %q is not authorized for Host %q", u.Host, r.Host) } func match(pattern, s string) (bool, error) { diff --git a/accept_test.go b/accept_test.go index d19f54e1..67ece253 100644 --- a/accept_test.go +++ b/accept_test.go @@ -39,7 +39,23 @@ func TestAccept(t *testing.T) { r.Header.Set("Origin", "harhar.com") _, err := Accept(w, r, nil) - assert.Contains(t, err, `request Origin "harhar.com" is not authorized for Host`) + assert.Contains(t, err, `request Origin "harhar.com" is not a valid URL with a host`) + }) + + // #247 + t.Run("unauthorizedOriginErrorMessage", func(t *testing.T) { + t.Parallel() + + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/", nil) + r.Header.Set("Connection", "Upgrade") + r.Header.Set("Upgrade", "websocket") + r.Header.Set("Sec-WebSocket-Version", "13") + r.Header.Set("Sec-WebSocket-Key", "meow123") + r.Header.Set("Origin", "https://harhar.com") + + _, err := Accept(w, r, nil) + assert.Contains(t, err, `request Origin "harhar.com" is not authorized for Host "example.com"`) }) t.Run("badCompression", func(t *testing.T) { From 7c0c0470590124d0ddd3f334765402c0605549f3 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 9 Jan 2021 08:36:33 -0500 Subject: [PATCH 347/519] Fix formatting --- conn_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conn_test.go b/conn_test.go index 3ca810c5..0fbd1740 100644 --- a/conn_test.go +++ b/conn_test.go @@ -214,7 +214,7 @@ func TestConn(t *testing.T) { n1 := websocket.NetConn(tt.ctx, c1, websocket.MessageBinary) n2 := websocket.NetConn(tt.ctx, c2, websocket.MessageBinary) - s := strings.Repeat("papa", 1 << 20) + s := strings.Repeat("papa", 1<<20) errs := xsync.Go(func() error { _, err := n2.Write([]byte(s)) if err != nil { From 6840778f54a29b77a58c43e7f5c58c4609ab10f2 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 9 Jan 2021 08:39:23 -0500 Subject: [PATCH 348/519] README.md: Update coverage --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8420bdbd..0ae739a0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # websocket [![godoc](https://godoc.org/nhooyr.io/websocket?status.svg)](https://pkg.go.dev/nhooyr.io/websocket) -[![coverage](https://img.shields.io/badge/coverage-88%25-success)](https://nhooyrio-websocket-coverage.netlify.app) +[![coverage](https://img.shields.io/badge/coverage-86%25-success)](https://nhooyrio-websocket-coverage.netlify.app) websocket is a minimal and idiomatic WebSocket library for Go. From 65dfbdd4c1106a9529bbf374aa92ea97a4456c2a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 9 Jan 2021 08:50:17 -0500 Subject: [PATCH 349/519] wasm: Add dial timeout test --- ws_js_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ws_js_test.go b/ws_js_test.go index e6be6181..ba98b9a0 100644 --- a/ws_js_test.go +++ b/ws_js_test.go @@ -36,3 +36,19 @@ func TestWasm(t *testing.T) { err = c.Close(websocket.StatusNormalClosure, "") assert.Success(t, err) } + +func TestWasmDialTimeout(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond) + defer cancel() + + beforeDial := time.Now() + _, _, err := websocket.Dial(ctx, "ws://example.com:9893", &websocket.DialOptions{ + Subprotocols: []string{"echo"}, + }) + assert.Error(t, err) + if time.Since(beforeDial) >= time.Second { + t.Fatal("wasm context dial timeout is not working", time.Since(beforeDial)) + } +} From 129d3035f688f8f1c8a03d65e874e15860d21365 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 7 Apr 2021 09:31:44 -0600 Subject: [PATCH 350/519] Fix DOS attack from malicious pongs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A double channel close panic was possible if a peer sent back multiple pongs for every ping. If the second pong arrived before the ping goroutine deleted its channel from the map, the channel would be closed twice and so a panic would ensue. This fixes that by having the read goroutine send on the ping goroutine's channel rather than closing it. Reported via email by Tibor Kálmán @kalmant Please update to the new release ASAP! --- conn_notjs.go | 2 +- read.go | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/conn_notjs.go b/conn_notjs.go index bb2eb22f..0c85ab77 100644 --- a/conn_notjs.go +++ b/conn_notjs.go @@ -189,7 +189,7 @@ func (c *Conn) Ping(ctx context.Context) error { } func (c *Conn) ping(ctx context.Context, p string) error { - pong := make(chan struct{}) + pong := make(chan struct{}, 1) c.activePingsMu.Lock() c.activePings[p] = pong diff --git a/read.go b/read.go index afd08cc7..ae05cf93 100644 --- a/read.go +++ b/read.go @@ -271,7 +271,10 @@ func (c *Conn) handleControl(ctx context.Context, h header) (err error) { pong, ok := c.activePings[string(b)] c.activePingsMu.Unlock() if ok { - close(pong) + select { + case pong <- struct{}{}: + default: + } } return nil } From b0d7a2712f3b3c9015a33065b556b6d75414d25a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 7 Apr 2021 09:55:44 -0600 Subject: [PATCH 351/519] Fix CI --- ci/container/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/container/Dockerfile b/ci/container/Dockerfile index fd008788..0c6c2a54 100644 --- a/ci/container/Dockerfile +++ b/ci/container/Dockerfile @@ -10,5 +10,5 @@ RUN go get golang.org/x/tools/cmd/stringer RUN go get golang.org/x/lint/golint RUN go get github.com/agnivade/wasmbrowsertest -RUN npm install -g prettier -RUN npm install -g netlify-cli +RUN npm --unsafe-perm=true install -g prettier +RUN npm --unsafe-perm=true install -g netlify-cli From 8dee580a7f74cf1713400307b4eee514b927870f Mon Sep 17 00:00:00 2001 From: arthmis Date: Fri, 9 Apr 2021 20:03:28 -0400 Subject: [PATCH 352/519] Fix grammar (#295) Co-authored-by: lazypassion <25536767+lazypassion@users.noreply.github.com> --- read.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/read.go b/read.go index ae05cf93..89a00988 100644 --- a/read.go +++ b/read.go @@ -16,7 +16,7 @@ import ( "nhooyr.io/websocket/internal/xsync" ) -// Reader reads from the connection until until there is a WebSocket +// Reader reads from the connection until there is a WebSocket // data message to be read. It will handle ping, pong and close frames as appropriate. // // It returns the type of the message and an io.Reader to read it. From 39bb58838b7a3608dad87e578b45cc0f79880c9f Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 13 Dec 2022 02:42:55 -0800 Subject: [PATCH 353/519] README.md: Add note --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index df20c581..28a7ce4e 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,11 @@ websocket is a minimal and idiomatic WebSocket library for Go. +> **note**: I haven't been responsive for questions/reports on the issue tracker but I do +> read through and I don't believe there are any outstanding bugs. There are certainly +> some nice to haves that I should merge in/figure out but nothing critical. I haven't +> given up on adding new features and cleaning up the code further, just been busy. + ## Install ```bash From 78f81f3a63504ed589c003d3755c133e1b029969 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 13 Dec 2022 03:12:20 -0800 Subject: [PATCH 354/519] REAME: Update note --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 28a7ce4e..c8cb2271 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ websocket is a minimal and idiomatic WebSocket library for Go. > read through and I don't believe there are any outstanding bugs. There are certainly > some nice to haves that I should merge in/figure out but nothing critical. I haven't > given up on adding new features and cleaning up the code further, just been busy. +> Should anything critical arise, I will fix it. ## Install From 14fb98eba64eeb5e9d06a88b98c47ae924ac82b4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 13 Dec 2022 14:35:01 -0800 Subject: [PATCH 355/519] README: Further update --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c8cb2271..15e3c8e6 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@ websocket is a minimal and idiomatic WebSocket library for Go. > **note**: I haven't been responsive for questions/reports on the issue tracker but I do -> read through and I don't believe there are any outstanding bugs. There are certainly -> some nice to haves that I should merge in/figure out but nothing critical. I haven't -> given up on adding new features and cleaning up the code further, just been busy. -> Should anything critical arise, I will fix it. +> read through and there are no outstanding bugs. There are certainly some nice to haves +> that I should merge in/figure out but nothing critical. I haven't given up on adding new +> features and cleaning up the code further, just been busy. Should anything critical +> arise, I will fix it. ## Install From 7fd613642282944805ba6f4f30bd5501b1f74e99 Mon Sep 17 00:00:00 2001 From: Gus Eggert Date: Mon, 30 Jan 2023 08:02:52 -0500 Subject: [PATCH 356/519] Fix dial panic when ctx is nil When the ctx is nil, http.NewRequestWithContext returns a "net/http: nil Context" error and a nil request. In this case, the dial function panics because it assumes the req is never nil. This checks the returning error and returns it, so that callers get an error instead of a panic in that scenario. --- dial.go | 5 ++++- dial_test.go | 22 ++++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/dial.go b/dial.go index 7a7787ff..0ae0d570 100644 --- a/dial.go +++ b/dial.go @@ -157,7 +157,10 @@ func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, copts return nil, fmt.Errorf("unexpected url scheme: %q", u.Scheme) } - req, _ := http.NewRequestWithContext(ctx, "GET", u.String(), nil) + req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) + if err != nil { + return nil, fmt.Errorf("failed to build HTTP request: %w", err) + } req.Header = opts.HTTPHeader.Clone() req.Header.Set("Connection", "Upgrade") req.Header.Set("Upgrade", "websocket") diff --git a/dial_test.go b/dial_test.go index 28c255c6..80ba9a3d 100644 --- a/dial_test.go +++ b/dial_test.go @@ -23,10 +23,11 @@ func TestBadDials(t *testing.T) { t.Parallel() testCases := []struct { - name string - url string - opts *DialOptions - rand readerFunc + name string + url string + opts *DialOptions + rand readerFunc + nilCtx bool }{ { name: "badURL", @@ -46,6 +47,11 @@ func TestBadDials(t *testing.T) { return 0, io.EOF }, }, + { + name: "nilContext", + url: "http://localhost", + nilCtx: true, + }, } for _, tc := range testCases { @@ -53,8 +59,12 @@ func TestBadDials(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - defer cancel() + var ctx context.Context + var cancel func() + if !tc.nilCtx { + ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + } if tc.rand == nil { tc.rand = rand.Reader.Read From e2bb5beb7b429305ba25e9bcf9365fda3406737f Mon Sep 17 00:00:00 2001 From: Teddy Okello <37796862+keystroke3@users.noreply.github.com> Date: Sun, 26 Feb 2023 00:25:22 +0300 Subject: [PATCH 357/519] Migrate from deprecated `io/ioutil` --- autobahn_test.go | 8 ++++---- conn_test.go | 5 ++--- dial.go | 5 ++--- dial_test.go | 5 ++--- doc.go | 10 +++++----- examples/chat/chat.go | 4 ++-- read.go | 3 +-- 7 files changed, 18 insertions(+), 22 deletions(-) diff --git a/autobahn_test.go b/autobahn_test.go index e56a4912..1bfb1419 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -6,7 +6,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net" "os" "os/exec" @@ -146,7 +146,7 @@ func wstestCaseCount(ctx context.Context, url string) (cases int, err error) { if err != nil { return 0, err } - b, err := ioutil.ReadAll(r) + b, err := io.ReadAll(r) if err != nil { return 0, err } @@ -161,7 +161,7 @@ func wstestCaseCount(ctx context.Context, url string) (cases int, err error) { } func checkWSTestIndex(t *testing.T, path string) { - wstestOut, err := ioutil.ReadFile(path) + wstestOut, err := os.ReadFile(path) assert.Success(t, err) var indexJSON map[string]map[string]struct { @@ -206,7 +206,7 @@ func unusedListenAddr() (_ string, err error) { } func tempJSONFile(v interface{}) (string, error) { - f, err := ioutil.TempFile("", "temp.json") + f, err := os.CreateTemp("", "temp.json") if err != nil { return "", fmt.Errorf("temp file: %w", err) } diff --git a/conn_test.go b/conn_test.go index c2c41292..d9723686 100644 --- a/conn_test.go +++ b/conn_test.go @@ -7,7 +7,6 @@ import ( "context" "fmt" "io" - "io/ioutil" "net/http" "net/http/httptest" "os" @@ -174,7 +173,7 @@ func TestConn(t *testing.T) { return n2.Close() }) - b, err := ioutil.ReadAll(n1) + b, err := io.ReadAll(n1) assert.Success(t, err) _, err = n1.Read(nil) @@ -205,7 +204,7 @@ func TestConn(t *testing.T) { return nil }) - _, err := ioutil.ReadAll(n1) + _, err := io.ReadAll(n1) assert.Contains(t, err, `unexpected frame type read (expected MessageBinary): MessageText`) select { diff --git a/dial.go b/dial.go index 7a7787ff..2889a37b 100644 --- a/dial.go +++ b/dial.go @@ -10,7 +10,6 @@ import ( "encoding/base64" "fmt" "io" - "io/ioutil" "net/http" "net/url" "strings" @@ -114,9 +113,9 @@ func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) ( }) defer timer.Stop() - b, _ := ioutil.ReadAll(r) + b, _ := io.ReadAll(r) respBody.Close() - resp.Body = ioutil.NopCloser(bytes.NewReader(b)) + resp.Body = io.NopCloser(bytes.NewReader(b)) } }() diff --git a/dial_test.go b/dial_test.go index 28c255c6..89ca3075 100644 --- a/dial_test.go +++ b/dial_test.go @@ -6,7 +6,6 @@ import ( "context" "crypto/rand" "io" - "io/ioutil" "net/http" "net/http/httptest" "strings" @@ -75,7 +74,7 @@ func TestBadDials(t *testing.T) { _, _, err := Dial(ctx, "ws://example.com", &DialOptions{ HTTPClient: mockHTTPClient(func(*http.Request) (*http.Response, error) { return &http.Response{ - Body: ioutil.NopCloser(strings.NewReader("hi")), + Body: io.NopCloser(strings.NewReader("hi")), }, nil }), }) @@ -97,7 +96,7 @@ func TestBadDials(t *testing.T) { return &http.Response{ StatusCode: http.StatusSwitchingProtocols, Header: h, - Body: ioutil.NopCloser(strings.NewReader("hi")), + Body: io.NopCloser(strings.NewReader("hi")), }, nil } diff --git a/doc.go b/doc.go index efa920e3..43c7d92d 100644 --- a/doc.go +++ b/doc.go @@ -16,7 +16,7 @@ // // More documentation at https://nhooyr.io/websocket. // -// Wasm +// # Wasm // // The client side supports compiling to Wasm. // It wraps the WebSocket browser API. @@ -25,8 +25,8 @@ // // Some important caveats to be aware of: // -// - Accept always errors out -// - Conn.Ping is no-op -// - HTTPClient, HTTPHeader and CompressionMode in DialOptions are no-op -// - *http.Response from Dial is &http.Response{} with a 101 status code on success +// - Accept always errors out +// - Conn.Ping is no-op +// - HTTPClient, HTTPHeader and CompressionMode in DialOptions are no-op +// - *http.Response from Dial is &http.Response{} with a 101 status code on success package websocket // import "nhooyr.io/websocket" diff --git a/examples/chat/chat.go b/examples/chat/chat.go index 532e50f5..9d393d87 100644 --- a/examples/chat/chat.go +++ b/examples/chat/chat.go @@ -3,7 +3,7 @@ package main import ( "context" "errors" - "io/ioutil" + "io" "log" "net/http" "sync" @@ -98,7 +98,7 @@ func (cs *chatServer) publishHandler(w http.ResponseWriter, r *http.Request) { return } body := http.MaxBytesReader(w, r.Body, 8192) - msg, err := ioutil.ReadAll(body) + msg, err := io.ReadAll(body) if err != nil { http.Error(w, http.StatusText(http.StatusRequestEntityTooLarge), http.StatusRequestEntityTooLarge) return diff --git a/read.go b/read.go index 89a00988..97a4f987 100644 --- a/read.go +++ b/read.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "strings" "time" @@ -38,7 +37,7 @@ func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { return 0, nil, err } - b, err := ioutil.ReadAll(r) + b, err := io.ReadAll(r) return typ, b, err } From 54809d605a9cd025bcd7336308ec0ec00c4b879b Mon Sep 17 00:00:00 2001 From: Gus Eggert Date: Tue, 7 Mar 2023 09:51:46 -0500 Subject: [PATCH 358/519] Update err message when dial ctx is nil Co-authored-by: Anmol Sethi --- dial.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dial.go b/dial.go index 0ae0d570..8634a5d6 100644 --- a/dial.go +++ b/dial.go @@ -159,7 +159,7 @@ func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, copts req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) if err != nil { - return nil, fmt.Errorf("failed to build HTTP request: %w", err) + return nil, fmt.Errorf("http.NewRequestWithContext failed: %w", err) } req.Header = opts.HTTPHeader.Clone() req.Header.Set("Connection", "Upgrade") From 6ead6aaf8eb5c3a5c7f533109e0a3baab763289a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 7 Apr 2023 07:59:28 -0700 Subject: [PATCH 359/519] autobahn_test: Use docker to avoid issues with python2 EOL Also ran gofmt on everything. Thanks again @paralin. #334 Co-authored-by: Christian Stewart --- accept.go | 1 + accept_test.go | 1 + autobahn_test.go | 62 +++++++++++++++++++++++++++++------- close.go | 1 + close_test.go | 1 + compress.go | 1 + compress_test.go | 1 + conn.go | 1 + conn_test.go | 1 + dial.go | 1 + dial_test.go | 1 + doc.go | 11 ++++--- export_test.go | 1 + frame_test.go | 1 + internal/test/wstest/echo.go | 2 +- internal/test/wstest/pipe.go | 1 + internal/wsjs/wsjs_js.go | 1 + make.sh | 17 ++++++++++ read.go | 1 + write.go | 1 + 20 files changed, 90 insertions(+), 18 deletions(-) create mode 100755 make.sh diff --git a/accept.go b/accept.go index 542b61e8..d918aab5 100644 --- a/accept.go +++ b/accept.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/accept_test.go b/accept_test.go index 67ece253..ae17c0b4 100644 --- a/accept_test.go +++ b/accept_test.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/autobahn_test.go b/autobahn_test.go index 7c735a38..4df4b66b 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket_test @@ -33,6 +34,12 @@ var excludedAutobahnCases = []string{ var autobahnCases = []string{"*"} +// Used to run individual test cases. autobahnCases runs only those cases matched +// and not excluded by excludedAutobahnCases. Adding cases here means excludedAutobahnCases +// is niled. +// TODO: +var forceAutobahnCases = []string{} + func TestAutobahn(t *testing.T) { t.Parallel() @@ -43,16 +50,18 @@ func TestAutobahn(t *testing.T) { if os.Getenv("AUTOBAHN") == "fast" { // These are the slow tests. excludedAutobahnCases = append(excludedAutobahnCases, - "9.*", "13.*", "12.*", + "9.*", "12.*", "13.*", ) } - ctx, cancel := context.WithTimeout(context.Background(), time.Minute*15) + ctx, cancel := context.WithTimeout(context.Background(), time.Hour) defer cancel() - wstestURL, closeFn, err := wstestClientServer(ctx) + wstestURL, closeFn, err := wstestServer(ctx) assert.Success(t, err) - defer closeFn() + defer func() { + assert.Success(t, closeFn()) + }() err = waitWS(ctx, wstestURL) assert.Success(t, err) @@ -100,17 +109,24 @@ func waitWS(ctx context.Context, url string) error { return ctx.Err() } -func wstestClientServer(ctx context.Context) (url string, closeFn func(), err error) { +// TODO: Let docker pick the port and use docker port to find it. +// Does mean we can't use -i but that's fine. +func wstestServer(ctx context.Context) (url string, closeFn func() error, err error) { serverAddr, err := unusedListenAddr() if err != nil { return "", nil, err } + _, serverPort, err := net.SplitHostPort(serverAddr) + if err != nil { + return "", nil, err + } url = "ws://" + serverAddr + const outDir = "ci/out/wstestClientReports" specFile, err := tempJSONFile(map[string]interface{}{ "url": url, - "outdir": "ci/out/wstestClientReports", + "outdir": outDir, "cases": autobahnCases, "exclude-cases": excludedAutobahnCases, }) @@ -118,26 +134,48 @@ func wstestClientServer(ctx context.Context) (url string, closeFn func(), err er return "", nil, fmt.Errorf("failed to write spec: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), time.Minute*15) + ctx, cancel := context.WithTimeout(ctx, time.Hour) defer func() { if err != nil { cancel() } }() - args := []string{"--mode", "fuzzingserver", "--spec", specFile, + wd, err := os.Getwd() + if err != nil { + return "", nil, err + } + + var args []string + args = append(args, "run", "-i", "--rm", + "-v", fmt.Sprintf("%s:%[1]s", specFile), + "-v", fmt.Sprintf("%s/ci:/ci", wd), + fmt.Sprintf("-p=%s:%s", serverAddr, serverPort), + "crossbario/autobahn-testsuite", + ) + args = append(args, "wstest", "--mode", "fuzzingserver", "--spec", specFile, // Disables some server that runs as part of fuzzingserver mode. // See https://github.com/crossbario/autobahn-testsuite/blob/058db3a36b7c3a1edf68c282307c6b899ca4857f/autobahntestsuite/autobahntestsuite/wstest.py#L124 "--webport=0", - } - wstest := exec.CommandContext(ctx, "wstest", args...) + ) + fmt.Println(strings.Join(args, " ")) + // TODO: pull image in advance + wstest := exec.CommandContext(ctx, "docker", args...) + // TODO: log to *testing.T + wstest.Stdout = os.Stdout + wstest.Stderr = os.Stderr err = wstest.Start() if err != nil { return "", nil, fmt.Errorf("failed to start wstest: %w", err) } - return url, func() { - wstest.Process.Kill() + // TODO: kill + return url, func() error { + err = wstest.Process.Kill() + if err != nil { + return fmt.Errorf("failed to kill wstest: %w", err) + } + return nil }, nil } diff --git a/close.go b/close.go index d76dc2f4..eab49a8f 100644 --- a/close.go +++ b/close.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/close_test.go b/close_test.go index 00a48d9e..6bf3c256 100644 --- a/close_test.go +++ b/close_test.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/compress.go b/compress.go index f49d9e5d..68734471 100644 --- a/compress.go +++ b/compress.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/compress_test.go b/compress_test.go index 2c4c896c..7b0e3a68 100644 --- a/compress_test.go +++ b/compress_test.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/conn.go b/conn.go index beb26cec..25b5a202 100644 --- a/conn.go +++ b/conn.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/conn_test.go b/conn_test.go index 0fbd1740..19961d18 100644 --- a/conn_test.go +++ b/conn_test.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket_test diff --git a/dial.go b/dial.go index a79b55e6..7e77d6e3 100644 --- a/dial.go +++ b/dial.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/dial_test.go b/dial_test.go index 28c255c6..e5f8ab3d 100644 --- a/dial_test.go +++ b/dial_test.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/doc.go b/doc.go index efa920e3..a2b873c7 100644 --- a/doc.go +++ b/doc.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js // Package websocket implements the RFC 6455 WebSocket protocol. @@ -16,7 +17,7 @@ // // More documentation at https://nhooyr.io/websocket. // -// Wasm +// # Wasm // // The client side supports compiling to Wasm. // It wraps the WebSocket browser API. @@ -25,8 +26,8 @@ // // Some important caveats to be aware of: // -// - Accept always errors out -// - Conn.Ping is no-op -// - HTTPClient, HTTPHeader and CompressionMode in DialOptions are no-op -// - *http.Response from Dial is &http.Response{} with a 101 status code on success +// - Accept always errors out +// - Conn.Ping is no-op +// - HTTPClient, HTTPHeader and CompressionMode in DialOptions are no-op +// - *http.Response from Dial is &http.Response{} with a 101 status code on success package websocket // import "nhooyr.io/websocket" diff --git a/export_test.go b/export_test.go index 88b82c9f..d618a154 100644 --- a/export_test.go +++ b/export_test.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/frame_test.go b/frame_test.go index 76826248..93ad8b5f 100644 --- a/frame_test.go +++ b/frame_test.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/internal/test/wstest/echo.go b/internal/test/wstest/echo.go index 8f4e47c8..0938a138 100644 --- a/internal/test/wstest/echo.go +++ b/internal/test/wstest/echo.go @@ -21,7 +21,7 @@ func EchoLoop(ctx context.Context, c *websocket.Conn) error { c.SetReadLimit(1 << 30) - ctx, cancel := context.WithTimeout(ctx, time.Minute) + ctx, cancel := context.WithTimeout(ctx, time.Minute*5) defer cancel() b := make([]byte, 32<<10) diff --git a/internal/test/wstest/pipe.go b/internal/test/wstest/pipe.go index f3d4c517..8e1deb47 100644 --- a/internal/test/wstest/pipe.go +++ b/internal/test/wstest/pipe.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package wstest diff --git a/internal/wsjs/wsjs_js.go b/internal/wsjs/wsjs_js.go index 26ffb456..88e8f43f 100644 --- a/internal/wsjs/wsjs_js.go +++ b/internal/wsjs/wsjs_js.go @@ -1,3 +1,4 @@ +//go:build js // +build js // Package wsjs implements typed access to the browser javascript WebSocket API. diff --git a/make.sh b/make.sh new file mode 100755 index 00000000..578203cd --- /dev/null +++ b/make.sh @@ -0,0 +1,17 @@ +#!/bin/sh +set -eu + +cd "$(dirname "$0")" + +fmt() { + go mod tidy + gofmt -s -w . + goimports -w "-local=$(go list -m)" . +} + +if ! command -v wasmbrowsertest >/dev/null; then + go install github.com/agnivade/wasmbrowsertest@latest +fi + +fmt +go test -race --timeout=1h ./... "$@" diff --git a/read.go b/read.go index c4234f20..19727fda 100644 --- a/read.go +++ b/read.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/write.go b/write.go index 58bfdf9a..7921eac9 100644 --- a/write.go +++ b/write.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket From 7c87cb5feb5c276cd7cce95f6b6a8e24cdd206b5 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 27 Sep 2023 23:37:06 -0700 Subject: [PATCH 360/519] Fix DOS attack from malicious pongs Cherry picked from master at 129d3035f688f8f1c8a03d65e874e15860d21365 --- conn.go | 2 +- read.go | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/conn.go b/conn.go index 25b5a202..ab37248e 100644 --- a/conn.go +++ b/conn.go @@ -205,7 +205,7 @@ func (c *Conn) Ping(ctx context.Context) error { } func (c *Conn) ping(ctx context.Context, p string) error { - pong := make(chan struct{}) + pong := make(chan struct{}, 1) c.activePingsMu.Lock() c.activePings[p] = pong diff --git a/read.go b/read.go index 19727fda..98766d7d 100644 --- a/read.go +++ b/read.go @@ -283,7 +283,10 @@ func (c *Conn) handleControl(ctx context.Context, h header) (err error) { pong, ok := c.activePings[string(b)] c.activePingsMu.Unlock() if ok { - close(pong) + select { + case pong <- struct{}{}: + default: + } } return nil } From b8f6512ef2184b9e53248e89b2fed8f79ffb8068 Mon Sep 17 00:00:00 2001 From: arthmis Date: Fri, 9 Apr 2021 20:03:28 -0400 Subject: [PATCH 361/519] Fix grammar (#295) Co-authored-by: lazypassion <25536767+lazypassion@users.noreply.github.com> --- read.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/read.go b/read.go index 98766d7d..87e93460 100644 --- a/read.go +++ b/read.go @@ -17,7 +17,7 @@ import ( "nhooyr.io/websocket/internal/xsync" ) -// Reader reads from the connection until until there is a WebSocket +// Reader reads from the connection until there is a WebSocket // data message to be read. It will handle ping, pong and close frames as appropriate. // // It returns the type of the message and an io.Reader to read it. From 9e84c8936f87c74a0d271f5f5e0a00d883f7c7cf Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 13 Dec 2022 02:42:55 -0800 Subject: [PATCH 362/519] README.md: Add note --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 0ae739a0..bc7047d1 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,11 @@ websocket is a minimal and idiomatic WebSocket library for Go. +> **note**: I haven't been responsive for questions/reports on the issue tracker but I do +> read through and I don't believe there are any outstanding bugs. There are certainly +> some nice to haves that I should merge in/figure out but nothing critical. I haven't +> given up on adding new features and cleaning up the code further, just been busy. + ## Install ```bash From de6965b26ed70b37365ba51131ce7eb93c16443e Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 13 Dec 2022 03:12:20 -0800 Subject: [PATCH 363/519] REAME: Update note --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index bc7047d1..380fc58e 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ websocket is a minimal and idiomatic WebSocket library for Go. > read through and I don't believe there are any outstanding bugs. There are certainly > some nice to haves that I should merge in/figure out but nothing critical. I haven't > given up on adding new features and cleaning up the code further, just been busy. +> Should anything critical arise, I will fix it. ## Install From 9e7b1d5a38230cb42267ad5bb92ed2762d9035ac Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 13 Dec 2022 14:35:01 -0800 Subject: [PATCH 364/519] README: Further update --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 380fc58e..4e73a266 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@ websocket is a minimal and idiomatic WebSocket library for Go. > **note**: I haven't been responsive for questions/reports on the issue tracker but I do -> read through and I don't believe there are any outstanding bugs. There are certainly -> some nice to haves that I should merge in/figure out but nothing critical. I haven't -> given up on adding new features and cleaning up the code further, just been busy. -> Should anything critical arise, I will fix it. +> read through and there are no outstanding bugs. There are certainly some nice to haves +> that I should merge in/figure out but nothing critical. I haven't given up on adding new +> features and cleaning up the code further, just been busy. Should anything critical +> arise, I will fix it. ## Install From 5dd228a41529d7e174c059e465b52eac1d8f1e5b Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 28 Sep 2023 08:14:30 -0700 Subject: [PATCH 365/519] compress.go: Add back comment about Safari compat layer being disabled --- compress.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/compress.go b/compress.go index 68734471..a9e1fa35 100644 --- a/compress.go +++ b/compress.go @@ -12,6 +12,12 @@ import ( // CompressionMode represents the modes available to the deflate extension. // See https://tools.ietf.org/html/rfc7692 +// +// A compatibility layer is implemented for the older deflate-frame extension used +// by safari. See https://tools.ietf.org/html/draft-tyoshino-hybi-websocket-perframe-deflate-06 +// It will work the same in every way except that we cannot signal to the peer we +// want to use no context takeover on our side, we can only signal that they should. +// But it is currently disabled due to Safari bugs. See https://github.com/nhooyr/websocket/issues/218 type CompressionMode int const ( From b9a4d42a16d442dfbccf2cf52c67311afb32893c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 28 Sep 2023 08:25:35 -0700 Subject: [PATCH 366/519] LICENSE.txt: Switch to OpenBSD's license --- LICENSE.txt | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index b5b5fef3..77b5bef6 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,21 +1,13 @@ -MIT License - -Copyright (c) 2018 Anmol Sethi - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Copyright (c) 2023 Anmol Sethi + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. From a374f19a700ae45ed57d8e391f769ef86ddab0fe Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 28 Sep 2023 08:28:30 -0700 Subject: [PATCH 367/519] .github: Delete CODEOWNERS --- .github/CODEOWNERS | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index d2eae33e..00000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @nhooyr From c45cd4cdecad5b6f817860e4d0aa428ac5e6faec Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 28 Sep 2023 08:28:36 -0700 Subject: [PATCH 368/519] ci/container: Fix for newer Go --- ci/container/Dockerfile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ci/container/Dockerfile b/ci/container/Dockerfile index 0c6c2a54..e2721b9b 100644 --- a/ci/container/Dockerfile +++ b/ci/container/Dockerfile @@ -4,11 +4,11 @@ RUN apt-get update RUN apt-get install -y npm shellcheck chromium ENV GO111MODULE=on -RUN go get golang.org/x/tools/cmd/goimports -RUN go get mvdan.cc/sh/v3/cmd/shfmt -RUN go get golang.org/x/tools/cmd/stringer -RUN go get golang.org/x/lint/golint -RUN go get github.com/agnivade/wasmbrowsertest +RUN go install golang.org/x/tools/cmd/goimports@latest +RUN go install mvdan.cc/sh/v3/cmd/shfmt@latest +RUN go install golang.org/x/tools/cmd/stringer@latest +RUN go install golang.org/x/lint/golint@latest +RUN go install github.com/agnivade/wasmbrowsertest@latest RUN npm --unsafe-perm=true install -g prettier RUN npm --unsafe-perm=true install -g netlify-cli From 118ea682a3ac882657ee11d7a2539a186a6766af Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 28 Sep 2023 08:36:13 -0700 Subject: [PATCH 369/519] ci: Fixes Credits to @maggie44 for making me add staticcheck. See #407 Co-authored-by: maggie0002 <64841595+maggie0002@users.noreply.github.com> --- .github/workflows/ci.yaml | 39 ------------------------------ .github/workflows/ci.yml | 39 ++++++++++++++++++++++++++++++ ci/all.sh | 12 ---------- ci/container/Dockerfile | 14 ----------- ci/fmt.sh | 50 ++++++++++++--------------------------- ci/lint.sh | 24 +++++++++---------- ci/test.sh | 33 +++++++++----------------- examples/chat/index.css | 8 +++---- examples/chat/index.html | 2 +- examples/chat/index.js | 36 ++++++++++++++-------------- make.sh | 18 ++++---------- 11 files changed, 103 insertions(+), 172 deletions(-) delete mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/ci.yml delete mode 100755 ci/all.sh delete mode 100644 ci/container/Dockerfile diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml deleted file mode 100644 index 3d9829ef..00000000 --- a/.github/workflows/ci.yaml +++ /dev/null @@ -1,39 +0,0 @@ -name: ci - -on: [push, pull_request] - -jobs: - fmt: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: Run ./ci/fmt.sh - uses: ./ci/container - with: - args: ./ci/fmt.sh - - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: Run ./ci/lint.sh - uses: ./ci/container - with: - args: ./ci/lint.sh - - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: Run ./ci/test.sh - uses: ./ci/container - with: - args: ./ci/test.sh - env: - NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - NETLIFY_SITE_ID: 9b3ee4dc-8297-4774-b4b9-a61561fbbce7 - - name: Upload coverage.html - uses: actions/upload-artifact@v2 - with: - name: coverage.html - path: ./ci/out/coverage.html diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..f31ea711 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: ci +on: [push, pull_request] +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +jobs: + fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v4 + with: + go-version-file: ./go.mod + - run: ./ci/fmt.sh + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: go version + - uses: actions/setup-go@v4 + with: + go-version-file: ./go.mod + - run: ./ci/lint.sh + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v4 + with: + go-version-file: ./go.mod + - run: ./ci/test.sh + - uses: actions/upload-artifact@v2 + if: always() + with: + name: coverage.html + path: ./ci/out/coverage.html diff --git a/ci/all.sh b/ci/all.sh deleted file mode 100755 index 1ee7640f..00000000 --- a/ci/all.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -main() { - cd "$(dirname "$0")/.." - - ./ci/fmt.sh - ./ci/lint.sh - ./ci/test.sh "$@" -} - -main "$@" diff --git a/ci/container/Dockerfile b/ci/container/Dockerfile deleted file mode 100644 index e2721b9b..00000000 --- a/ci/container/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM golang - -RUN apt-get update -RUN apt-get install -y npm shellcheck chromium - -ENV GO111MODULE=on -RUN go install golang.org/x/tools/cmd/goimports@latest -RUN go install mvdan.cc/sh/v3/cmd/shfmt@latest -RUN go install golang.org/x/tools/cmd/stringer@latest -RUN go install golang.org/x/lint/golint@latest -RUN go install github.com/agnivade/wasmbrowsertest@latest - -RUN npm --unsafe-perm=true install -g prettier -RUN npm --unsafe-perm=true install -g netlify-cli diff --git a/ci/fmt.sh b/ci/fmt.sh index b34f1438..0d902732 100755 --- a/ci/fmt.sh +++ b/ci/fmt.sh @@ -1,38 +1,18 @@ -#!/usr/bin/env bash -set -euo pipefail +#!/bin/sh +set -eu +cd -- "$(dirname "$0")/.." -main() { - cd "$(dirname "$0")/.." +go mod tidy +gofmt -w -s . +go run golang.org/x/tools/cmd/goimports@latest -w "-local=$(go list -m)" . - go mod tidy - gofmt -w -s . - goimports -w "-local=$(go list -m)" . +npx prettier@3.0.3 \ + --write \ + --log-level=warn \ + --print-width=90 \ + --no-semi \ + --single-quote \ + --arrow-parens=avoid \ + $(git ls-files "*.yml" "*.md" "*.js" "*.css" "*.html") - prettier \ - --write \ - --print-width=120 \ - --no-semi \ - --trailing-comma=all \ - --loglevel=warn \ - --arrow-parens=avoid \ - $(git ls-files "*.yml" "*.md" "*.js" "*.css" "*.html") - shfmt -i 2 -w -s -sr $(git ls-files "*.sh") - - stringer -type=opcode,MessageType,StatusCode -output=stringer.go - - if [[ ${CI-} ]]; then - assert_no_changes - fi -} - -assert_no_changes() { - if [[ $(git ls-files --other --modified --exclude-standard) ]]; then - git -c color.ui=always --no-pager diff - echo - echo "Please run the following locally:" - echo " ./ci/fmt.sh" - exit 1 - fi -} - -main "$@" +go run golang.org/x/tools/cmd/stringer@latest -type=opcode,MessageType,StatusCode -output=stringer.go diff --git a/ci/lint.sh b/ci/lint.sh index e1053d13..a8ab3027 100755 --- a/ci/lint.sh +++ b/ci/lint.sh @@ -1,16 +1,14 @@ -#!/usr/bin/env bash -set -euo pipefail +#!/bin/sh +set -eu +cd -- "$(dirname "$0")/.." -main() { - cd "$(dirname "$0")/.." +go vet ./... +GOOS=js GOARCH=wasm go vet ./... - go vet ./... - GOOS=js GOARCH=wasm go vet ./... +go install golang.org/x/lint/golint@latest +golint -set_exit_status ./... +GOOS=js GOARCH=wasm golint -set_exit_status ./... - golint -set_exit_status ./... - GOOS=js GOARCH=wasm golint -set_exit_status ./... - - shellcheck --exclude=SC2046 $(git ls-files "*.sh") -} - -main "$@" +go install honnef.co/go/tools/cmd/staticcheck@latest +staticcheck ./... +GOOS=js GOARCH=wasm staticcheck ./... diff --git a/ci/test.sh b/ci/test.sh index bd68b80e..1b3d6cc3 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -1,25 +1,14 @@ -#!/usr/bin/env bash -set -euo pipefail +#!/bin/sh +set -eu +cd -- "$(dirname "$0")/.." -main() { - cd "$(dirname "$0")/.." +go install github.com/agnivade/wasmbrowsertest@latest +go test --race --timeout=1h --covermode=atomic --coverprofile=ci/out/coverage.prof --coverpkg=./... "$@" ./... +sed -i.bak '/stringer\.go/d' ci/out/coverage.prof +sed -i.bak '/nhooyr.io\/websocket\/internal\/test/d' ci/out/coverage.prof +sed -i.bak '/examples/d' ci/out/coverage.prof - go test -timeout=30m -covermode=atomic -coverprofile=ci/out/coverage.prof -coverpkg=./... "$@" ./... - sed -i.bak '/stringer\.go/d' ci/out/coverage.prof - sed -i.bak '/nhooyr.io\/websocket\/internal\/test/d' ci/out/coverage.prof - sed -i.bak '/examples/d' ci/out/coverage.prof +# Last line is the total coverage. +go tool cover -func ci/out/coverage.prof | tail -n1 - # Last line is the total coverage. - go tool cover -func ci/out/coverage.prof | tail -n1 - - go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html - - if [[ ${CI-} && ${GITHUB_REF-} == *master ]]; then - local deployDir - deployDir="$(mktemp -d)" - cp ci/out/coverage.html "$deployDir/index.html" - netlify deploy --prod "--dir=$deployDir" - fi -} - -main "$@" +go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html diff --git a/examples/chat/index.css b/examples/chat/index.css index 73a8e0f3..ce27c378 100644 --- a/examples/chat/index.css +++ b/examples/chat/index.css @@ -54,7 +54,7 @@ body { margin: 0 0 0 10px; } -#publish-form input[type="text"] { +#publish-form input[type='text'] { flex-grow: 1; -moz-appearance: none; @@ -64,7 +64,7 @@ body { border: 1px solid #ccc; } -#publish-form input[type="submit"] { +#publish-form input[type='submit'] { color: white; background-color: black; border-radius: 5px; @@ -72,10 +72,10 @@ body { border: none; } -#publish-form input[type="submit"]:hover { +#publish-form input[type='submit']:hover { background-color: red; } -#publish-form input[type="submit"]:active { +#publish-form input[type='submit']:active { background-color: red; } diff --git a/examples/chat/index.html b/examples/chat/index.html index 76ae8370..64edd286 100644 --- a/examples/chat/index.html +++ b/examples/chat/index.html @@ -1,4 +1,4 @@ - + diff --git a/examples/chat/index.js b/examples/chat/index.js index 5868e7ca..2efca013 100644 --- a/examples/chat/index.js +++ b/examples/chat/index.js @@ -6,21 +6,21 @@ function dial() { const conn = new WebSocket(`ws://${location.host}/subscribe`) - conn.addEventListener("close", ev => { + conn.addEventListener('close', ev => { appendLog(`WebSocket Disconnected code: ${ev.code}, reason: ${ev.reason}`, true) if (ev.code !== 1001) { - appendLog("Reconnecting in 1s", true) + appendLog('Reconnecting in 1s', true) setTimeout(dial, 1000) } }) - conn.addEventListener("open", ev => { - console.info("websocket connected") + conn.addEventListener('open', ev => { + console.info('websocket connected') }) // This is where we handle messages received. - conn.addEventListener("message", ev => { - if (typeof ev.data !== "string") { - console.error("unexpected message type", typeof ev.data) + conn.addEventListener('message', ev => { + if (typeof ev.data !== 'string') { + console.error('unexpected message type', typeof ev.data) return } const p = appendLog(ev.data) @@ -32,38 +32,38 @@ } dial() - const messageLog = document.getElementById("message-log") - const publishForm = document.getElementById("publish-form") - const messageInput = document.getElementById("message-input") + const messageLog = document.getElementById('message-log') + const publishForm = document.getElementById('publish-form') + const messageInput = document.getElementById('message-input') // appendLog appends the passed text to messageLog. function appendLog(text, error) { - const p = document.createElement("p") + const p = document.createElement('p') // Adding a timestamp to each message makes the log easier to read. p.innerText = `${new Date().toLocaleTimeString()}: ${text}` if (error) { - p.style.color = "red" - p.style.fontStyle = "bold" + p.style.color = 'red' + p.style.fontStyle = 'bold' } messageLog.append(p) return p } - appendLog("Submit a message to get started!") + appendLog('Submit a message to get started!') // onsubmit publishes the message from the user when the form is submitted. publishForm.onsubmit = async ev => { ev.preventDefault() const msg = messageInput.value - if (msg === "") { + if (msg === '') { return } - messageInput.value = "" + messageInput.value = '' expectingMessage = true try { - const resp = await fetch("/publish", { - method: "POST", + const resp = await fetch('/publish', { + method: 'POST', body: msg, }) if (resp.status !== 202) { diff --git a/make.sh b/make.sh index 578203cd..6f5d1f57 100755 --- a/make.sh +++ b/make.sh @@ -1,17 +1,7 @@ #!/bin/sh set -eu +cd -- "$(dirname "$0")" -cd "$(dirname "$0")" - -fmt() { - go mod tidy - gofmt -s -w . - goimports -w "-local=$(go list -m)" . -} - -if ! command -v wasmbrowsertest >/dev/null; then - go install github.com/agnivade/wasmbrowsertest@latest -fi - -fmt -go test -race --timeout=1h ./... "$@" +./ci/fmt.sh +./ci/lint.sh +./ci/test.sh From 8d3d892cf636d3465b1c7424ca91ffc1720db172 Mon Sep 17 00:00:00 2001 From: Jacalz Date: Sun, 12 Mar 2023 15:39:19 +0100 Subject: [PATCH 370/519] Update Go module version to 1.18 Fixes https://github.com/nhooyr/websocket/issues/359 --- go.mod | 22 +++++++++++++++++++--- go.sum | 1 - 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index d4bca923..ad9bc045 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,30 @@ module nhooyr.io/websocket -go 1.13 +go 1.18 require ( github.com/gin-gonic/gin v1.6.3 - github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee // indirect - github.com/gobwas/pool v0.2.0 // indirect github.com/gobwas/ws v1.0.2 github.com/golang/protobuf v1.3.5 github.com/google/go-cmp v0.4.0 github.com/gorilla/websocket v1.4.1 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 ) + +require ( + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.13.0 // indirect + github.com/go-playground/universal-translator v0.17.0 // indirect + github.com/go-playground/validator/v10 v10.2.0 // indirect + github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee // indirect + github.com/gobwas/pool v0.2.0 // indirect + github.com/json-iterator/go v1.1.9 // indirect + github.com/leodido/go-urn v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect + github.com/ugorji/go/codec v1.1.7 // indirect + golang.org/x/sys v0.0.0-20200116001909-b77594299b42 // indirect + golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect + gopkg.in/yaml.v2 v2.2.8 // indirect +) diff --git a/go.sum b/go.sum index 1344e958..75854444 100644 --- a/go.sum +++ b/go.sum @@ -43,7 +43,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= From 4188bcfe6f341ddc7c5d298d4f776cd737113204 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 10 Oct 2023 03:55:09 -0700 Subject: [PATCH 371/519] go.mod: Regenerate --- go.mod | 51 +++++++++++++--------- go.sum | 131 ++++++++++++++++++++++++++++++++++++--------------------- 2 files changed, 115 insertions(+), 67 deletions(-) diff --git a/go.mod b/go.mod index ad9bc045..c7e7dfd2 100644 --- a/go.mod +++ b/go.mod @@ -3,28 +3,39 @@ module nhooyr.io/websocket go 1.18 require ( - github.com/gin-gonic/gin v1.6.3 - github.com/gobwas/ws v1.0.2 - github.com/golang/protobuf v1.3.5 - github.com/google/go-cmp v0.4.0 - github.com/gorilla/websocket v1.4.1 - golang.org/x/time v0.0.0-20191024005414-555d28b269f0 + github.com/gin-gonic/gin v1.9.1 + github.com/gobwas/ws v1.3.0 + github.com/golang/protobuf v1.5.3 + github.com/google/go-cmp v0.5.9 + github.com/gorilla/websocket v1.5.0 + golang.org/x/time v0.3.0 ) require ( + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gin-contrib/sse v0.1.0 // indirect - github.com/go-playground/locales v0.13.0 // indirect - github.com/go-playground/universal-translator v0.17.0 // indirect - github.com/go-playground/validator/v10 v10.2.0 // indirect - github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee // indirect - github.com/gobwas/pool v0.2.0 // indirect - github.com/json-iterator/go v1.1.9 // indirect - github.com/leodido/go-urn v1.2.0 // indirect - github.com/mattn/go-isatty v0.0.12 // indirect - github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect - github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect - github.com/ugorji/go/codec v1.1.7 // indirect - golang.org/x/sys v0.0.0-20200116001909-b77594299b42 // indirect - golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect - gopkg.in/yaml.v2 v2.2.8 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/gobwas/httphead v0.1.0 // indirect + github.com/gobwas/pool v0.2.1 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.9.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 75854444..78c452e4 100644 --- a/go.sum +++ b/go.sum @@ -1,61 +1,98 @@ +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= -github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= -github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= -github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= -github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= -github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= -github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= -github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= -github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.3.0 h1:sbeU3Y4Qzlb+MOzIe6mQGf7QR4Hkv6ZD0qhGkBFL2O0= +github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= -github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= -github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= From 1c90f47e4929302ce89b3e7c5778eeac5a3ae7ab Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 10 Oct 2023 04:02:03 -0700 Subject: [PATCH 372/519] ci.yml: Fix --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f31ea711..8b88e81c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,8 +32,7 @@ jobs: with: go-version-file: ./go.mod - run: ./ci/test.sh - - uses: actions/upload-artifact@v2 - if: always() + - uses: actions/upload-artifact@v3 with: name: coverage.html path: ./ci/out/coverage.html From 2a5a56660c4f17cc12c8532ced8869f25a310ad8 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 10 Oct 2023 04:08:26 -0700 Subject: [PATCH 373/519] go.mod: Upgrade to Go 1.19 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index c7e7dfd2..50c873bf 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module nhooyr.io/websocket -go 1.18 +go 1.19 require ( github.com/gin-gonic/gin v1.9.1 From e1e65adca29fa3d2989286c16d737c9bc276779a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 10 Oct 2023 04:11:09 -0700 Subject: [PATCH 374/519] daily.yml: Add to run AUTOBAHN tests daily --- .github/workflows/daily.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/daily.yml diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml new file mode 100644 index 00000000..cbac574d --- /dev/null +++ b/.github/workflows/daily.yml @@ -0,0 +1,22 @@ +name: daily +on: + workflow_dispatch: + schedule: + - cron: '42 0 * * *' # daily at 00:42 +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: true + +jobs: + ci: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v4 + with: + go-version-file: ./go.mod + - run: AUTOBAHN=1 ./ci/test.sh + - uses: actions/upload-artifact@v3 + with: + name: coverage.html + path: ./ci/out/coverage.html From 75bf907768b38735aaa002044e81fad7c443ba43 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 10 Oct 2023 08:25:37 -0700 Subject: [PATCH 375/519] autobahn_test.go: Pull image before starting container --- autobahn_test.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/autobahn_test.go b/autobahn_test.go index 4df4b66b..23723b51 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -112,6 +112,8 @@ func waitWS(ctx context.Context, url string) error { // TODO: Let docker pick the port and use docker port to find it. // Does mean we can't use -i but that's fine. func wstestServer(ctx context.Context) (url string, closeFn func() error, err error) { + defer errd.Wrap(&err, "failed to start autobahn wstest server") + serverAddr, err := unusedListenAddr() if err != nil { return "", nil, err @@ -141,6 +143,15 @@ func wstestServer(ctx context.Context) (url string, closeFn func() error, err er } }() + dockerPull := exec.CommandContext(ctx, "docker", "pull", "crossbario/autobahn-testsuite") + // TODO: log to *testing.T + dockerPull.Stdout = os.Stdout + dockerPull.Stderr = os.Stderr + err = dockerPull.Run() + if err != nil { + return "", nil, fmt.Errorf("failed to pull docker image: %w", err) + } + wd, err := os.Getwd() if err != nil { return "", nil, err @@ -159,7 +170,6 @@ func wstestServer(ctx context.Context) (url string, closeFn func() error, err er "--webport=0", ) fmt.Println(strings.Join(args, " ")) - // TODO: pull image in advance wstest := exec.CommandContext(ctx, "docker", args...) // TODO: log to *testing.T wstest.Stdout = os.Stdout From 4ab2f5421083225550ef44000699a0d6e899983c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 10 Oct 2023 08:34:49 -0700 Subject: [PATCH 376/519] conn_test: Remove ioutil --- conn_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conn_test.go b/conn_test.go index f474ae0c..639666b1 100644 --- a/conn_test.go +++ b/conn_test.go @@ -223,7 +223,7 @@ func TestConn(t *testing.T) { return n2.Close() }) - b, err := ioutil.ReadAll(n1) + b, err := io.ReadAll(n1) assert.Success(t, err) _, err = n1.Read(nil) From e9d08816010996a14241f008ac097c5621bd1f30 Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Wed, 19 May 2021 23:52:23 +0200 Subject: [PATCH 377/519] Use net.ErrClosed Go 1.16 has introduced net.ErrClosed, which should be returned/wrapped when an I/O call is performed on a network connection which has already been closed. This is useful to avoid cluttering logs with messages like "failed to close WebSocket: already wrote close". Closes: https://github.com/nhooyr/websocket/issues/286 --- close.go | 4 +--- close_go113.go | 9 +++++++++ close_go116.go | 9 +++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 close_go113.go create mode 100644 close_go116.go diff --git a/close.go b/close.go index eab49a8f..1e13ca73 100644 --- a/close.go +++ b/close.go @@ -119,15 +119,13 @@ func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { return nil } -var errAlreadyWroteClose = errors.New("already wrote close") - func (c *Conn) writeClose(code StatusCode, reason string) error { c.closeMu.Lock() wroteClose := c.wroteClose c.wroteClose = true c.closeMu.Unlock() if wroteClose { - return errAlreadyWroteClose + return errClosed } ce := CloseError{ diff --git a/close_go113.go b/close_go113.go new file mode 100644 index 00000000..4f586dcb --- /dev/null +++ b/close_go113.go @@ -0,0 +1,9 @@ +// +build !go1.16 + +package websocket + +import ( + "errors" +) + +var errClosed = errors.New("use of closed network connection") diff --git a/close_go116.go b/close_go116.go new file mode 100644 index 00000000..0a6e5f15 --- /dev/null +++ b/close_go116.go @@ -0,0 +1,9 @@ +// +build go1.16 + +package websocket + +import ( + "net" +) + +var errClosed = net.ErrClosed From e3050279d59cc6896b8e056ac1b1ec3eca484176 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 00:25:59 -0700 Subject: [PATCH 378/519] dial.go: Clarify http.NewRequestWithContext error --- dial.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dial.go b/dial.go index ac05cba6..4b2b7b62 100644 --- a/dial.go +++ b/dial.go @@ -166,7 +166,7 @@ func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, copts req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) if err != nil { - return nil, fmt.Errorf("http.NewRequestWithContext failed: %w", err) + return nil, fmt.Errorf("failed to create new http request: %w", err) } req.Header = opts.HTTPHeader.Clone() req.Header.Set("Connection", "Upgrade") From ac385120c6e34fa6584f3856d5db949a21bbb65e Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 00:29:58 -0700 Subject: [PATCH 379/519] wspb: Remove The library we're currently using for protobufs is deprecated. Doesn't belong in the library core anyway. Closes #311 Updates #297 --- conn_test.go | 21 --------------- wspb/wspb.go | 73 ---------------------------------------------------- 2 files changed, 94 deletions(-) delete mode 100644 wspb/wspb.go diff --git a/conn_test.go b/conn_test.go index 639666b1..a3f3d787 100644 --- a/conn_test.go +++ b/conn_test.go @@ -17,8 +17,6 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/golang/protobuf/ptypes" - "github.com/golang/protobuf/ptypes/duration" "nhooyr.io/websocket" "nhooyr.io/websocket/internal/errd" @@ -27,7 +25,6 @@ import ( "nhooyr.io/websocket/internal/test/xrand" "nhooyr.io/websocket/internal/xsync" "nhooyr.io/websocket/wsjson" - "nhooyr.io/websocket/wspb" ) func TestConn(t *testing.T) { @@ -267,24 +264,6 @@ func TestConn(t *testing.T) { err = c1.Close(websocket.StatusNormalClosure, "") assert.Success(t, err) }) - - t.Run("wspb", func(t *testing.T) { - tt, c1, c2 := newConnTest(t, nil, nil) - - tt.goEchoLoop(c2) - - exp := ptypes.DurationProto(100) - err := wspb.Write(tt.ctx, c1, exp) - assert.Success(t, err) - - act := &duration.Duration{} - err = wspb.Read(tt.ctx, c1, act) - assert.Success(t, err) - assert.Equal(t, "read msg", exp, act) - - err = c1.Close(websocket.StatusNormalClosure, "") - assert.Success(t, err) - }) } func TestWasm(t *testing.T) { diff --git a/wspb/wspb.go b/wspb/wspb.go deleted file mode 100644 index e43042d5..00000000 --- a/wspb/wspb.go +++ /dev/null @@ -1,73 +0,0 @@ -// Package wspb provides helpers for reading and writing protobuf messages. -package wspb // import "nhooyr.io/websocket/wspb" - -import ( - "bytes" - "context" - "fmt" - - "github.com/golang/protobuf/proto" - - "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/bpool" - "nhooyr.io/websocket/internal/errd" -) - -// Read reads a protobuf message from c into v. -// It will reuse buffers in between calls to avoid allocations. -func Read(ctx context.Context, c *websocket.Conn, v proto.Message) error { - return read(ctx, c, v) -} - -func read(ctx context.Context, c *websocket.Conn, v proto.Message) (err error) { - defer errd.Wrap(&err, "failed to read protobuf message") - - typ, r, err := c.Reader(ctx) - if err != nil { - return err - } - - if typ != websocket.MessageBinary { - c.Close(websocket.StatusUnsupportedData, "expected binary message") - return fmt.Errorf("expected binary message for protobuf but got: %v", typ) - } - - b := bpool.Get() - defer bpool.Put(b) - - _, err = b.ReadFrom(r) - if err != nil { - return err - } - - err = proto.Unmarshal(b.Bytes(), v) - if err != nil { - c.Close(websocket.StatusInvalidFramePayloadData, "failed to unmarshal protobuf") - return fmt.Errorf("failed to unmarshal protobuf: %w", err) - } - - return nil -} - -// Write writes the protobuf message v to c. -// It will reuse buffers in between calls to avoid allocations. -func Write(ctx context.Context, c *websocket.Conn, v proto.Message) error { - return write(ctx, c, v) -} - -func write(ctx context.Context, c *websocket.Conn, v proto.Message) (err error) { - defer errd.Wrap(&err, "failed to write protobuf message") - - b := bpool.Get() - pb := proto.NewBuffer(b.Bytes()) - defer func() { - bpool.Put(bytes.NewBuffer(pb.Bytes())) - }() - - err = pb.Marshal(v) - if err != nil { - return fmt.Errorf("failed to marshal protobuf: %w", err) - } - - return c.Write(ctx, websocket.MessageBinary, pb.Bytes()) -} From 9b5a15bfc3b8b016eda073c01fec299ea84f8804 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 00:33:13 -0700 Subject: [PATCH 380/519] close_goXXX.go: fmt --- close_go113.go | 1 + close_go116.go | 1 + 2 files changed, 2 insertions(+) diff --git a/close_go113.go b/close_go113.go index 4f586dcb..fb226475 100644 --- a/close_go113.go +++ b/close_go113.go @@ -1,3 +1,4 @@ +//go:build !go1.16 // +build !go1.16 package websocket diff --git a/close_go116.go b/close_go116.go index 0a6e5f15..2724e0ca 100644 --- a/close_go116.go +++ b/close_go116.go @@ -1,3 +1,4 @@ +//go:build go1.16 // +build go1.16 package websocket From a633a10fb558ad5b93247ca57f45480f643f7ce6 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 00:49:40 -0700 Subject: [PATCH 381/519] lint.sh: Pass --- accept.go | 39 ++-------------------------------- autobahn_test.go | 2 +- close_go113.go | 3 +-- close_go116.go | 3 +-- compress.go | 6 +++--- frame.go | 2 ++ frame_test.go | 6 ++++-- go.mod | 1 - go.sum | 3 --- internal/test/assert/assert.go | 3 +-- internal/wsjs/wsjs_js.go | 2 -- netconn.go | 4 ++-- ws_js.go | 26 ++++++++++++++++++++--- 13 files changed, 40 insertions(+), 60 deletions(-) diff --git a/accept.go b/accept.go index d918aab5..ff2033e7 100644 --- a/accept.go +++ b/accept.go @@ -245,11 +245,10 @@ func acceptCompression(r *http.Request, w http.ResponseWriter, mode CompressionM for _, ext := range websocketExtensions(r.Header) { switch ext.name { + // We used to implement x-webkit-deflate-fram too but Safari has bugs. + // See https://github.com/nhooyr/websocket/issues/218 case "permessage-deflate": return acceptDeflate(w, ext, mode) - // Disabled for now, see https://github.com/nhooyr/websocket/issues/218 - // case "x-webkit-deflate-frame": - // return acceptWebkitDeflate(w, ext, mode) } } return nil, nil @@ -283,40 +282,6 @@ func acceptDeflate(w http.ResponseWriter, ext websocketExtension, mode Compressi return copts, nil } -func acceptWebkitDeflate(w http.ResponseWriter, ext websocketExtension, mode CompressionMode) (*compressionOptions, error) { - copts := mode.opts() - // The peer must explicitly request it. - copts.serverNoContextTakeover = false - - for _, p := range ext.params { - if p == "no_context_takeover" { - copts.serverNoContextTakeover = true - continue - } - - // We explicitly fail on x-webkit-deflate-frame's max_window_bits parameter instead - // of ignoring it as the draft spec is unclear. It says the server can ignore it - // but the server has no way of signalling to the client it was ignored as the parameters - // are set one way. - // Thus us ignoring it would make the client think we understood it which would cause issues. - // See https://tools.ietf.org/html/draft-tyoshino-hybi-websocket-perframe-deflate-06#section-4.1 - // - // Either way, we're only implementing this for webkit which never sends the max_window_bits - // parameter so we don't need to worry about it. - err := fmt.Errorf("unsupported x-webkit-deflate-frame parameter: %q", p) - http.Error(w, err.Error(), http.StatusBadRequest) - return nil, err - } - - s := "x-webkit-deflate-frame" - if copts.clientNoContextTakeover { - s += "; no_context_takeover" - } - w.Header().Set("Sec-WebSocket-Extensions", s) - - return copts, nil -} - func headerContainsTokenIgnoreCase(h http.Header, key, token string) bool { for _, t := range headerTokens(h, key) { if strings.EqualFold(t, token) { diff --git a/autobahn_test.go b/autobahn_test.go index 8100c37f..41fae555 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -38,7 +38,7 @@ var autobahnCases = []string{"*"} // and not excluded by excludedAutobahnCases. Adding cases here means excludedAutobahnCases // is niled. // TODO: -var forceAutobahnCases = []string{} +// var forceAutobahnCases = []string{} func TestAutobahn(t *testing.T) { t.Parallel() diff --git a/close_go113.go b/close_go113.go index fb226475..caf1b89e 100644 --- a/close_go113.go +++ b/close_go113.go @@ -1,5 +1,4 @@ -//go:build !go1.16 -// +build !go1.16 +//go:build !go1.16 && !js package websocket diff --git a/close_go116.go b/close_go116.go index 2724e0ca..9d986109 100644 --- a/close_go116.go +++ b/close_go116.go @@ -1,5 +1,4 @@ -//go:build go1.16 -// +build go1.16 +//go:build go1.16 && !js package websocket diff --git a/compress.go b/compress.go index a9e1fa35..e6722fc7 100644 --- a/compress.go +++ b/compress.go @@ -201,9 +201,9 @@ func (sw *slidingWindow) init(n int) { } p := slidingWindowPool(n) - buf, ok := p.Get().([]byte) + buf, ok := p.Get().(*[]byte) if ok { - sw.buf = buf[:0] + sw.buf = (*buf)[:0] } else { sw.buf = make([]byte, 0, n) } @@ -215,7 +215,7 @@ func (sw *slidingWindow) close() { } swPoolMu.Lock() - swPool[cap(sw.buf)].Put(sw.buf) + swPool[cap(sw.buf)].Put(&sw.buf) swPoolMu.Unlock() sw.buf = nil } diff --git a/frame.go b/frame.go index 2a036f94..351632fd 100644 --- a/frame.go +++ b/frame.go @@ -1,3 +1,5 @@ +//go:build !js + package websocket import ( diff --git a/frame_test.go b/frame_test.go index 93ad8b5f..2f4f2e25 100644 --- a/frame_test.go +++ b/frame_test.go @@ -55,7 +55,7 @@ func TestHeader(t *testing.T) { r := rand.New(rand.NewSource(time.Now().UnixNano())) randBool := func() bool { - return r.Intn(1) == 0 + return r.Intn(2) == 0 } for i := 0; i < 10000; i++ { @@ -67,9 +67,11 @@ func TestHeader(t *testing.T) { opcode: opcode(r.Intn(16)), masked: randBool(), - maskKey: r.Uint32(), payloadLength: r.Int63(), } + if h.masked { + h.maskKey = r.Uint32() + } testHeader(t, h) } diff --git a/go.mod b/go.mod index 50c873bf..95a1df92 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.19 require ( github.com/gin-gonic/gin v1.9.1 github.com/gobwas/ws v1.3.0 - github.com/golang/protobuf v1.5.3 github.com/google/go-cmp v0.5.9 github.com/gorilla/websocket v1.5.0 golang.org/x/time v0.3.0 diff --git a/go.sum b/go.sum index 78c452e4..dc4743dd 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,6 @@ github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/K github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -87,7 +85,6 @@ golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/internal/test/assert/assert.go b/internal/test/assert/assert.go index 6eaf7fc3..e37e9573 100644 --- a/internal/test/assert/assert.go +++ b/internal/test/assert/assert.go @@ -6,7 +6,6 @@ import ( "strings" "testing" - "github.com/golang/protobuf/proto" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" ) @@ -15,7 +14,7 @@ import ( func Diff(v1, v2 interface{}) string { return cmp.Diff(v1, v2, cmpopts.EquateErrors(), cmp.Exporter(func(r reflect.Type) bool { return true - }), cmp.Comparer(proto.Equal)) + })) } // Equal asserts exp == act. diff --git a/internal/wsjs/wsjs_js.go b/internal/wsjs/wsjs_js.go index 88e8f43f..11eb59cb 100644 --- a/internal/wsjs/wsjs_js.go +++ b/internal/wsjs/wsjs_js.go @@ -119,8 +119,6 @@ func (c WebSocket) OnMessage(fn func(m MessageEvent)) (remove func()) { Data: data, } fn(me) - - return }) } diff --git a/netconn.go b/netconn.go index aea1a02d..4af6c202 100644 --- a/netconn.go +++ b/netconn.go @@ -200,7 +200,7 @@ func (nc *netConn) SetWriteDeadline(t time.Time) error { if t.IsZero() { nc.writeTimer.Stop() } else { - nc.writeTimer.Reset(t.Sub(time.Now())) + nc.writeTimer.Reset(time.Until(t)) } return nil } @@ -210,7 +210,7 @@ func (nc *netConn) SetReadDeadline(t time.Time) error { if t.IsZero() { nc.readTimer.Stop() } else { - nc.readTimer.Reset(t.Sub(time.Now())) + nc.readTimer.Reset(time.Until(t)) } return nil } diff --git a/ws_js.go b/ws_js.go index d1361328..3248933c 100644 --- a/ws_js.go +++ b/ws_js.go @@ -18,6 +18,26 @@ import ( "nhooyr.io/websocket/internal/xsync" ) +// opcode represents a WebSocket opcode. +type opcode int + +// https://tools.ietf.org/html/rfc6455#section-11.8. +const ( + opContinuation opcode = iota + opText + opBinary + // 3 - 7 are reserved for further non-control frames. + _ + _ + _ + _ + _ + opClose + opPing + opPong + // 11-16 are reserved for further control frames. +) + // Conn provides a wrapper around the browser WebSocket API. type Conn struct { ws wsjs.WebSocket @@ -302,7 +322,7 @@ func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) { // It buffers the entire message in memory and then sends it when the writer // is closed. func (c *Conn) Writer(ctx context.Context, typ MessageType) (io.WriteCloser, error) { - return writer{ + return &writer{ c: c, ctx: ctx, typ: typ, @@ -320,7 +340,7 @@ type writer struct { b *bytes.Buffer } -func (w writer) Write(p []byte) (int, error) { +func (w *writer) Write(p []byte) (int, error) { if w.closed { return 0, errors.New("cannot write to closed writer") } @@ -331,7 +351,7 @@ func (w writer) Write(p []byte) (int, error) { return n, nil } -func (w writer) Close() error { +func (w *writer) Close() error { if w.closed { return errors.New("cannot close closed writer") } From 3f26c9f6f1ec6ac0bba963f250194a55da16a211 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 00:57:27 -0700 Subject: [PATCH 382/519] wsjson: Write messages in a single frame always Closes #315 --- internal/util/util.go | 7 +++++++ wsjson/wsjson.go | 17 +++++++++-------- 2 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 internal/util/util.go diff --git a/internal/util/util.go b/internal/util/util.go new file mode 100644 index 00000000..1ff25dac --- /dev/null +++ b/internal/util/util.go @@ -0,0 +1,7 @@ +package util + +type WriterFunc func(p []byte) (int, error) + +func (f WriterFunc) Write(p []byte) (int, error) { + return f(p) +} diff --git a/wsjson/wsjson.go b/wsjson/wsjson.go index 2000a77a..c6b29ee1 100644 --- a/wsjson/wsjson.go +++ b/wsjson/wsjson.go @@ -8,6 +8,7 @@ import ( "nhooyr.io/websocket" "nhooyr.io/websocket/internal/bpool" + "nhooyr.io/websocket/internal/util" "nhooyr.io/websocket/internal/errd" ) @@ -51,17 +52,17 @@ func Write(ctx context.Context, c *websocket.Conn, v interface{}) error { func write(ctx context.Context, c *websocket.Conn, v interface{}) (err error) { defer errd.Wrap(&err, "failed to write JSON message") - w, err := c.Writer(ctx, websocket.MessageText) - if err != nil { - return err - } - // json.Marshal cannot reuse buffers between calls as it has to return // a copy of the byte slice but Encoder does as it directly writes to w. - err = json.NewEncoder(w).Encode(v) + err = json.NewEncoder(util.WriterFunc(func(p []byte) (int, error) { + err := c.Write(ctx, websocket.MessageText, p) + if err != nil { + return 0, err + } + return len(p), nil + })).Encode(v) if err != nil { return fmt.Errorf("failed to marshal JSON: %w", err) } - - return w.Close() + return nil } From f7bed7c75ec38a4cee43efdbba57da76daf5305e Mon Sep 17 00:00:00 2001 From: Martin Benda Date: Thu, 28 Apr 2022 15:26:06 +0200 Subject: [PATCH 383/519] Extend DialOptions to allow Host header override --- dial.go | 7 ++++++ dial_test.go | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/dial.go b/dial.go index 4b2b7b62..510b94b1 100644 --- a/dial.go +++ b/dial.go @@ -30,6 +30,10 @@ type DialOptions struct { // HTTPHeader specifies the HTTP headers included in the handshake request. HTTPHeader http.Header + // Host optionally overrides the Host HTTP header to send. If empty, the value + // of URL.Host will be used. + Host string + // Subprotocols lists the WebSocket subprotocols to negotiate with the server. Subprotocols []string @@ -168,6 +172,9 @@ func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, copts if err != nil { return nil, fmt.Errorf("failed to create new http request: %w", err) } + if len(opts.Host) > 0 { + req.Host = opts.Host + } req.Header = opts.HTTPHeader.Clone() req.Header.Set("Connection", "Upgrade") req.Header.Set("Upgrade", "websocket") diff --git a/dial_test.go b/dial_test.go index 75d59540..8680147e 100644 --- a/dial_test.go +++ b/dial_test.go @@ -4,6 +4,7 @@ package websocket import ( + "bytes" "context" "crypto/rand" "io" @@ -118,6 +119,65 @@ func TestBadDials(t *testing.T) { }) } +func Test_verifyHostOverride(t *testing.T) { + testCases := []struct { + name string + host string + exp string + }{ + { + name: "noOverride", + host: "", + exp: "example.com", + }, + { + name: "hostOverride", + host: "example.net", + exp: "example.net", + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + rt := func(r *http.Request) (*http.Response, error) { + assert.Equal(t, "Host", tc.exp, r.Host) + + h := http.Header{} + h.Set("Connection", "Upgrade") + h.Set("Upgrade", "websocket") + h.Set("Sec-WebSocket-Accept", secWebSocketAccept(r.Header.Get("Sec-WebSocket-Key"))) + + return &http.Response{ + StatusCode: http.StatusSwitchingProtocols, + Header: h, + Body: mockBody{bytes.NewBufferString("hi")}, + }, nil + } + + _, _, err := Dial(ctx, "ws://example.com", &DialOptions{ + HTTPClient: mockHTTPClient(rt), + Host: tc.host, + }) + assert.Success(t, err) + }) + } + +} + +type mockBody struct { + *bytes.Buffer +} + +func (mb mockBody) Close() error { + return nil +} + func Test_verifyServerHandshake(t *testing.T) { t.Parallel() From 98732747dc4b5e44bdbd80e6af21cde946621511 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 01:08:25 -0700 Subject: [PATCH 384/519] wsjson: fmt --- wsjson/wsjson.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wsjson/wsjson.go b/wsjson/wsjson.go index c6b29ee1..7c986a0d 100644 --- a/wsjson/wsjson.go +++ b/wsjson/wsjson.go @@ -8,8 +8,8 @@ import ( "nhooyr.io/websocket" "nhooyr.io/websocket/internal/bpool" - "nhooyr.io/websocket/internal/util" "nhooyr.io/websocket/internal/errd" + "nhooyr.io/websocket/internal/util" ) // Read reads a JSON message from c into v. From fecf26c12678e046275c4e99fad7f9bcda78fd83 Mon Sep 17 00:00:00 2001 From: photostorm Date: Fri, 23 Apr 2021 23:20:27 -0400 Subject: [PATCH 385/519] netconn.go: Return real remote and local address where possible --- conn_test.go | 4 ++-- netconn.go | 17 +++++++---------- netconn_js.go | 11 +++++++++++ netconn_notjs.go | 20 ++++++++++++++++++++ 4 files changed, 40 insertions(+), 12 deletions(-) create mode 100644 netconn_js.go create mode 100644 netconn_notjs.go diff --git a/conn_test.go b/conn_test.go index a3f3d787..b9e2063d 100644 --- a/conn_test.go +++ b/conn_test.go @@ -155,8 +155,8 @@ func TestConn(t *testing.T) { n1.SetDeadline(time.Time{}) assert.Equal(t, "remote addr", n1.RemoteAddr(), n1.LocalAddr()) - assert.Equal(t, "remote addr string", "websocket/unknown-addr", n1.RemoteAddr().String()) - assert.Equal(t, "remote addr network", "websocket", n1.RemoteAddr().Network()) + assert.Equal(t, "remote addr string", "pipe", n1.RemoteAddr().String()) + assert.Equal(t, "remote addr network", "pipe", n1.RemoteAddr().Network()) errs := xsync.Go(func() error { _, err := n2.Write([]byte("hello")) diff --git a/netconn.go b/netconn.go index 4af6c202..74000c9e 100644 --- a/netconn.go +++ b/netconn.go @@ -33,8 +33,13 @@ import ( // where only the reading/writing goroutines are interrupted but the connection // is kept alive. // -// The Addr methods will return a mock net.Addr that returns "websocket" for Network -// and "websocket/unknown-addr" for String. +// The Addr methods will return the real addresses for connections obtained +// from websocket.Accept. But for connections obtained from websocket.Dial, a mock net.Addr +// will be returned that gives "websocket" for Network() and "websocket/unknown-addr" for +// String(). This is because websocket.Dial only exposes a io.ReadWriteCloser instead of the +// full net.Conn to us. +// +// When running as WASM, the Addr methods will always return the mock address described above. // // A received StatusNormalClosure or StatusGoingAway close frame will be translated to // io.EOF when reading. @@ -181,14 +186,6 @@ func (a websocketAddr) String() string { return "websocket/unknown-addr" } -func (nc *netConn) RemoteAddr() net.Addr { - return websocketAddr{} -} - -func (nc *netConn) LocalAddr() net.Addr { - return websocketAddr{} -} - func (nc *netConn) SetDeadline(t time.Time) error { nc.SetWriteDeadline(t) nc.SetReadDeadline(t) diff --git a/netconn_js.go b/netconn_js.go new file mode 100644 index 00000000..ccc8c89f --- /dev/null +++ b/netconn_js.go @@ -0,0 +1,11 @@ +package websocket + +import "net" + +func (nc *netConn) RemoteAddr() net.Addr { + return websocketAddr{} +} + +func (nc *netConn) LocalAddr() net.Addr { + return websocketAddr{} +} diff --git a/netconn_notjs.go b/netconn_notjs.go new file mode 100644 index 00000000..f3eb0d66 --- /dev/null +++ b/netconn_notjs.go @@ -0,0 +1,20 @@ +//go:build !js +// +build !js + +package websocket + +import "net" + +func (nc *netConn) RemoteAddr() net.Addr { + if unc, ok := nc.c.rwc.(net.Conn); ok { + return unc.RemoteAddr() + } + return websocketAddr{} +} + +func (nc *netConn) LocalAddr() net.Addr { + if unc, ok := nc.c.rwc.(net.Conn); ok { + return unc.LocalAddr() + } + return websocketAddr{} +} From 5793e7d5804bf2bc775a271e5882625c02f83d47 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 01:23:08 -0700 Subject: [PATCH 386/519] internal/util: golint --- internal/util/util.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/util/util.go b/internal/util/util.go index 1ff25dac..f23fb67b 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -1,5 +1,6 @@ package util +// WriterFunc is used to implement one off io.Writers. type WriterFunc func(p []byte) (int, error) func (f WriterFunc) Write(p []byte) (int, error) { From 2598ea2175350ae8280757752ac6143693506e6d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 02:07:24 -0700 Subject: [PATCH 387/519] Remove third party dependencies from go.mod and go.sum Closes #297 --- README.md | 4 +- ci/lint.sh | 23 ++++ ci/test.sh | 9 ++ conn_test.go | 42 +------- frame_test.go | 88 --------------- go.mod | 37 ------- go.sum | 95 ----------------- {examples => internal/examples}/README.md | 0 .../examples}/chat/README.md | 0 {examples => internal/examples}/chat/chat.go | 0 .../examples}/chat/chat_test.go | 0 .../examples}/chat/index.css | 0 .../examples}/chat/index.html | 0 {examples => internal/examples}/chat/index.js | 0 {examples => internal/examples}/chat/main.go | 0 .../examples}/echo/README.md | 0 {examples => internal/examples}/echo/main.go | 0 .../examples}/echo/server.go | 0 .../examples}/echo/server_test.go | 0 internal/examples/go.mod | 11 ++ internal/examples/go.sum | 41 +++++++ internal/test/assert/assert.go | 16 +-- internal/test/wstest/echo.go | 3 +- internal/thirdparty/doc.go | 2 + internal/thirdparty/frame_test.go | 100 ++++++++++++++++++ internal/thirdparty/gin_test.go | 75 +++++++++++++ internal/thirdparty/go.mod | 41 +++++++ internal/thirdparty/go.sum | 94 ++++++++++++++++ 28 files changed, 406 insertions(+), 275 deletions(-) rename {examples => internal/examples}/README.md (100%) rename {examples => internal/examples}/chat/README.md (100%) rename {examples => internal/examples}/chat/chat.go (100%) rename {examples => internal/examples}/chat/chat_test.go (100%) rename {examples => internal/examples}/chat/index.css (100%) rename {examples => internal/examples}/chat/index.html (100%) rename {examples => internal/examples}/chat/index.js (100%) rename {examples => internal/examples}/chat/main.go (100%) rename {examples => internal/examples}/echo/README.md (100%) rename {examples => internal/examples}/echo/main.go (100%) rename {examples => internal/examples}/echo/server.go (100%) rename {examples => internal/examples}/echo/server_test.go (100%) create mode 100644 internal/examples/go.mod create mode 100644 internal/examples/go.sum create mode 100644 internal/thirdparty/doc.go create mode 100644 internal/thirdparty/frame_test.go create mode 100644 internal/thirdparty/gin_test.go create mode 100644 internal/thirdparty/go.mod create mode 100644 internal/thirdparty/go.sum diff --git a/README.md b/README.md index 4e73a266..f1a45972 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,9 @@ go get nhooyr.io/websocket ## Examples For a production quality example that demonstrates the complete API, see the -[echo example](./examples/echo). +[echo example](./internal/examples/echo). -For a full stack example, see the [chat example](./examples/chat). +For a full stack example, see the [chat example](./internal/examples/chat). ### Server diff --git a/ci/lint.sh b/ci/lint.sh index a8ab3027..80f309be 100755 --- a/ci/lint.sh +++ b/ci/lint.sh @@ -12,3 +12,26 @@ GOOS=js GOARCH=wasm golint -set_exit_status ./... go install honnef.co/go/tools/cmd/staticcheck@latest staticcheck ./... GOOS=js GOARCH=wasm staticcheck ./... + +govulncheck() { + tmpf=$(mktemp) + if ! command govulncheck "$@" >"$tmpf" 2>&1; then + cat "$tmpf" + fi +} +go install golang.org/x/vuln/cmd/govulncheck@latest +govulncheck ./... +GOOS=js GOARCH=wasm govulncheck ./... + +( + cd ./internal/examples + go vet ./... + staticcheck ./... + govulncheck ./... +) +( + cd ./internal/thirdparty + go vet ./... + staticcheck ./... + govulncheck ./... +) diff --git a/ci/test.sh b/ci/test.sh index 1b3d6cc3..32bdcec1 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -12,3 +12,12 @@ sed -i.bak '/examples/d' ci/out/coverage.prof go tool cover -func ci/out/coverage.prof | tail -n1 go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html + +( + cd ./internal/examples + go test "$@" ./... +) +( + cd ./internal/thirdparty + go test "$@" ./... +) diff --git a/conn_test.go b/conn_test.go index b9e2063d..d80acce2 100644 --- a/conn_test.go +++ b/conn_test.go @@ -1,11 +1,11 @@ //go:build !js -// +build !js package websocket_test import ( "bytes" "context" + "errors" "fmt" "io" "net/http" @@ -16,8 +16,6 @@ import ( "testing" "time" - "github.com/gin-gonic/gin" - "nhooyr.io/websocket" "nhooyr.io/websocket/internal/errd" "nhooyr.io/websocket/internal/test/assert" @@ -140,7 +138,9 @@ func TestConn(t *testing.T) { defer cancel() err = c1.Write(ctx, websocket.MessageText, []byte("x")) - assert.Equal(t, "write error", context.DeadlineExceeded, err) + if !errors.Is(err, context.DeadlineExceeded) { + t.Fatalf("unexpected error: %#v", err) + } }) t.Run("netConn", func(t *testing.T) { @@ -482,37 +482,3 @@ func echoServer(w http.ResponseWriter, r *http.Request, opts *websocket.AcceptOp err = wstest.EchoLoop(r.Context(), c) return assertCloseStatus(websocket.StatusNormalClosure, err) } - -func TestGin(t *testing.T) { - t.Parallel() - - gin.SetMode(gin.ReleaseMode) - r := gin.New() - r.GET("/", func(ginCtx *gin.Context) { - err := echoServer(ginCtx.Writer, ginCtx.Request, nil) - if err != nil { - t.Error(err) - } - }) - - s := httptest.NewServer(r) - defer s.Close() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) - defer cancel() - - c, _, err := websocket.Dial(ctx, s.URL, nil) - assert.Success(t, err) - defer c.Close(websocket.StatusInternalError, "") - - err = wsjson.Write(ctx, c, "hello") - assert.Success(t, err) - - var v interface{} - err = wsjson.Read(ctx, c, &v) - assert.Success(t, err) - assert.Equal(t, "read msg", "hello", v) - - err = c.Close(websocket.StatusNormalClosure, "") - assert.Success(t, err) -} diff --git a/frame_test.go b/frame_test.go index 2f4f2e25..e697e198 100644 --- a/frame_test.go +++ b/frame_test.go @@ -12,10 +12,6 @@ import ( "strconv" "testing" "time" - _ "unsafe" - - "github.com/gobwas/ws" - _ "github.com/gorilla/websocket" "nhooyr.io/websocket/internal/test/assert" ) @@ -109,87 +105,3 @@ func Test_mask(t *testing.T) { expKey32 := bits.RotateLeft32(key32, -8) assert.Equal(t, "key32", expKey32, gotKey32) } - -func basicMask(maskKey [4]byte, pos int, b []byte) int { - for i := range b { - b[i] ^= maskKey[pos&3] - pos++ - } - return pos & 3 -} - -//go:linkname gorillaMaskBytes github.com/gorilla/websocket.maskBytes -func gorillaMaskBytes(key [4]byte, pos int, b []byte) int - -func Benchmark_mask(b *testing.B) { - sizes := []int{ - 2, - 3, - 4, - 8, - 16, - 32, - 128, - 512, - 4096, - 16384, - } - - fns := []struct { - name string - fn func(b *testing.B, key [4]byte, p []byte) - }{ - { - name: "basic", - fn: func(b *testing.B, key [4]byte, p []byte) { - for i := 0; i < b.N; i++ { - basicMask(key, 0, p) - } - }, - }, - - { - name: "nhooyr", - fn: func(b *testing.B, key [4]byte, p []byte) { - key32 := binary.LittleEndian.Uint32(key[:]) - b.ResetTimer() - - for i := 0; i < b.N; i++ { - mask(key32, p) - } - }, - }, - { - name: "gorilla", - fn: func(b *testing.B, key [4]byte, p []byte) { - for i := 0; i < b.N; i++ { - gorillaMaskBytes(key, 0, p) - } - }, - }, - { - name: "gobwas", - fn: func(b *testing.B, key [4]byte, p []byte) { - for i := 0; i < b.N; i++ { - ws.Cipher(p, key, 0) - } - }, - }, - } - - key := [4]byte{1, 2, 3, 4} - - for _, size := range sizes { - p := make([]byte, size) - - b.Run(strconv.Itoa(size), func(b *testing.B) { - for _, fn := range fns { - b.Run(fn.name, func(b *testing.B) { - b.SetBytes(int64(size)) - - fn.fn(b, key, p) - }) - } - }) - } -} diff --git a/go.mod b/go.mod index 95a1df92..715a9f7a 100644 --- a/go.mod +++ b/go.mod @@ -1,40 +1,3 @@ module nhooyr.io/websocket go 1.19 - -require ( - github.com/gin-gonic/gin v1.9.1 - github.com/gobwas/ws v1.3.0 - github.com/google/go-cmp v0.5.9 - github.com/gorilla/websocket v1.5.0 - golang.org/x/time v0.3.0 -) - -require ( - github.com/bytedance/sonic v1.9.1 // indirect - github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect - github.com/gabriel-vasile/mimetype v1.4.2 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.14.0 // indirect - github.com/gobwas/httphead v0.1.0 // indirect - github.com/gobwas/pool v0.2.1 // indirect - github.com/goccy/go-json v0.10.2 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.4 // indirect - github.com/leodido/go-urn v1.2.4 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.0.8 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.11 // indirect - golang.org/x/arch v0.3.0 // indirect - golang.org/x/crypto v0.9.0 // indirect - golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.8.0 // indirect - golang.org/x/text v0.9.0 // indirect - google.golang.org/protobuf v1.30.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/go.sum b/go.sum index dc4743dd..e69de29b 100644 --- a/go.sum +++ b/go.sum @@ -1,95 +0,0 @@ -github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= -github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= -github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= -github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= -github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= -github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= -github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= -github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= -github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= -github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= -github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.3.0 h1:sbeU3Y4Qzlb+MOzIe6mQGf7QR4Hkv6ZD0qhGkBFL2O0= -github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= -github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= -github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= -github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= -github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= -golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/examples/README.md b/internal/examples/README.md similarity index 100% rename from examples/README.md rename to internal/examples/README.md diff --git a/examples/chat/README.md b/internal/examples/chat/README.md similarity index 100% rename from examples/chat/README.md rename to internal/examples/chat/README.md diff --git a/examples/chat/chat.go b/internal/examples/chat/chat.go similarity index 100% rename from examples/chat/chat.go rename to internal/examples/chat/chat.go diff --git a/examples/chat/chat_test.go b/internal/examples/chat/chat_test.go similarity index 100% rename from examples/chat/chat_test.go rename to internal/examples/chat/chat_test.go diff --git a/examples/chat/index.css b/internal/examples/chat/index.css similarity index 100% rename from examples/chat/index.css rename to internal/examples/chat/index.css diff --git a/examples/chat/index.html b/internal/examples/chat/index.html similarity index 100% rename from examples/chat/index.html rename to internal/examples/chat/index.html diff --git a/examples/chat/index.js b/internal/examples/chat/index.js similarity index 100% rename from examples/chat/index.js rename to internal/examples/chat/index.js diff --git a/examples/chat/main.go b/internal/examples/chat/main.go similarity index 100% rename from examples/chat/main.go rename to internal/examples/chat/main.go diff --git a/examples/echo/README.md b/internal/examples/echo/README.md similarity index 100% rename from examples/echo/README.md rename to internal/examples/echo/README.md diff --git a/examples/echo/main.go b/internal/examples/echo/main.go similarity index 100% rename from examples/echo/main.go rename to internal/examples/echo/main.go diff --git a/examples/echo/server.go b/internal/examples/echo/server.go similarity index 100% rename from examples/echo/server.go rename to internal/examples/echo/server.go diff --git a/examples/echo/server_test.go b/internal/examples/echo/server_test.go similarity index 100% rename from examples/echo/server_test.go rename to internal/examples/echo/server_test.go diff --git a/internal/examples/go.mod b/internal/examples/go.mod new file mode 100644 index 00000000..ef4c5f67 --- /dev/null +++ b/internal/examples/go.mod @@ -0,0 +1,11 @@ +module nhooyr.io/websocket/examples + +go 1.22 + +replace nhooyr.io/websocket => ../.. + +require ( + github.com/klauspost/compress v1.10.3 // indirect + golang.org/x/time v0.3.0 // indirect + nhooyr.io/websocket v1.8.7 // indirect +) diff --git a/internal/examples/go.sum b/internal/examples/go.sum new file mode 100644 index 00000000..03aa32c2 --- /dev/null +++ b/internal/examples/go.sum @@ -0,0 +1,41 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= diff --git a/internal/test/assert/assert.go b/internal/test/assert/assert.go index e37e9573..64c938c5 100644 --- a/internal/test/assert/assert.go +++ b/internal/test/assert/assert.go @@ -5,24 +5,14 @@ import ( "reflect" "strings" "testing" - - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" ) -// Diff returns a human readable diff between v1 and v2 -func Diff(v1, v2 interface{}) string { - return cmp.Diff(v1, v2, cmpopts.EquateErrors(), cmp.Exporter(func(r reflect.Type) bool { - return true - })) -} - // Equal asserts exp == act. -func Equal(t testing.TB, name string, exp, act interface{}) { +func Equal(t testing.TB, name string, exp, got interface{}) { t.Helper() - if diff := Diff(exp, act); diff != "" { - t.Fatalf("unexpected %v: %v", name, diff) + if !reflect.DeepEqual(exp, got) { + t.Fatalf("unexpected %v: expected %#v but got %#v", name, exp, got) } } diff --git a/internal/test/wstest/echo.go b/internal/test/wstest/echo.go index 0938a138..dc21a8f0 100644 --- a/internal/test/wstest/echo.go +++ b/internal/test/wstest/echo.go @@ -8,7 +8,6 @@ import ( "time" "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/test/assert" "nhooyr.io/websocket/internal/test/xrand" "nhooyr.io/websocket/internal/xsync" ) @@ -76,7 +75,7 @@ func Echo(ctx context.Context, c *websocket.Conn, max int) error { } if !bytes.Equal(msg, act) { - return fmt.Errorf("unexpected msg read: %v", assert.Diff(msg, act)) + return fmt.Errorf("unexpected msg read: %#v", act) } return nil diff --git a/internal/thirdparty/doc.go b/internal/thirdparty/doc.go new file mode 100644 index 00000000..e756d09f --- /dev/null +++ b/internal/thirdparty/doc.go @@ -0,0 +1,2 @@ +// Package thirdparty contains third party benchmarks and tests. +package thirdparty diff --git a/internal/thirdparty/frame_test.go b/internal/thirdparty/frame_test.go new file mode 100644 index 00000000..1a0ed125 --- /dev/null +++ b/internal/thirdparty/frame_test.go @@ -0,0 +1,100 @@ +package thirdparty + +import ( + "encoding/binary" + "strconv" + "testing" + _ "unsafe" + + "github.com/gobwas/ws" + _ "github.com/gorilla/websocket" + + _ "nhooyr.io/websocket" +) + +func basicMask(maskKey [4]byte, pos int, b []byte) int { + for i := range b { + b[i] ^= maskKey[pos&3] + pos++ + } + return pos & 3 +} + +//go:linkname gorillaMaskBytes github.com/gorilla/websocket.maskBytes +func gorillaMaskBytes(key [4]byte, pos int, b []byte) int + +//go:linkname mask nhooyr.io/websocket.mask +func mask(key32 uint32, b []byte) int + +func Benchmark_mask(b *testing.B) { + sizes := []int{ + 2, + 3, + 4, + 8, + 16, + 32, + 128, + 512, + 4096, + 16384, + } + + fns := []struct { + name string + fn func(b *testing.B, key [4]byte, p []byte) + }{ + { + name: "basic", + fn: func(b *testing.B, key [4]byte, p []byte) { + for i := 0; i < b.N; i++ { + basicMask(key, 0, p) + } + }, + }, + + { + name: "nhooyr", + fn: func(b *testing.B, key [4]byte, p []byte) { + key32 := binary.LittleEndian.Uint32(key[:]) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + mask(key32, p) + } + }, + }, + { + name: "gorilla", + fn: func(b *testing.B, key [4]byte, p []byte) { + for i := 0; i < b.N; i++ { + gorillaMaskBytes(key, 0, p) + } + }, + }, + { + name: "gobwas", + fn: func(b *testing.B, key [4]byte, p []byte) { + for i := 0; i < b.N; i++ { + ws.Cipher(p, key, 0) + } + }, + }, + } + + key := [4]byte{1, 2, 3, 4} + + for _, size := range sizes { + p := make([]byte, size) + + b.Run(strconv.Itoa(size), func(b *testing.B) { + for _, fn := range fns { + b.Run(fn.name, func(b *testing.B) { + b.SetBytes(int64(size)) + + fn.fn(b, key, p) + }) + } + }) + } +} diff --git a/internal/thirdparty/gin_test.go b/internal/thirdparty/gin_test.go new file mode 100644 index 00000000..6d59578d --- /dev/null +++ b/internal/thirdparty/gin_test.go @@ -0,0 +1,75 @@ +package thirdparty + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gin-gonic/gin" + + "nhooyr.io/websocket" + "nhooyr.io/websocket/internal/errd" + "nhooyr.io/websocket/internal/test/assert" + "nhooyr.io/websocket/internal/test/wstest" + "nhooyr.io/websocket/wsjson" +) + +func TestGin(t *testing.T) { + t.Parallel() + + gin.SetMode(gin.ReleaseMode) + r := gin.New() + r.GET("/", func(ginCtx *gin.Context) { + err := echoServer(ginCtx.Writer, ginCtx.Request, nil) + if err != nil { + t.Error(err) + } + }) + + s := httptest.NewServer(r) + defer s.Close() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + + c, _, err := websocket.Dial(ctx, s.URL, nil) + assert.Success(t, err) + defer c.Close(websocket.StatusInternalError, "") + + err = wsjson.Write(ctx, c, "hello") + assert.Success(t, err) + + var v interface{} + err = wsjson.Read(ctx, c, &v) + assert.Success(t, err) + assert.Equal(t, "read msg", "hello", v) + + err = c.Close(websocket.StatusNormalClosure, "") + assert.Success(t, err) +} + +func echoServer(w http.ResponseWriter, r *http.Request, opts *websocket.AcceptOptions) (err error) { + defer errd.Wrap(&err, "echo server failed") + + c, err := websocket.Accept(w, r, opts) + if err != nil { + return err + } + defer c.Close(websocket.StatusInternalError, "") + + err = wstest.EchoLoop(r.Context(), c) + return assertCloseStatus(websocket.StatusNormalClosure, err) +} + +func assertCloseStatus(exp websocket.StatusCode, err error) error { + if websocket.CloseStatus(err) == -1 { + return fmt.Errorf("expected websocket.CloseError: %T %v", err, err) + } + if websocket.CloseStatus(err) != exp { + return fmt.Errorf("expected close status %v but got %v", exp, err) + } + return nil +} diff --git a/internal/thirdparty/go.mod b/internal/thirdparty/go.mod new file mode 100644 index 00000000..b0a979f5 --- /dev/null +++ b/internal/thirdparty/go.mod @@ -0,0 +1,41 @@ +module nhooyr.io/websocket/internal/thirdparty + +go 1.22 + +replace nhooyr.io/websocket => ../.. + +require ( + github.com/gin-gonic/gin v1.9.1 + github.com/gobwas/ws v1.3.0 + github.com/gorilla/websocket v1.5.0 + nhooyr.io/websocket v1.8.7 +) + +require ( + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/gobwas/httphead v0.1.0 // indirect + github.com/gobwas/pool v0.2.1 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.9.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/internal/thirdparty/go.sum b/internal/thirdparty/go.sum new file mode 100644 index 00000000..80e4ad52 --- /dev/null +++ b/internal/thirdparty/go.sum @@ -0,0 +1,94 @@ +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.3.0 h1:sbeU3Y4Qzlb+MOzIe6mQGf7QR4Hkv6ZD0qhGkBFL2O0= +github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= From b4b86b904ee818dc480b8b7384bd92a751a5c0ee Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 02:17:45 -0700 Subject: [PATCH 388/519] dial.go: Use timeout on HTTPClient properly Closes #341 --- conn_test.go | 31 +++++++++++++++++++++++++++++++ dial.go | 9 +++++---- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/conn_test.go b/conn_test.go index d80acce2..59661b73 100644 --- a/conn_test.go +++ b/conn_test.go @@ -264,6 +264,37 @@ func TestConn(t *testing.T) { err = c1.Close(websocket.StatusNormalClosure, "") assert.Success(t, err) }) + + t.Run("HTTPClient.Timeout", func(t *testing.T) { + tt, c1, c2 := newConnTest(t, &websocket.DialOptions{ + HTTPClient: &http.Client{Timeout: time.Second*5}, + }, nil) + + tt.goEchoLoop(c2) + + c1.SetReadLimit(1 << 30) + + exp := xrand.String(xrand.Int(131072)) + + werr := xsync.Go(func() error { + return wsjson.Write(tt.ctx, c1, exp) + }) + + var act interface{} + err := wsjson.Read(tt.ctx, c1, &act) + assert.Success(t, err) + assert.Equal(t, "read msg", exp, act) + + select { + case err := <-werr: + assert.Success(t, err) + case <-tt.ctx.Done(): + t.Fatal(tt.ctx.Err()) + } + + err = c1.Close(websocket.StatusNormalClosure, "") + assert.Success(t, err) + }) } func TestWasm(t *testing.T) { diff --git a/dial.go b/dial.go index 510b94b1..0f2735da 100644 --- a/dial.go +++ b/dial.go @@ -59,12 +59,13 @@ func (opts *DialOptions) cloneWithDefaults(ctx context.Context) (context.Context } if o.HTTPClient == nil { o.HTTPClient = http.DefaultClient - } else if opts.HTTPClient.Timeout > 0 { - ctx, cancel = context.WithTimeout(ctx, opts.HTTPClient.Timeout) + } + if o.HTTPClient.Timeout > 0 { + ctx, cancel = context.WithTimeout(ctx, o.HTTPClient.Timeout) - newClient := *opts.HTTPClient + newClient := *o.HTTPClient newClient.Timeout = 0 - opts.HTTPClient = &newClient + o.HTTPClient = &newClient } if o.HTTPHeader == nil { o.HTTPHeader = http.Header{} From a6b946487cbd40aaa9867930235c1d2ed7017f53 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 02:23:30 -0700 Subject: [PATCH 389/519] conn: Add noCopy Closes #349 --- conn.go | 5 +++++ ws_js.go | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/conn.go b/conn.go index ab37248e..17a6b966 100644 --- a/conn.go +++ b/conn.go @@ -42,6 +42,8 @@ const ( // This applies to context expirations as well unfortunately. // See https://github.com/nhooyr/websocket/issues/242#issuecomment-633182220 type Conn struct { + noCopy + subprotocol string rwc io.ReadWriteCloser client bool @@ -288,3 +290,6 @@ func (m *mu) unlock() { default: } } + +type noCopy struct{} +func (*noCopy) Lock() {} diff --git a/ws_js.go b/ws_js.go index 3248933c..05f2202e 100644 --- a/ws_js.go +++ b/ws_js.go @@ -40,6 +40,7 @@ const ( // Conn provides a wrapper around the browser WebSocket API. type Conn struct { + noCopy ws wsjs.WebSocket // read limit for a message in bytes. @@ -563,3 +564,6 @@ func (m *mu) unlock() { default: } } + +type noCopy struct{} +func (*noCopy) Lock() {} From 4e15d756f556869a9f170f7b52ac357e9b6ae888 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 02:32:37 -0700 Subject: [PATCH 390/519] ci/bench.sh: Add --- .github/workflows/daily.yml | 10 +++++++++- ci/bench.sh | 9 +++++++++ make.sh | 1 + 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100755 ci/bench.sh diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index cbac574d..d10c142f 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -8,7 +8,15 @@ concurrency: cancel-in-progress: true jobs: - ci: + bench: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v4 + with: + go-version-file: ./go.mod + - run: AUTOBAHN=1 ./ci/bench.sh + test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/ci/bench.sh b/ci/bench.sh new file mode 100755 index 00000000..31bf2f15 --- /dev/null +++ b/ci/bench.sh @@ -0,0 +1,9 @@ +#!/bin/sh +set -eu +cd -- "$(dirname "$0")/.." + +go test --bench=. "$@" ./... +( + cd ./internal/thirdparty + go test --bench=. "$@" ./... +) diff --git a/make.sh b/make.sh index 6f5d1f57..68a98ac1 100755 --- a/make.sh +++ b/make.sh @@ -5,3 +5,4 @@ cd -- "$(dirname "$0")" ./ci/fmt.sh ./ci/lint.sh ./ci/test.sh +./ci/bench.sh From a02cbef5605d23c97972fbea8dd16488cf437b7a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 03:34:15 -0700 Subject: [PATCH 391/519] compress.go: Fix context takeover --- accept.go | 1 + ci/bench.sh | 4 ++-- compress.go | 16 ++++++---------- conn.go | 1 + conn_test.go | 4 ++-- dial_test.go | 3 ++- export_test.go | 6 ++++-- internal/util/util.go | 7 +++++++ internal/xsync/go.go | 3 ++- read.go | 27 ++++++++++++++++----------- write.go | 11 +++-------- ws_js.go | 1 + 12 files changed, 47 insertions(+), 37 deletions(-) diff --git a/accept.go b/accept.go index ff2033e7..6c63e730 100644 --- a/accept.go +++ b/accept.go @@ -269,6 +269,7 @@ func acceptDeflate(w http.ResponseWriter, ext websocketExtension, mode Compressi if strings.HasPrefix(p, "client_max_window_bits") { // We cannot adjust the read sliding window so cannot make use of this. + // By not responding to it, we tell the client we're ignoring it. continue } diff --git a/ci/bench.sh b/ci/bench.sh index 31bf2f15..8f99278d 100755 --- a/ci/bench.sh +++ b/ci/bench.sh @@ -2,8 +2,8 @@ set -eu cd -- "$(dirname "$0")/.." -go test --bench=. "$@" ./... +go test --run=^$ --bench=. "$@" ./... ( cd ./internal/thirdparty - go test --bench=. "$@" ./... + go test --run=^$ --bench=. "$@" ./... ) diff --git a/compress.go b/compress.go index e6722fc7..61e6e268 100644 --- a/compress.go +++ b/compress.go @@ -31,7 +31,7 @@ const ( CompressionDisabled CompressionMode = iota // CompressionContextTakeover uses a 32 kB sliding window and flate.Writer per connection. - // It reusing the sliding window from previous messages. + // It reuses the sliding window from previous messages. // As most WebSocket protocols are repetitive, this can be very efficient. // It carries an overhead of 32 kB + 1.2 MB for every connection compared to CompressionNoContextTakeover. // @@ -80,7 +80,7 @@ func (copts *compressionOptions) setHeader(h http.Header) { // They are removed when sending to avoid the overhead as // WebSocket framing tell's when the message has ended but then // we need to add them back otherwise flate.Reader keeps -// trying to return more bytes. +// trying to read more bytes. const deflateMessageTail = "\x00\x00\xff\xff" type trimLastFourBytesWriter struct { @@ -201,23 +201,19 @@ func (sw *slidingWindow) init(n int) { } p := slidingWindowPool(n) - buf, ok := p.Get().(*[]byte) + sw2, ok := p.Get().(*slidingWindow) if ok { - sw.buf = (*buf)[:0] + *sw = *sw2 } else { sw.buf = make([]byte, 0, n) } } func (sw *slidingWindow) close() { - if sw.buf == nil { - return - } - + sw.buf = sw.buf[:0] swPoolMu.Lock() - swPool[cap(sw.buf)].Put(&sw.buf) + swPool[cap(sw.buf)].Put(sw) swPoolMu.Unlock() - sw.buf = nil } func (sw *slidingWindow) write(p []byte) { diff --git a/conn.go b/conn.go index 17a6b966..81a57c7f 100644 --- a/conn.go +++ b/conn.go @@ -292,4 +292,5 @@ func (m *mu) unlock() { } type noCopy struct{} + func (*noCopy) Lock() {} diff --git a/conn_test.go b/conn_test.go index 59661b73..7a6a0c39 100644 --- a/conn_test.go +++ b/conn_test.go @@ -267,7 +267,7 @@ func TestConn(t *testing.T) { t.Run("HTTPClient.Timeout", func(t *testing.T) { tt, c1, c2 := newConnTest(t, &websocket.DialOptions{ - HTTPClient: &http.Client{Timeout: time.Second*5}, + HTTPClient: &http.Client{Timeout: time.Second * 5}, }, nil) tt.goEchoLoop(c2) @@ -458,7 +458,7 @@ func BenchmarkConn(b *testing.B) { typ, r, err := c1.Reader(bb.ctx) if err != nil { - b.Fatal(err) + b.Fatal(i, err) } if websocket.MessageText != typ { assert.Equal(b, "data type", websocket.MessageText, typ) diff --git a/dial_test.go b/dial_test.go index 8680147e..e072db2d 100644 --- a/dial_test.go +++ b/dial_test.go @@ -15,6 +15,7 @@ import ( "time" "nhooyr.io/websocket/internal/test/assert" + "nhooyr.io/websocket/internal/util" ) func TestBadDials(t *testing.T) { @@ -27,7 +28,7 @@ func TestBadDials(t *testing.T) { name string url string opts *DialOptions - rand readerFunc + rand util.ReaderFunc nilCtx bool }{ { diff --git a/export_test.go b/export_test.go index d618a154..8731b6d8 100644 --- a/export_test.go +++ b/export_test.go @@ -3,9 +3,11 @@ package websocket +import "nhooyr.io/websocket/internal/util" + func (c *Conn) RecordBytesWritten() *int { var bytesWritten int - c.bw.Reset(writerFunc(func(p []byte) (int, error) { + c.bw.Reset(util.WriterFunc(func(p []byte) (int, error) { bytesWritten += len(p) return c.rwc.Write(p) })) @@ -14,7 +16,7 @@ func (c *Conn) RecordBytesWritten() *int { func (c *Conn) RecordBytesRead() *int { var bytesRead int - c.br.Reset(readerFunc(func(p []byte) (int, error) { + c.br.Reset(util.ReaderFunc(func(p []byte) (int, error) { n, err := c.rwc.Read(p) bytesRead += n return n, err diff --git a/internal/util/util.go b/internal/util/util.go index f23fb67b..aa210703 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -6,3 +6,10 @@ type WriterFunc func(p []byte) (int, error) func (f WriterFunc) Write(p []byte) (int, error) { return f(p) } + +// ReaderFunc is used to implement one off io.Readers. +type ReaderFunc func(p []byte) (int, error) + +func (f ReaderFunc) Read(p []byte) (int, error) { + return f(p) +} diff --git a/internal/xsync/go.go b/internal/xsync/go.go index 7a61f27f..5229b12a 100644 --- a/internal/xsync/go.go +++ b/internal/xsync/go.go @@ -2,6 +2,7 @@ package xsync import ( "fmt" + "runtime/debug" ) // Go allows running a function in another goroutine @@ -13,7 +14,7 @@ func Go(fn func() error) <-chan error { r := recover() if r != nil { select { - case errs <- fmt.Errorf("panic in go fn: %v", r): + case errs <- fmt.Errorf("panic in go fn: %v, %s", r, debug.Stack()): default: } } diff --git a/read.go b/read.go index 7bc6f20d..d3217861 100644 --- a/read.go +++ b/read.go @@ -13,6 +13,7 @@ import ( "time" "nhooyr.io/websocket/internal/errd" + "nhooyr.io/websocket/internal/util" "nhooyr.io/websocket/internal/xsync" ) @@ -101,13 +102,20 @@ func newMsgReader(c *Conn) *msgReader { func (mr *msgReader) resetFlate() { if mr.flateContextTakeover() { + if mr.dict == nil { + mr.dict = &slidingWindow{} + } mr.dict.init(32768) } if mr.flateBufio == nil { mr.flateBufio = getBufioReader(mr.readFunc) } - mr.flateReader = getFlateReader(mr.flateBufio, mr.dict.buf) + if mr.flateContextTakeover() { + mr.flateReader = getFlateReader(mr.flateBufio, mr.dict.buf) + } else { + mr.flateReader = getFlateReader(mr.flateBufio, nil) + } mr.limitReader.r = mr.flateReader mr.flateTail.Reset(deflateMessageTail) } @@ -122,7 +130,10 @@ func (mr *msgReader) putFlateReader() { func (mr *msgReader) close() { mr.c.readMu.forceLock() mr.putFlateReader() - mr.dict.close() + if mr.dict != nil { + mr.dict.close() + mr.dict = nil + } if mr.flateBufio != nil { putBufioReader(mr.flateBufio) } @@ -348,14 +359,14 @@ type msgReader struct { flateBufio *bufio.Reader flateTail strings.Reader limitReader *limitReader - dict slidingWindow + dict *slidingWindow fin bool payloadLength int64 maskKey uint32 - // readerFunc(mr.Read) to avoid continuous allocations. - readFunc readerFunc + // util.ReaderFunc(mr.Read) to avoid continuous allocations. + readFunc util.ReaderFunc } func (mr *msgReader) reset(ctx context.Context, h header) { @@ -484,9 +495,3 @@ func (lr *limitReader) Read(p []byte) (int, error) { } return n, err } - -type readerFunc func(p []byte) (int, error) - -func (f readerFunc) Read(p []byte) (int, error) { - return f(p) -} diff --git a/write.go b/write.go index 7921eac9..500609dd 100644 --- a/write.go +++ b/write.go @@ -16,6 +16,7 @@ import ( "compress/flate" "nhooyr.io/websocket/internal/errd" + "nhooyr.io/websocket/internal/util" ) // Writer returns a writer bounded by the context that will write @@ -93,7 +94,7 @@ func newMsgWriterState(c *Conn) *msgWriterState { func (mw *msgWriterState) ensureFlate() { if mw.trimWriter == nil { mw.trimWriter = &trimLastFourBytesWriter{ - w: writerFunc(mw.write), + w: util.WriterFunc(mw.write), } } @@ -380,17 +381,11 @@ func (c *Conn) writeFramePayload(p []byte) (n int, err error) { return n, nil } -type writerFunc func(p []byte) (int, error) - -func (f writerFunc) Write(p []byte) (int, error) { - return f(p) -} - // extractBufioWriterBuf grabs the []byte backing a *bufio.Writer // and returns it. func extractBufioWriterBuf(bw *bufio.Writer, w io.Writer) []byte { var writeBuf []byte - bw.Reset(writerFunc(func(p2 []byte) (int, error) { + bw.Reset(util.WriterFunc(func(p2 []byte) (int, error) { writeBuf = p2[:cap(p2)] return len(p2), nil })) diff --git a/ws_js.go b/ws_js.go index 05f2202e..9f0e19e9 100644 --- a/ws_js.go +++ b/ws_js.go @@ -566,4 +566,5 @@ func (m *mu) unlock() { } type noCopy struct{} + func (*noCopy) Lock() {} From 81afa8a34970dc1f5b2a59084a17d1a1a8d248ea Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 04:30:08 -0700 Subject: [PATCH 392/519] netconn: Avoid returning 0, nil in NetConn.Read Closes #367 --- netconn.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/netconn.go b/netconn.go index 74000c9e..e398b4f7 100644 --- a/netconn.go +++ b/netconn.go @@ -141,6 +141,19 @@ func (nc *netConn) Read(p []byte) (int, error) { nc.readMu.forceLock() defer nc.readMu.unlock() + for { + n, err := nc.read(p) + if err != nil { + return n, err + } + if n == 0 { + continue + } + return n, nil + } +} + +func (nc *netConn) read(p []byte) (int, error) { if atomic.LoadInt64(&nc.readExpired) == 1 { return 0, fmt.Errorf("failed to read: %w", context.DeadlineExceeded) } From 136f95448245daf0643ce6524382ccf80264d36e Mon Sep 17 00:00:00 2001 From: Andy Bursavich Date: Tue, 8 Sep 2020 17:22:22 -0700 Subject: [PATCH 393/519] Client allows server to specify server_max_window_bits --- dial.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dial.go b/dial.go index 0f2735da..9acca133 100644 --- a/dial.go +++ b/dial.go @@ -273,6 +273,10 @@ func verifyServerExtensions(copts *compressionOptions, h http.Header) (*compress copts.serverNoContextTakeover = true continue } + if strings.HasPrefix(p, "server_max_window_bits=") { + // We can't adjust the deflate window, but decoding with a larger window is acceptable. + continue + } return nil, fmt.Errorf("unsupported permessage-deflate parameter: %q", p) } From 2291d83f761e83e3cb3946529d59f38309212a16 Mon Sep 17 00:00:00 2001 From: Andy Bursavich Date: Tue, 8 Sep 2020 17:14:43 -0700 Subject: [PATCH 394/519] Server allows client to specify server_max_window_bits=15 --- accept.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/accept.go b/accept.go index 6c63e730..15e14285 100644 --- a/accept.go +++ b/accept.go @@ -265,6 +265,8 @@ func acceptDeflate(w http.ResponseWriter, ext websocketExtension, mode Compressi case "server_no_context_takeover": copts.serverNoContextTakeover = true continue + case "server_max_window_bits=15": + continue } if strings.HasPrefix(p, "client_max_window_bits") { From 711aa3f7aa251ac5628122bf0871cd59a32e9ce5 Mon Sep 17 00:00:00 2001 From: Andy Bursavich Date: Tue, 8 Sep 2020 19:29:18 -0700 Subject: [PATCH 395/519] Server selects first acceptable compression offer Unacceptable offers are declined without rejecting the request. --- accept.go | 41 ++++++------- accept_test.go | 109 +++++++++++++++++++++------------ compress.go | 5 +- dial.go | 2 +- internal/test/assert/assert.go | 10 +++ 5 files changed, 102 insertions(+), 65 deletions(-) diff --git a/accept.go b/accept.go index 15e14285..19e388ec 100644 --- a/accept.go +++ b/accept.go @@ -123,9 +123,9 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con w.Header().Set("Sec-WebSocket-Protocol", subproto) } - copts, err := acceptCompression(r, w, opts.CompressionMode) - if err != nil { - return nil, err + copts, ok := selectDeflate(websocketExtensions(r.Header), opts.CompressionMode) + if ok { + w.Header().Set("Sec-WebSocket-Extensions", copts.String()) } w.WriteHeader(http.StatusSwitchingProtocols) @@ -238,25 +238,26 @@ func selectSubprotocol(r *http.Request, subprotocols []string) string { return "" } -func acceptCompression(r *http.Request, w http.ResponseWriter, mode CompressionMode) (*compressionOptions, error) { +func selectDeflate(extensions []websocketExtension, mode CompressionMode) (*compressionOptions, bool) { if mode == CompressionDisabled { - return nil, nil + return nil, false } - - for _, ext := range websocketExtensions(r.Header) { + for _, ext := range extensions { switch ext.name { // We used to implement x-webkit-deflate-fram too but Safari has bugs. // See https://github.com/nhooyr/websocket/issues/218 case "permessage-deflate": - return acceptDeflate(w, ext, mode) + copts, ok := acceptDeflate(ext, mode) + if ok { + return copts, true + } } } - return nil, nil + return nil, false } -func acceptDeflate(w http.ResponseWriter, ext websocketExtension, mode CompressionMode) (*compressionOptions, error) { +func acceptDeflate(ext websocketExtension, mode CompressionMode) (*compressionOptions, bool) { copts := mode.opts() - for _, p := range ext.params { switch p { case "client_no_context_takeover": @@ -265,24 +266,18 @@ func acceptDeflate(w http.ResponseWriter, ext websocketExtension, mode Compressi case "server_no_context_takeover": copts.serverNoContextTakeover = true continue - case "server_max_window_bits=15": + case "client_max_window_bits", + "server_max_window_bits=15": continue } - if strings.HasPrefix(p, "client_max_window_bits") { - // We cannot adjust the read sliding window so cannot make use of this. - // By not responding to it, we tell the client we're ignoring it. + if strings.HasPrefix(p, "client_max_window_bits=") { + // We can't adjust the deflate window, but decoding with a larger window is acceptable. continue } - - err := fmt.Errorf("unsupported permessage-deflate parameter: %q", p) - http.Error(w, err.Error(), http.StatusBadRequest) - return nil, err + return nil, false } - - copts.setHeader(w.Header()) - - return copts, nil + return copts, true } func headerContainsTokenIgnoreCase(h http.Header, key, token string) bool { diff --git a/accept_test.go b/accept_test.go index ae17c0b4..513313ec 100644 --- a/accept_test.go +++ b/accept_test.go @@ -62,20 +62,50 @@ func TestAccept(t *testing.T) { t.Run("badCompression", func(t *testing.T) { t.Parallel() - w := mockHijacker{ - ResponseWriter: httptest.NewRecorder(), + newRequest := func(extensions string) *http.Request { + r := httptest.NewRequest("GET", "/", nil) + r.Header.Set("Connection", "Upgrade") + r.Header.Set("Upgrade", "websocket") + r.Header.Set("Sec-WebSocket-Version", "13") + r.Header.Set("Sec-WebSocket-Key", "meow123") + r.Header.Set("Sec-WebSocket-Extensions", extensions) + return r + } + errHijack := errors.New("hijack error") + newResponseWriter := func() http.ResponseWriter { + return mockHijacker{ + ResponseWriter: httptest.NewRecorder(), + hijack: func() (net.Conn, *bufio.ReadWriter, error) { + return nil, nil, errHijack + }, + } } - r := httptest.NewRequest("GET", "/", nil) - r.Header.Set("Connection", "Upgrade") - r.Header.Set("Upgrade", "websocket") - r.Header.Set("Sec-WebSocket-Version", "13") - r.Header.Set("Sec-WebSocket-Key", "meow123") - r.Header.Set("Sec-WebSocket-Extensions", "permessage-deflate; harharhar") - _, err := Accept(w, r, &AcceptOptions{ - CompressionMode: CompressionContextTakeover, + t.Run("withoutFallback", func(t *testing.T) { + t.Parallel() + + w := newResponseWriter() + r := newRequest("permessage-deflate; harharhar") + _, err := Accept(w, r, &AcceptOptions{ + CompressionMode: CompressionNoContextTakeover, + }) + assert.ErrorIs(t, errHijack, err) + assert.Equal(t, "extension header", w.Header().Get("Sec-WebSocket-Extensions"), "") + }) + t.Run("withFallback", func(t *testing.T) { + t.Parallel() + + w := newResponseWriter() + r := newRequest("permessage-deflate; harharhar, permessage-deflate") + _, err := Accept(w, r, &AcceptOptions{ + CompressionMode: CompressionNoContextTakeover, + }) + assert.ErrorIs(t, errHijack, err) + assert.Equal(t, "extension header", + w.Header().Get("Sec-WebSocket-Extensions"), + CompressionNoContextTakeover.opts().String(), + ) }) - assert.Contains(t, err, `unsupported permessage-deflate parameter`) }) t.Run("requireHttpHijacker", func(t *testing.T) { @@ -344,42 +374,53 @@ func Test_authenticateOrigin(t *testing.T) { } } -func Test_acceptCompression(t *testing.T) { +func Test_selectDeflate(t *testing.T) { t.Parallel() testCases := []struct { - name string - mode CompressionMode - reqSecWebSocketExtensions string - respSecWebSocketExtensions string - expCopts *compressionOptions - error bool + name string + mode CompressionMode + header string + expCopts *compressionOptions + expOK bool }{ { name: "disabled", mode: CompressionDisabled, expCopts: nil, + expOK: false, }, { name: "noClientSupport", mode: CompressionNoContextTakeover, expCopts: nil, + expOK: false, }, { - name: "permessage-deflate", - mode: CompressionNoContextTakeover, - reqSecWebSocketExtensions: "permessage-deflate; client_max_window_bits", - respSecWebSocketExtensions: "permessage-deflate; client_no_context_takeover; server_no_context_takeover", + name: "permessage-deflate", + mode: CompressionNoContextTakeover, + header: "permessage-deflate; client_max_window_bits", expCopts: &compressionOptions{ clientNoContextTakeover: true, serverNoContextTakeover: true, }, + expOK: true, + }, + { + name: "permessage-deflate/unknown-parameter", + mode: CompressionNoContextTakeover, + header: "permessage-deflate; meow", + expOK: false, }, { - name: "permessage-deflate/error", - mode: CompressionNoContextTakeover, - reqSecWebSocketExtensions: "permessage-deflate; meow", - error: true, + name: "permessage-deflate/unknown-parameter", + mode: CompressionNoContextTakeover, + header: "permessage-deflate; meow, permessage-deflate; client_max_window_bits", + expCopts: &compressionOptions{ + clientNoContextTakeover: true, + serverNoContextTakeover: true, + }, + expOK: true, }, // { // name: "x-webkit-deflate-frame", @@ -404,19 +445,11 @@ func Test_acceptCompression(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - r := httptest.NewRequest(http.MethodGet, "/", nil) - r.Header.Set("Sec-WebSocket-Extensions", tc.reqSecWebSocketExtensions) - - w := httptest.NewRecorder() - copts, err := acceptCompression(r, w, tc.mode) - if tc.error { - assert.Error(t, err) - return - } - - assert.Success(t, err) + h := http.Header{} + h.Set("Sec-WebSocket-Extensions", tc.header) + copts, ok := selectDeflate(websocketExtensions(h), tc.mode) + assert.Equal(t, "selected options", tc.expOK, ok) assert.Equal(t, "compression options", tc.expCopts, copts) - assert.Equal(t, "Sec-WebSocket-Extensions", tc.respSecWebSocketExtensions, w.Header().Get("Sec-WebSocket-Extensions")) }) } } diff --git a/compress.go b/compress.go index 61e6e268..ee21e1d1 100644 --- a/compress.go +++ b/compress.go @@ -6,7 +6,6 @@ package websocket import ( "compress/flate" "io" - "net/http" "sync" ) @@ -65,7 +64,7 @@ type compressionOptions struct { serverNoContextTakeover bool } -func (copts *compressionOptions) setHeader(h http.Header) { +func (copts *compressionOptions) String() string { s := "permessage-deflate" if copts.clientNoContextTakeover { s += "; client_no_context_takeover" @@ -73,7 +72,7 @@ func (copts *compressionOptions) setHeader(h http.Header) { if copts.serverNoContextTakeover { s += "; server_no_context_takeover" } - h.Set("Sec-WebSocket-Extensions", s) + return s } // These bytes are required to get flate.Reader to return. diff --git a/dial.go b/dial.go index 9acca133..e72432e7 100644 --- a/dial.go +++ b/dial.go @@ -185,7 +185,7 @@ func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, copts req.Header.Set("Sec-WebSocket-Protocol", strings.Join(opts.Subprotocols, ",")) } if copts != nil { - copts.setHeader(req.Header) + req.Header.Set("Sec-WebSocket-Extensions", copts.String()) } resp, err := opts.HTTPClient.Do(req) diff --git a/internal/test/assert/assert.go b/internal/test/assert/assert.go index 64c938c5..1b90cc9f 100644 --- a/internal/test/assert/assert.go +++ b/internal/test/assert/assert.go @@ -1,6 +1,7 @@ package assert import ( + "errors" "fmt" "reflect" "strings" @@ -43,3 +44,12 @@ func Contains(t testing.TB, v interface{}, sub string) { t.Fatalf("expected %q to contain %q", s, sub) } } + +// ErrorIs asserts errors.Is(got, exp) +func ErrorIs(t testing.TB, exp, got error) { + t.Helper() + + if !errors.Is(got, exp) { + t.Fatalf("expected %v but got %v", exp, got) + } +} From d6b342b14042413308040566fcfd0d3f3ea85d10 Mon Sep 17 00:00:00 2001 From: Andy Bursavich Date: Tue, 8 Sep 2020 19:30:22 -0700 Subject: [PATCH 396/519] Remove x-webkit-deflate-frame dead code --- accept_test.go | 16 ---------------- compress.go | 6 +----- ws_js.go | 7 +------ 3 files changed, 2 insertions(+), 27 deletions(-) diff --git a/accept_test.go b/accept_test.go index 513313ec..c554bdaf 100644 --- a/accept_test.go +++ b/accept_test.go @@ -422,22 +422,6 @@ func Test_selectDeflate(t *testing.T) { }, expOK: true, }, - // { - // name: "x-webkit-deflate-frame", - // mode: CompressionNoContextTakeover, - // reqSecWebSocketExtensions: "x-webkit-deflate-frame; no_context_takeover", - // respSecWebSocketExtensions: "x-webkit-deflate-frame; no_context_takeover", - // expCopts: &compressionOptions{ - // clientNoContextTakeover: true, - // serverNoContextTakeover: true, - // }, - // }, - // { - // name: "x-webkit-deflate/error", - // mode: CompressionNoContextTakeover, - // reqSecWebSocketExtensions: "x-webkit-deflate-frame; max_window_bits", - // error: true, - // }, } for _, tc := range testCases { diff --git a/compress.go b/compress.go index ee21e1d1..81de751b 100644 --- a/compress.go +++ b/compress.go @@ -12,11 +12,7 @@ import ( // CompressionMode represents the modes available to the deflate extension. // See https://tools.ietf.org/html/rfc7692 // -// A compatibility layer is implemented for the older deflate-frame extension used -// by safari. See https://tools.ietf.org/html/draft-tyoshino-hybi-websocket-perframe-deflate-06 -// It will work the same in every way except that we cannot signal to the peer we -// want to use no context takeover on our side, we can only signal that they should. -// But it is currently disabled due to Safari bugs. See https://github.com/nhooyr/websocket/issues/218 +// Works in all browsers except Safari which does not implement the deflate extension. type CompressionMode int const ( diff --git a/ws_js.go b/ws_js.go index 9f0e19e9..e60601e3 100644 --- a/ws_js.go +++ b/ws_js.go @@ -485,12 +485,7 @@ func CloseStatus(err error) StatusCode { // CompressionMode represents the modes available to the deflate extension. // See https://tools.ietf.org/html/rfc7692 -// -// A compatibility layer is implemented for the older deflate-frame extension used -// by safari. See https://tools.ietf.org/html/draft-tyoshino-hybi-websocket-perframe-deflate-06 -// It will work the same in every way except that we cannot signal to the peer we -// want to use no context takeover on our side, we can only signal that they should. -// It is however currently disabled due to Safari bugs. See https://github.com/nhooyr/websocket/issues/218 +// Works in all browsers except Safari which does not implement the deflate extension. type CompressionMode int const ( From a975390c8cd69948d2e3e8f0665aaf131400f550 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 12:53:12 -0700 Subject: [PATCH 397/519] internal/*/go.mod: Use go 1.19 too --- internal/examples/go.mod | 2 +- internal/thirdparty/go.mod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/examples/go.mod b/internal/examples/go.mod index ef4c5f67..b5cdcc1d 100644 --- a/internal/examples/go.mod +++ b/internal/examples/go.mod @@ -1,6 +1,6 @@ module nhooyr.io/websocket/examples -go 1.22 +go 1.19 replace nhooyr.io/websocket => ../.. diff --git a/internal/thirdparty/go.mod b/internal/thirdparty/go.mod index b0a979f5..e8c3e2c0 100644 --- a/internal/thirdparty/go.mod +++ b/internal/thirdparty/go.mod @@ -1,6 +1,6 @@ module nhooyr.io/websocket/internal/thirdparty -go 1.22 +go 1.19 replace nhooyr.io/websocket => ../.. From 1dbc1412d602060f6362a66fdc181da79b8136a4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 18:17:02 -0700 Subject: [PATCH 398/519] write: Zero alloc writes with Writer Closes #354 --- .gitignore | 1 - ci/bench.sh | 4 ++-- conn.go | 9 ++++---- write.go | 62 +++++++++++++++++++++-------------------------------- 4 files changed, 31 insertions(+), 45 deletions(-) delete mode 100644 .gitignore diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 6961e5c8..00000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -websocket.test diff --git a/ci/bench.sh b/ci/bench.sh index 8f99278d..a553b93a 100755 --- a/ci/bench.sh +++ b/ci/bench.sh @@ -2,8 +2,8 @@ set -eu cd -- "$(dirname "$0")/.." -go test --run=^$ --bench=. "$@" ./... +go test --run=^$ --bench=. --benchmem --memprofile ci/out/prof.mem --cpuprofile ci/out/prof.cpu -o ci/out/websocket.test "$@" . ( cd ./internal/thirdparty - go test --run=^$ --bench=. "$@" ./... + go test --run=^$ --bench=. --benchmem --memprofile ../../ci/out/prof-thirdparty.mem --cpuprofile ../../ci/out/prof-thirdparty.cpu -o ../../ci/out/thirdparty.test "$@" . ) diff --git a/conn.go b/conn.go index 81a57c7f..78eaad82 100644 --- a/conn.go +++ b/conn.go @@ -63,7 +63,7 @@ type Conn struct { readCloseFrameErr error // Write state. - msgWriterState *msgWriterState + msgWriter *msgWriter writeFrameMu *mu writeBuf []byte writeHeaderBuf [8]byte @@ -113,14 +113,14 @@ func newConn(cfg connConfig) *Conn { c.msgReader = newMsgReader(c) - c.msgWriterState = newMsgWriterState(c) + c.msgWriter = newMsgWriter(c) if c.client { c.writeBuf = extractBufioWriterBuf(c.bw, c.rwc) } if c.flate() && c.flateThreshold == 0 { c.flateThreshold = 128 - if !c.msgWriterState.flateContextTakeover() { + if !c.msgWriter.flateContextTakeover() { c.flateThreshold = 512 } } @@ -157,8 +157,7 @@ func (c *Conn) close(err error) { c.rwc.Close() go func() { - c.msgWriterState.close() - + c.msgWriter.close() c.msgReader.close() }() } diff --git a/write.go b/write.go index 500609dd..20a71d3e 100644 --- a/write.go +++ b/write.go @@ -49,30 +49,11 @@ func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { } type msgWriter struct { - mw *msgWriterState - closed bool -} - -func (mw *msgWriter) Write(p []byte) (int, error) { - if mw.closed { - return 0, errors.New("cannot use closed writer") - } - return mw.mw.Write(p) -} - -func (mw *msgWriter) Close() error { - if mw.closed { - return errors.New("cannot use closed writer") - } - mw.closed = true - return mw.mw.Close() -} - -type msgWriterState struct { c *Conn mu *mu writeMu *mu + closed bool ctx context.Context opcode opcode @@ -82,8 +63,8 @@ type msgWriterState struct { flateWriter *flate.Writer } -func newMsgWriterState(c *Conn) *msgWriterState { - mw := &msgWriterState{ +func newMsgWriter(c *Conn) *msgWriter { + mw := &msgWriter{ c: c, mu: newMu(c), writeMu: newMu(c), @@ -91,7 +72,7 @@ func newMsgWriterState(c *Conn) *msgWriterState { return mw } -func (mw *msgWriterState) ensureFlate() { +func (mw *msgWriter) ensureFlate() { if mw.trimWriter == nil { mw.trimWriter = &trimLastFourBytesWriter{ w: util.WriterFunc(mw.write), @@ -104,7 +85,7 @@ func (mw *msgWriterState) ensureFlate() { mw.flate = true } -func (mw *msgWriterState) flateContextTakeover() bool { +func (mw *msgWriter) flateContextTakeover() bool { if mw.c.client { return !mw.c.copts.clientNoContextTakeover } @@ -112,14 +93,11 @@ func (mw *msgWriterState) flateContextTakeover() bool { } func (c *Conn) writer(ctx context.Context, typ MessageType) (io.WriteCloser, error) { - err := c.msgWriterState.reset(ctx, typ) + err := c.msgWriter.reset(ctx, typ) if err != nil { return nil, err } - return &msgWriter{ - mw: c.msgWriterState, - closed: false, - }, nil + return c.msgWriter, nil } func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error) { @@ -129,8 +107,8 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error } if !c.flate() { - defer c.msgWriterState.mu.unlock() - return c.writeFrame(ctx, true, false, c.msgWriterState.opcode, p) + defer c.msgWriter.mu.unlock() + return c.writeFrame(ctx, true, false, c.msgWriter.opcode, p) } n, err := mw.Write(p) @@ -142,7 +120,7 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error return n, err } -func (mw *msgWriterState) reset(ctx context.Context, typ MessageType) error { +func (mw *msgWriter) reset(ctx context.Context, typ MessageType) error { err := mw.mu.lock(ctx) if err != nil { return err @@ -151,13 +129,14 @@ func (mw *msgWriterState) reset(ctx context.Context, typ MessageType) error { mw.ctx = ctx mw.opcode = opcode(typ) mw.flate = false + mw.closed = false mw.trimWriter.reset() return nil } -func (mw *msgWriterState) putFlateWriter() { +func (mw *msgWriter) putFlateWriter() { if mw.flateWriter != nil { putFlateWriter(mw.flateWriter) mw.flateWriter = nil @@ -165,7 +144,11 @@ func (mw *msgWriterState) putFlateWriter() { } // Write writes the given bytes to the WebSocket connection. -func (mw *msgWriterState) Write(p []byte) (_ int, err error) { +func (mw *msgWriter) Write(p []byte) (_ int, err error) { + if mw.closed { + return 0, errors.New("cannot use closed writer") + } + err = mw.writeMu.lock(mw.ctx) if err != nil { return 0, fmt.Errorf("failed to write: %w", err) @@ -194,7 +177,7 @@ func (mw *msgWriterState) Write(p []byte) (_ int, err error) { return mw.write(p) } -func (mw *msgWriterState) write(p []byte) (int, error) { +func (mw *msgWriter) write(p []byte) (int, error) { n, err := mw.c.writeFrame(mw.ctx, false, mw.flate, mw.opcode, p) if err != nil { return n, fmt.Errorf("failed to write data frame: %w", err) @@ -204,9 +187,14 @@ func (mw *msgWriterState) write(p []byte) (int, error) { } // Close flushes the frame to the connection. -func (mw *msgWriterState) Close() (err error) { +func (mw *msgWriter) Close() (err error) { defer errd.Wrap(&err, "failed to close writer") + if mw.closed { + return errors.New("writer already closed") + } + mw.closed = true + err = mw.writeMu.lock(mw.ctx) if err != nil { return err @@ -232,7 +220,7 @@ func (mw *msgWriterState) Close() (err error) { return nil } -func (mw *msgWriterState) close() { +func (mw *msgWriter) close() { if mw.c.client { mw.c.writeFrameMu.forceLock() putBufioWriter(mw.c.bw) From a94999fb3a308b562b13c85f4d458564adea9147 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 18:28:04 -0700 Subject: [PATCH 399/519] close: Implement CloseNow Closes #384 --- close.go | 15 ++++++++++++++- conn.go | 3 +++ conn_test.go | 13 +++++++++++++ export_test.go | 2 ++ 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/close.go b/close.go index 1e13ca73..25160ee1 100644 --- a/close.go +++ b/close.go @@ -102,6 +102,19 @@ func (c *Conn) Close(code StatusCode, reason string) error { return c.closeHandshake(code, reason) } +// CloseNow closes the WebSocket connection without attempting a close handshake. +// Use When you do not want the overhead of the close handshake. +func (c *Conn) CloseNow() (err error) { + defer errd.Wrap(&err, "failed to close WebSocket") + + if c.isClosed() { + return errClosed + } + + c.close(nil) + return c.closeErr +} + func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { defer errd.Wrap(&err, "failed to close WebSocket") @@ -265,7 +278,7 @@ func (c *Conn) setCloseErr(err error) { } func (c *Conn) setCloseErrLocked(err error) { - if c.closeErr == nil { + if c.closeErr == nil && err != nil { c.closeErr = fmt.Errorf("WebSocket closed: %w", err) } } diff --git a/conn.go b/conn.go index 78eaad82..3713b1f8 100644 --- a/conn.go +++ b/conn.go @@ -147,6 +147,9 @@ func (c *Conn) close(err error) { if c.isClosed() { return } + if err == nil { + err = c.rwc.Close() + } c.setCloseErrLocked(err) close(c.closed) runtime.SetFinalizer(c, nil) diff --git a/conn_test.go b/conn_test.go index 7a6a0c39..50b844b9 100644 --- a/conn_test.go +++ b/conn_test.go @@ -295,6 +295,19 @@ func TestConn(t *testing.T) { err = c1.Close(websocket.StatusNormalClosure, "") assert.Success(t, err) }) + + t.Run("CloseNow", func(t *testing.T) { + _, c1, c2 := newConnTest(t, nil, nil) + + err1 := c1.CloseNow() + err2 := c2.CloseNow() + assert.Success(t, err1) + assert.Success(t, err2) + err1 = c1.CloseNow() + err2 = c2.CloseNow() + assert.ErrorIs(t, websocket.ErrClosed, err1) + assert.ErrorIs(t, websocket.ErrClosed, err2) + }) } func TestWasm(t *testing.T) { diff --git a/export_test.go b/export_test.go index 8731b6d8..114796d0 100644 --- a/export_test.go +++ b/export_test.go @@ -23,3 +23,5 @@ func (c *Conn) RecordBytesRead() *int { })) return &bytesRead } + +var ErrClosed = errClosed From e314da6c5e9edeaa1457dd9d869ff080b07c54f5 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 14 Oct 2023 06:13:10 -0700 Subject: [PATCH 400/519] dial: Redirect wss/ws correctly by modifying the http client Closes #333 --- dial.go | 15 +++++++++++++++ dial_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/dial.go b/dial.go index e72432e7..e4c4daa1 100644 --- a/dial.go +++ b/dial.go @@ -70,6 +70,21 @@ func (opts *DialOptions) cloneWithDefaults(ctx context.Context) (context.Context if o.HTTPHeader == nil { o.HTTPHeader = http.Header{} } + newClient := *o.HTTPClient + oldCheckRedirect := o.HTTPClient.CheckRedirect + newClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { + switch req.URL.Scheme { + case "ws": + req.URL.Scheme = "http" + case "wss": + req.URL.Scheme = "https" + } + if oldCheckRedirect != nil { + return oldCheckRedirect(req, via) + } + return nil + } + o.HTTPClient = &newClient return ctx, cancel, &o } diff --git a/dial_test.go b/dial_test.go index e072db2d..3652f8d4 100644 --- a/dial_test.go +++ b/dial_test.go @@ -304,3 +304,28 @@ type roundTripperFunc func(*http.Request) (*http.Response, error) func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } + +func TestDialRedirect(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + _, _, err := Dial(ctx, "ws://example.com", &DialOptions{ + HTTPClient: mockHTTPClient(func(r *http.Request) (*http.Response, error) { + resp := &http.Response{ + Header: http.Header{}, + } + if r.URL.Scheme != "https" { + resp.Header.Set("Location", "wss://example.com") + resp.StatusCode = http.StatusFound + return resp, nil + } + resp.Header.Set("Connection", "Upgrade") + resp.Header.Set("Upgrade", "meow") + resp.StatusCode = http.StatusSwitchingProtocols + return resp, nil + }), + }) + assert.Contains(t, err, "failed to WebSocket dial: WebSocket protocol violation: Upgrade header \"meow\" does not contain websocket") +} From 249edb209389a1b6fd3b1f79de78417982077284 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 18 Oct 2023 20:44:27 -0700 Subject: [PATCH 401/519] dial_test: Add TestDialViaProxy For #395 Somehow currently reproduces #391... Debugging still. --- conn_test.go | 26 ++++++++++++ dial_test.go | 110 ++++++++++++++++++++++++++++++++++++++++++------- export_test.go | 7 ++++ 3 files changed, 128 insertions(+), 15 deletions(-) diff --git a/conn_test.go b/conn_test.go index 50b844b9..5f78cad5 100644 --- a/conn_test.go +++ b/conn_test.go @@ -526,3 +526,29 @@ func echoServer(w http.ResponseWriter, r *http.Request, opts *websocket.AcceptOp err = wstest.EchoLoop(r.Context(), c) return assertCloseStatus(websocket.StatusNormalClosure, err) } + +func assertEcho(tb testing.TB, ctx context.Context, c *websocket.Conn) { + exp := xrand.String(xrand.Int(131072)) + + werr := xsync.Go(func() error { + return wsjson.Write(ctx, c, exp) + }) + + var act interface{} + err := wsjson.Read(ctx, c, &act) + assert.Success(tb, err) + assert.Equal(tb, "read msg", exp, act) + + select { + case err := <-werr: + assert.Success(tb, err) + case <-ctx.Done(): + tb.Fatal(ctx.Err()) + } +} + +func assertClose(tb testing.TB, c *websocket.Conn) { + tb.Helper() + err := c.Close(websocket.StatusNormalClosure, "") + assert.Success(tb, err) +} diff --git a/dial_test.go b/dial_test.go index 3652f8d4..7a84436d 100644 --- a/dial_test.go +++ b/dial_test.go @@ -1,7 +1,7 @@ //go:build !js // +build !js -package websocket +package websocket_test import ( "bytes" @@ -10,12 +10,15 @@ import ( "io" "net/http" "net/http/httptest" + "net/url" "strings" "testing" "time" + "nhooyr.io/websocket" "nhooyr.io/websocket/internal/test/assert" "nhooyr.io/websocket/internal/util" + "nhooyr.io/websocket/internal/xsync" ) func TestBadDials(t *testing.T) { @@ -27,7 +30,7 @@ func TestBadDials(t *testing.T) { testCases := []struct { name string url string - opts *DialOptions + opts *websocket.DialOptions rand util.ReaderFunc nilCtx bool }{ @@ -72,7 +75,7 @@ func TestBadDials(t *testing.T) { tc.rand = rand.Reader.Read } - _, _, err := dial(ctx, tc.url, tc.opts, tc.rand) + _, _, err := websocket.ExportedDial(ctx, tc.url, tc.opts, tc.rand) assert.Error(t, err) }) } @@ -84,7 +87,7 @@ func TestBadDials(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() - _, _, err := Dial(ctx, "ws://example.com", &DialOptions{ + _, _, err := websocket.Dial(ctx, "ws://example.com", &websocket.DialOptions{ HTTPClient: mockHTTPClient(func(*http.Request) (*http.Response, error) { return &http.Response{ Body: io.NopCloser(strings.NewReader("hi")), @@ -104,7 +107,7 @@ func TestBadDials(t *testing.T) { h := http.Header{} h.Set("Connection", "Upgrade") h.Set("Upgrade", "websocket") - h.Set("Sec-WebSocket-Accept", secWebSocketAccept(r.Header.Get("Sec-WebSocket-Key"))) + h.Set("Sec-WebSocket-Accept", websocket.SecWebSocketAccept(r.Header.Get("Sec-WebSocket-Key"))) return &http.Response{ StatusCode: http.StatusSwitchingProtocols, @@ -113,7 +116,7 @@ func TestBadDials(t *testing.T) { }, nil } - _, _, err := Dial(ctx, "ws://example.com", &DialOptions{ + _, _, err := websocket.Dial(ctx, "ws://example.com", &websocket.DialOptions{ HTTPClient: mockHTTPClient(rt), }) assert.Contains(t, err, "response body is not a io.ReadWriteCloser") @@ -152,7 +155,7 @@ func Test_verifyHostOverride(t *testing.T) { h := http.Header{} h.Set("Connection", "Upgrade") h.Set("Upgrade", "websocket") - h.Set("Sec-WebSocket-Accept", secWebSocketAccept(r.Header.Get("Sec-WebSocket-Key"))) + h.Set("Sec-WebSocket-Accept", websocket.SecWebSocketAccept(r.Header.Get("Sec-WebSocket-Key"))) return &http.Response{ StatusCode: http.StatusSwitchingProtocols, @@ -161,7 +164,7 @@ func Test_verifyHostOverride(t *testing.T) { }, nil } - _, _, err := Dial(ctx, "ws://example.com", &DialOptions{ + _, _, err := websocket.Dial(ctx, "ws://example.com", &websocket.DialOptions{ HTTPClient: mockHTTPClient(rt), Host: tc.host, }) @@ -272,18 +275,18 @@ func Test_verifyServerHandshake(t *testing.T) { resp := w.Result() r := httptest.NewRequest("GET", "/", nil) - key, err := secWebSocketKey(rand.Reader) + key, err := websocket.SecWebSocketKey(rand.Reader) assert.Success(t, err) r.Header.Set("Sec-WebSocket-Key", key) if resp.Header.Get("Sec-WebSocket-Accept") == "" { - resp.Header.Set("Sec-WebSocket-Accept", secWebSocketAccept(key)) + resp.Header.Set("Sec-WebSocket-Accept", websocket.SecWebSocketAccept(key)) } - opts := &DialOptions{ + opts := &websocket.DialOptions{ Subprotocols: strings.Split(r.Header.Get("Sec-WebSocket-Protocol"), ","), } - _, err = verifyServerResponse(opts, opts.CompressionMode.opts(), key, resp) + _, err = websocket.VerifyServerResponse(opts, websocket.CompressionModeOpts(opts.CompressionMode), key, resp) if tc.success { assert.Success(t, err) } else { @@ -311,7 +314,7 @@ func TestDialRedirect(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() - _, _, err := Dial(ctx, "ws://example.com", &DialOptions{ + _, _, err := websocket.Dial(ctx, "ws://example.com", &websocket.DialOptions{ HTTPClient: mockHTTPClient(func(r *http.Request) (*http.Response, error) { resp := &http.Response{ Header: http.Header{}, @@ -321,11 +324,88 @@ func TestDialRedirect(t *testing.T) { resp.StatusCode = http.StatusFound return resp, nil } - resp.Header.Set("Connection", "Upgrade") - resp.Header.Set("Upgrade", "meow") + resp.Header.Set("Connection", "Upgrade") + resp.Header.Set("Upgrade", "meow") resp.StatusCode = http.StatusSwitchingProtocols return resp, nil }), }) assert.Contains(t, err, "failed to WebSocket dial: WebSocket protocol violation: Upgrade header \"meow\" does not contain websocket") } + +type forwardProxy struct { + hc *http.Client +} + +func newForwardProxy() *forwardProxy { + return &forwardProxy{ + hc: &http.Client{}, + } +} + +func (fc *forwardProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), time.Second*10) + defer cancel() + + r = r.WithContext(ctx) + r.RequestURI = "" + resp, err := fc.hc.Do(r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + defer resp.Body.Close() + + for k, v := range resp.Header { + w.Header()[k] = v + } + w.Header().Set("PROXIED", "true") + w.WriteHeader(resp.StatusCode) + errc1 := xsync.Go(func() error { + _, err := io.Copy(w, resp.Body) + return err + }) + var errc2 <-chan error + if bodyw, ok := resp.Body.(io.Writer); ok { + errc2 = xsync.Go(func() error { + _, err := io.Copy(bodyw, r.Body) + return err + }) + } + select { + case <-errc1: + case <-errc2: + case <-r.Context().Done(): + } +} + +func TestDialViaProxy(t *testing.T) { + t.Parallel() + + ps := httptest.NewServer(newForwardProxy()) + defer ps.Close() + + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := echoServer(w, r, nil) + assert.Success(t, err) + })) + defer s.Close() + + psu, err := url.Parse(ps.URL) + assert.Success(t, err) + proxyTransport := http.DefaultTransport.(*http.Transport).Clone() + proxyTransport.Proxy = http.ProxyURL(psu) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + c, resp, err := websocket.Dial(ctx, s.URL, &websocket.DialOptions{ + HTTPClient: &http.Client{ + Transport: proxyTransport, + }, + }) + assert.Success(t, err) + assert.Equal(t, "", "true", resp.Header.Get("PROXIED")) + + assertEcho(t, ctx, c) + assertClose(t, c) +} diff --git a/export_test.go b/export_test.go index 114796d0..e322c36f 100644 --- a/export_test.go +++ b/export_test.go @@ -25,3 +25,10 @@ func (c *Conn) RecordBytesRead() *int { } var ErrClosed = errClosed + +var ExportedDial = dial +var SecWebSocketAccept = secWebSocketAccept +var SecWebSocketKey = secWebSocketKey +var VerifyServerResponse = verifyServerResponse + +var CompressionModeOpts = CompressionMode.opts From 818579b7f9eb42c34dceb41d2b113d382cede0df Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 18 Oct 2023 21:05:02 -0700 Subject: [PATCH 402/519] TestDialViaProxy: Fix bug in forward proxy Closes #395 Confirmed library works correctly with a working forward proxy. --- conn_test.go | 1 + dial_test.go | 34 +++++++++++++++++++++------------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/conn_test.go b/conn_test.go index 5f78cad5..c814ca28 100644 --- a/conn_test.go +++ b/conn_test.go @@ -535,6 +535,7 @@ func assertEcho(tb testing.TB, ctx context.Context, c *websocket.Conn) { }) var act interface{} + c.SetReadLimit(1 << 30) err := wsjson.Read(ctx, c, &act) assert.Success(tb, err) assert.Equal(tb, "read msg", exp, act) diff --git a/dial_test.go b/dial_test.go index 7a84436d..63cb4be6 100644 --- a/dial_test.go +++ b/dial_test.go @@ -361,21 +361,29 @@ func (fc *forwardProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { } w.Header().Set("PROXIED", "true") w.WriteHeader(resp.StatusCode) - errc1 := xsync.Go(func() error { - _, err := io.Copy(w, resp.Body) - return err - }) - var errc2 <-chan error - if bodyw, ok := resp.Body.(io.Writer); ok { - errc2 = xsync.Go(func() error { - _, err := io.Copy(bodyw, r.Body) + if resprw, ok := resp.Body.(io.ReadWriter); ok { + c, brw, err := w.(http.Hijacker).Hijack() + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + brw.Flush() + + errc1 := xsync.Go(func() error { + _, err := io.Copy(c, resprw) return err }) - } - select { - case <-errc1: - case <-errc2: - case <-r.Context().Done(): + errc2 := xsync.Go(func() error { + _, err := io.Copy(resprw, c) + return err + }) + select { + case <-errc1: + case <-errc2: + case <-r.Context().Done(): + } + } else { + io.Copy(w, resp.Body) } } From 20b883815e581f10639cf96d6132dcdac92b596a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 18 Oct 2023 21:11:33 -0700 Subject: [PATCH 403/519] ci: Add dev to daily --- .github/workflows/daily.yml | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index d10c142f..b625fd68 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -23,7 +23,31 @@ jobs: - uses: actions/setup-go@v4 with: go-version-file: ./go.mod - - run: AUTOBAHN=1 ./ci/test.sh + - run: AUTOBAHN=1 ./ci/test.sh -bench=. + - uses: actions/upload-artifact@v3 + with: + name: coverage.html + path: ./ci/out/coverage.html + bench-dev: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: dev + - uses: actions/setup-go@v4 + with: + go-version-file: ./go.mod + - run: AUTOBAHN=1 ./ci/bench.sh + test-dev: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: dev + - uses: actions/setup-go@v4 + with: + go-version-file: ./go.mod + - run: AUTOBAHN=1 ./ci/test.sh -bench=. - uses: actions/upload-artifact@v3 with: name: coverage.html From 591ff8e56211cab65a30d6bd5efa0902719a92ea Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 18 Oct 2023 21:12:41 -0700 Subject: [PATCH 404/519] accept.go: Comment typo --- accept.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accept.go b/accept.go index 19e388ec..b90e15eb 100644 --- a/accept.go +++ b/accept.go @@ -244,7 +244,7 @@ func selectDeflate(extensions []websocketExtension, mode CompressionMode) (*comp } for _, ext := range extensions { switch ext.name { - // We used to implement x-webkit-deflate-fram too but Safari has bugs. + // We used to implement x-webkit-deflate-frame too for Safari but Safari has bugs... // See https://github.com/nhooyr/websocket/issues/218 case "permessage-deflate": copts, ok := acceptDeflate(ext, mode) From 64ce00991a066009cdeb34971f3c21ebb3e2766f Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 18 Oct 2023 21:17:07 -0700 Subject: [PATCH 405/519] conn: Return net.ErrClosed whenever appropriate Updates e9d08816010996a14241f008ac097c5621bd1f30 --- close.go | 2 +- conn.go | 6 +++--- make.sh | 2 +- read.go | 12 ++++++------ write.go | 8 ++++---- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/close.go b/close.go index 25160ee1..24907c64 100644 --- a/close.go +++ b/close.go @@ -125,7 +125,7 @@ func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { return writeErr } - if CloseStatus(closeHandshakeErr) == -1 { + if CloseStatus(closeHandshakeErr) == -1 && !errors.Is(errClosed, closeHandshakeErr) { return closeHandshakeErr } diff --git a/conn.go b/conn.go index 3713b1f8..36662a93 100644 --- a/conn.go +++ b/conn.go @@ -228,7 +228,7 @@ func (c *Conn) ping(ctx context.Context, p string) error { select { case <-c.closed: - return c.closeErr + return errClosed case <-ctx.Done(): err := fmt.Errorf("failed to wait for pong: %w", ctx.Err()) c.close(err) @@ -266,7 +266,7 @@ func (m *mu) tryLock() bool { func (m *mu) lock(ctx context.Context) error { select { case <-m.c.closed: - return m.c.closeErr + return errClosed case <-ctx.Done(): err := fmt.Errorf("failed to acquire lock: %w", ctx.Err()) m.c.close(err) @@ -279,7 +279,7 @@ func (m *mu) lock(ctx context.Context) error { case <-m.c.closed: // Make sure to release. m.unlock() - return m.c.closeErr + return errClosed default: } return nil diff --git a/make.sh b/make.sh index 68a98ac1..81909d72 100755 --- a/make.sh +++ b/make.sh @@ -4,5 +4,5 @@ cd -- "$(dirname "$0")" ./ci/fmt.sh ./ci/lint.sh -./ci/test.sh +./ci/test.sh "$@" ./ci/bench.sh diff --git a/read.go b/read.go index d3217861..bf4362df 100644 --- a/read.go +++ b/read.go @@ -203,7 +203,7 @@ func (c *Conn) readLoop(ctx context.Context) (header, error) { func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { select { case <-c.closed: - return header{}, c.closeErr + return header{}, errClosed case c.readTimeout <- ctx: } @@ -211,7 +211,7 @@ func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { if err != nil { select { case <-c.closed: - return header{}, c.closeErr + return header{}, errClosed case <-ctx.Done(): return header{}, ctx.Err() default: @@ -222,7 +222,7 @@ func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { select { case <-c.closed: - return header{}, c.closeErr + return header{}, errClosed case c.readTimeout <- context.Background(): } @@ -232,7 +232,7 @@ func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { select { case <-c.closed: - return 0, c.closeErr + return 0, errClosed case c.readTimeout <- ctx: } @@ -240,7 +240,7 @@ func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { if err != nil { select { case <-c.closed: - return n, c.closeErr + return n, errClosed case <-ctx.Done(): return n, ctx.Err() default: @@ -252,7 +252,7 @@ func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { select { case <-c.closed: - return n, c.closeErr + return n, errClosed case c.readTimeout <- context.Background(): } diff --git a/write.go b/write.go index 20a71d3e..b7cf6600 100644 --- a/write.go +++ b/write.go @@ -262,14 +262,14 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco case <-ctx.Done(): return 0, ctx.Err() case <-c.closed: - return 0, c.closeErr + return 0, errClosed } } defer c.writeFrameMu.unlock() select { case <-c.closed: - return 0, c.closeErr + return 0, errClosed case c.writeTimeout <- ctx: } @@ -277,7 +277,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco if err != nil { select { case <-c.closed: - err = c.closeErr + err = errClosed case <-ctx.Done(): err = ctx.Err() } @@ -323,7 +323,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco select { case <-c.closed: - return n, c.closeErr + return n, errClosed case c.writeTimeout <- context.Background(): } From 1a344a4c1349d0947dcb6346ea2d35523625a265 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 20 Dec 2022 14:16:01 -0600 Subject: [PATCH 406/519] Reject invalid "Sec-WebSocket-Key" headers from clients Client "Sec-WebSocket-Key" should be a valid 16 byte base64 encoded nonce. If the header is not valid, the server should reject the client. --- accept.go | 7 ++++++- accept_test.go | 33 ++++++++++++++++++++++++++------- internal/test/xrand/xrand.go | 5 +++++ 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/accept.go b/accept.go index b90e15eb..2f4fb2eb 100644 --- a/accept.go +++ b/accept.go @@ -185,10 +185,15 @@ func verifyClientRequest(w http.ResponseWriter, r *http.Request) (errCode int, _ return http.StatusBadRequest, fmt.Errorf("unsupported WebSocket protocol version (only 13 is supported): %q", r.Header.Get("Sec-WebSocket-Version")) } - if r.Header.Get("Sec-WebSocket-Key") == "" { + websocketSecKey := r.Header.Get("Sec-WebSocket-Key") + if websocketSecKey == "" { return http.StatusBadRequest, errors.New("WebSocket protocol violation: missing Sec-WebSocket-Key") } + if v, err := base64.StdEncoding.DecodeString(websocketSecKey); err != nil || len(v) != 16 { + return http.StatusBadRequest, fmt.Errorf("WebSocket protocol violation: invalid Sec-WebSocket-Key %q, must be a 16 byte base64 encoded string", websocketSecKey) + } + return 0, nil } diff --git a/accept_test.go b/accept_test.go index c554bdaf..d0cc4878 100644 --- a/accept_test.go +++ b/accept_test.go @@ -9,6 +9,7 @@ import ( "net" "net/http" "net/http/httptest" + "nhooyr.io/websocket/internal/test/xrand" "strings" "testing" @@ -36,7 +37,7 @@ func TestAccept(t *testing.T) { r.Header.Set("Connection", "Upgrade") r.Header.Set("Upgrade", "websocket") r.Header.Set("Sec-WebSocket-Version", "13") - r.Header.Set("Sec-WebSocket-Key", "meow123") + r.Header.Set("Sec-WebSocket-Key", xrand.Base64(16)) r.Header.Set("Origin", "harhar.com") _, err := Accept(w, r, nil) @@ -52,7 +53,7 @@ func TestAccept(t *testing.T) { r.Header.Set("Connection", "Upgrade") r.Header.Set("Upgrade", "websocket") r.Header.Set("Sec-WebSocket-Version", "13") - r.Header.Set("Sec-WebSocket-Key", "meow123") + r.Header.Set("Sec-WebSocket-Key", xrand.Base64(16)) r.Header.Set("Origin", "https://harhar.com") _, err := Accept(w, r, nil) @@ -116,7 +117,7 @@ func TestAccept(t *testing.T) { r.Header.Set("Connection", "Upgrade") r.Header.Set("Upgrade", "websocket") r.Header.Set("Sec-WebSocket-Version", "13") - r.Header.Set("Sec-WebSocket-Key", "meow123") + r.Header.Set("Sec-WebSocket-Key", xrand.Base64(16)) _, err := Accept(w, r, nil) assert.Contains(t, err, `http.ResponseWriter does not implement http.Hijacker`) @@ -136,7 +137,7 @@ func TestAccept(t *testing.T) { r.Header.Set("Connection", "Upgrade") r.Header.Set("Upgrade", "websocket") r.Header.Set("Sec-WebSocket-Version", "13") - r.Header.Set("Sec-WebSocket-Key", "meow123") + r.Header.Set("Sec-WebSocket-Key", xrand.Base64(16)) _, err := Accept(w, r, nil) assert.Contains(t, err, `failed to hijack connection`) @@ -183,7 +184,7 @@ func Test_verifyClientHandshake(t *testing.T) { }, }, { - name: "badWebSocketKey", + name: "missingWebSocketKey", h: map[string]string{ "Connection": "Upgrade", "Upgrade": "websocket", @@ -191,13 +192,31 @@ func Test_verifyClientHandshake(t *testing.T) { "Sec-WebSocket-Key": "", }, }, + { + name: "shortWebSocketKey", + h: map[string]string{ + "Connection": "Upgrade", + "Upgrade": "websocket", + "Sec-WebSocket-Version": "13", + "Sec-WebSocket-Key": xrand.Base64(15), + }, + }, + { + name: "invalidWebSocketKey", + h: map[string]string{ + "Connection": "Upgrade", + "Upgrade": "websocket", + "Sec-WebSocket-Version": "13", + "Sec-WebSocket-Key": "notbase64", + }, + }, { name: "badHTTPVersion", h: map[string]string{ "Connection": "Upgrade", "Upgrade": "websocket", "Sec-WebSocket-Version": "13", - "Sec-WebSocket-Key": "meow123", + "Sec-WebSocket-Key": xrand.Base64(16), }, http1: true, }, @@ -207,7 +226,7 @@ func Test_verifyClientHandshake(t *testing.T) { "Connection": "keep-alive, Upgrade", "Upgrade": "websocket", "Sec-WebSocket-Version": "13", - "Sec-WebSocket-Key": "meow123", + "Sec-WebSocket-Key": xrand.Base64(16), }, success: true, }, diff --git a/internal/test/xrand/xrand.go b/internal/test/xrand/xrand.go index 8de1ede8..82064d5c 100644 --- a/internal/test/xrand/xrand.go +++ b/internal/test/xrand/xrand.go @@ -2,6 +2,7 @@ package xrand import ( "crypto/rand" + "encoding/base64" "fmt" "math/big" "strings" @@ -45,3 +46,7 @@ func Int(max int) int { } return int(x.Int64()) } + +func Base64(n int) string { + return base64.StdEncoding.EncodeToString(Bytes(n)) +} From f46da9af6d363b6a5fe10d09705acd9d933af644 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 20 Dec 2022 14:18:41 -0600 Subject: [PATCH 407/519] Remove build tag at top of files --- accept.go | 1 - accept_test.go | 1 - 2 files changed, 2 deletions(-) diff --git a/accept.go b/accept.go index 2f4fb2eb..24c5dca3 100644 --- a/accept.go +++ b/accept.go @@ -1,4 +1,3 @@ -//go:build !js // +build !js package websocket diff --git a/accept_test.go b/accept_test.go index d0cc4878..270f62da 100644 --- a/accept_test.go +++ b/accept_test.go @@ -1,4 +1,3 @@ -//go:build !js // +build !js package websocket From 3233cb5a0622a6eba869e3d169e98d11e1f2688f Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 20 Dec 2022 14:27:27 -0600 Subject: [PATCH 408/519] Remove all leading and trailing whitespace --- accept.go | 3 +++ accept_test.go | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/accept.go b/accept.go index 24c5dca3..11b312d1 100644 --- a/accept.go +++ b/accept.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket @@ -185,6 +186,8 @@ func verifyClientRequest(w http.ResponseWriter, r *http.Request) (errCode int, _ } websocketSecKey := r.Header.Get("Sec-WebSocket-Key") + // The RFC states to remove any leading or trailing whitespace. + websocketSecKey = strings.TrimSpace(websocketSecKey) if websocketSecKey == "" { return http.StatusBadRequest, errors.New("WebSocket protocol violation: missing Sec-WebSocket-Key") } diff --git a/accept_test.go b/accept_test.go index 270f62da..ba245c47 100644 --- a/accept_test.go +++ b/accept_test.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket @@ -229,6 +230,16 @@ func Test_verifyClientHandshake(t *testing.T) { }, success: true, }, + { + name: "successSecKeyExtraSpace", + h: map[string]string{ + "Connection": "keep-alive, Upgrade", + "Upgrade": "websocket", + "Sec-WebSocket-Version": "13", + "Sec-WebSocket-Key": " " + xrand.Base64(16) + " ", + }, + success: true, + }, } for _, tc := range testCases { From 309e088c4f56983e20ae732aa59669f85144818f Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 20 Dec 2022 14:46:50 -0600 Subject: [PATCH 409/519] Handle multiple sec-websocket-keys --- accept.go | 12 ++++++++---- accept_test.go | 22 +++++++++++++++++++++- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/accept.go b/accept.go index 11b312d1..285b3103 100644 --- a/accept.go +++ b/accept.go @@ -185,13 +185,17 @@ func verifyClientRequest(w http.ResponseWriter, r *http.Request) (errCode int, _ return http.StatusBadRequest, fmt.Errorf("unsupported WebSocket protocol version (only 13 is supported): %q", r.Header.Get("Sec-WebSocket-Version")) } - websocketSecKey := r.Header.Get("Sec-WebSocket-Key") - // The RFC states to remove any leading or trailing whitespace. - websocketSecKey = strings.TrimSpace(websocketSecKey) - if websocketSecKey == "" { + websocketSecKeys := r.Header.Values("Sec-WebSocket-Key") + if len(websocketSecKeys) == 0 { return http.StatusBadRequest, errors.New("WebSocket protocol violation: missing Sec-WebSocket-Key") } + if len(websocketSecKeys) > 1 { + return http.StatusBadRequest, errors.New("WebSocket protocol violation: multiple Sec-WebSocket-Key headers") + } + + // The RFC states to remove any leading or trailing whitespace. + websocketSecKey := strings.TrimSpace(websocketSecKeys[0]) if v, err := base64.StdEncoding.DecodeString(websocketSecKey); err != nil || len(v) != 16 { return http.StatusBadRequest, fmt.Errorf("WebSocket protocol violation: invalid Sec-WebSocket-Key %q, must be a 16 byte base64 encoded string", websocketSecKey) } diff --git a/accept_test.go b/accept_test.go index ba245c47..5b37dfc8 100644 --- a/accept_test.go +++ b/accept_test.go @@ -185,6 +185,14 @@ func Test_verifyClientHandshake(t *testing.T) { }, { name: "missingWebSocketKey", + h: map[string]string{ + "Connection": "Upgrade", + "Upgrade": "websocket", + "Sec-WebSocket-Version": "13", + }, + }, + { + name: "emptyWebSocketKey", h: map[string]string{ "Connection": "Upgrade", "Upgrade": "websocket", @@ -210,6 +218,18 @@ func Test_verifyClientHandshake(t *testing.T) { "Sec-WebSocket-Key": "notbase64", }, }, + { + name: "extraWebSocketKey", + h: map[string]string{ + "Connection": "Upgrade", + "Upgrade": "websocket", + "Sec-WebSocket-Version": "13", + // Kinda cheeky, but http headers are case-insensitive. + // If 2 sec keys are present, this is a failure condition. + "Sec-WebSocket-Key": xrand.Base64(16), + "sec-webSocket-key": xrand.Base64(16), + }, + }, { name: "badHTTPVersion", h: map[string]string{ @@ -256,7 +276,7 @@ func Test_verifyClientHandshake(t *testing.T) { } for k, v := range tc.h { - r.Header.Set(k, v) + r.Header.Add(k, v) } _, err := verifyClientRequest(httptest.NewRecorder(), r) From 305eab9a519e2d563636ea1ea7b0b82377acf2fb Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 18 Oct 2023 21:31:59 -0700 Subject: [PATCH 410/519] misc: Format and compile #360 --- accept_test.go | 4 ++-- internal/test/xrand/xrand.go | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/accept_test.go b/accept_test.go index 5b37dfc8..7cb85d0f 100644 --- a/accept_test.go +++ b/accept_test.go @@ -9,11 +9,11 @@ import ( "net" "net/http" "net/http/httptest" - "nhooyr.io/websocket/internal/test/xrand" "strings" "testing" "nhooyr.io/websocket/internal/test/assert" + "nhooyr.io/websocket/internal/test/xrand" ) func TestAccept(t *testing.T) { @@ -68,7 +68,7 @@ func TestAccept(t *testing.T) { r.Header.Set("Connection", "Upgrade") r.Header.Set("Upgrade", "websocket") r.Header.Set("Sec-WebSocket-Version", "13") - r.Header.Set("Sec-WebSocket-Key", "meow123") + r.Header.Set("Sec-WebSocket-Key", xrand.Base64(16)) r.Header.Set("Sec-WebSocket-Extensions", extensions) return r } diff --git a/internal/test/xrand/xrand.go b/internal/test/xrand/xrand.go index 82064d5c..9bfb39ce 100644 --- a/internal/test/xrand/xrand.go +++ b/internal/test/xrand/xrand.go @@ -47,6 +47,7 @@ func Int(max int) int { return int(x.Int64()) } +// Base64 returns a randomly generated base64 string of length n. func Base64(n int) string { return base64.StdEncoding.EncodeToString(Bytes(n)) } From e361137d7e762ad4d58cd7ea244e052ba4fdb891 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 18 Oct 2023 21:40:57 -0700 Subject: [PATCH 411/519] wsjs: Register OnError Closes #400 --- ws_js.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ws_js.go b/ws_js.go index e60601e3..03919692 100644 --- a/ws_js.go +++ b/ws_js.go @@ -55,6 +55,7 @@ type Conn struct { closeWasClean bool releaseOnClose func() + releaseOnError func() releaseOnMessage func() readSignal chan struct{} @@ -92,9 +93,15 @@ func (c *Conn) init() { c.close(err, e.WasClean) c.releaseOnClose() + c.releaseOnError() c.releaseOnMessage() }) + c.releaseOnError = c.ws.OnError(func(v js.Value) { + c.setCloseErr(errors.New(v.Get("message").String())) + c.closeWithInternal() + }) + c.releaseOnMessage = c.ws.OnMessage(func(e wsjs.MessageEvent) { c.readBufMu.Lock() defer c.readBufMu.Unlock() From 8abed3a7c004a4f51453dd6f01fc881f0af07cf4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 18 Oct 2023 21:54:08 -0700 Subject: [PATCH 412/519] close.go: Remove unnecessary log.Printf call --- close.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/close.go b/close.go index 24907c64..d78a5442 100644 --- a/close.go +++ b/close.go @@ -8,7 +8,6 @@ import ( "encoding/binary" "errors" "fmt" - "log" "time" "nhooyr.io/websocket/internal/errd" @@ -150,9 +149,6 @@ func (c *Conn) writeClose(code StatusCode, reason string) error { var marshalErr error if ce.Code != StatusNoStatusRcvd { p, marshalErr = ce.bytes() - if marshalErr != nil { - log.Printf("websocket: %v", marshalErr) - } } writeErr := c.writeControl(context.Background(), opClose, p) From e4879ab74e5dd045a4ef5707f2c542b9cf4a4321 Mon Sep 17 00:00:00 2001 From: univerio Date: Thu, 25 May 2023 12:34:29 +0200 Subject: [PATCH 413/519] conn_test: Add TestConcurrentClosePing Updates #298 --- conn_test.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/conn_test.go b/conn_test.go index c814ca28..3df6c64a 100644 --- a/conn_test.go +++ b/conn_test.go @@ -553,3 +553,26 @@ func assertClose(tb testing.TB, c *websocket.Conn) { err := c.Close(websocket.StatusNormalClosure, "") assert.Success(tb, err) } + +func TestConcurrentClosePing(t *testing.T) { + t.Parallel() + for i := 0; i < 64; i++ { + func() { + c1, c2 := wstest.Pipe(nil, nil) + defer c1.CloseNow() + defer c2.CloseNow() + c1.CloseRead(context.Background()) + c2.CloseRead(context.Background()) + go func() { + for range time.Tick(time.Millisecond) { + if err := c1.Ping(context.Background()); err != nil { + return + } + } + }() + + time.Sleep(10 * time.Millisecond) + assert.Success(t, c1.Close(websocket.StatusNormalClosure, "")) + }() + } +} From 28c670953e8a6c6ecf147f89ac0085ac6510999e Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 18 Oct 2023 22:37:11 -0700 Subject: [PATCH 414/519] conn_test.go: Fix TestConcurrentClosePing Closes #298 Closes #394 The close frame was being received from the peer before we were able to reset our write timeout and so we thought the write kept failing but it never was... Thanks @univerio and @bhallionOhbibi --- read.go | 7 +++++-- write.go | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/read.go b/read.go index bf4362df..72386088 100644 --- a/read.go +++ b/read.go @@ -62,9 +62,12 @@ func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { func (c *Conn) CloseRead(ctx context.Context) context.Context { ctx, cancel := context.WithCancel(ctx) go func() { + defer c.CloseNow() defer cancel() - c.Reader(ctx) - c.Close(StatusPolicyViolation, "unexpected data message") + _, _, err := c.Reader(ctx) + if err == nil { + c.Close(StatusPolicyViolation, "unexpected data message") + } }() return ctx } diff --git a/write.go b/write.go index b7cf6600..6747513d 100644 --- a/write.go +++ b/write.go @@ -323,6 +323,9 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco select { case <-c.closed: + if opcode == opClose { + return n, nil + } return n, errClosed case c.writeTimeout <- context.Background(): } From 6cec2ca22e36e702265fd0a9173be341c8e44397 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 18 Oct 2023 22:47:59 -0700 Subject: [PATCH 415/519] close.go: Fix mid read close Closes #355 --- close.go | 7 +++++++ conn_test.go | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/close.go b/close.go index d78a5442..fe1ced34 100644 --- a/close.go +++ b/close.go @@ -182,6 +182,13 @@ func (c *Conn) waitCloseHandshake() error { return c.readCloseFrameErr } + for i := int64(0); i < c.msgReader.payloadLength; i++ { + _, err := c.br.ReadByte() + if err != nil { + return err + } + } + for { h, err := c.readLoop(ctx) if err != nil { diff --git a/conn_test.go b/conn_test.go index 3df6c64a..abc1c81d 100644 --- a/conn_test.go +++ b/conn_test.go @@ -308,6 +308,27 @@ func TestConn(t *testing.T) { assert.ErrorIs(t, websocket.ErrClosed, err1) assert.ErrorIs(t, websocket.ErrClosed, err2) }) + + t.Run("MidReadClose", func(t *testing.T) { + tt, c1, c2 := newConnTest(t, nil, nil) + + tt.goEchoLoop(c2) + + c1.SetReadLimit(131072) + + for i := 0; i < 5; i++ { + err := wstest.Echo(tt.ctx, c1, 131072) + assert.Success(t, err) + } + + err := wsjson.Write(tt.ctx, c1, "four") + assert.Success(t, err) + _, _, err = c1.Reader(tt.ctx) + assert.Success(t, err) + + err = c1.Close(websocket.StatusNormalClosure, "") + assert.Success(t, err) + }) } func TestWasm(t *testing.T) { From 5fe95bbfc2939b32e43b94768be7b6a23f86cbc4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 18 Oct 2023 22:50:56 -0700 Subject: [PATCH 416/519] write.go: Fix potential writeFrame deadlock Closes #405 You should always be reading from the connection with CloseRead so this shouldn't have affected anyone using the library correctly. --- write.go | 1 + 1 file changed, 1 insertion(+) diff --git a/write.go b/write.go index 6747513d..708d5a6a 100644 --- a/write.go +++ b/write.go @@ -280,6 +280,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco err = errClosed case <-ctx.Done(): err = ctx.Err() + default: } c.close(err) err = fmt.Errorf("failed to write frame: %w", err) From 308a8e26527cdb9c3ffc87bbdb299cd0d438fec4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 18 Oct 2023 23:21:29 -0700 Subject: [PATCH 417/519] autobahn_test.go: Fix TODOs --- autobahn_test.go | 53 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/autobahn_test.go b/autobahn_test.go index 41fae555..57ceebd5 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -6,6 +6,7 @@ package websocket_test import ( "context" "encoding/json" + "errors" "fmt" "io" "net" @@ -20,6 +21,7 @@ import ( "nhooyr.io/websocket/internal/errd" "nhooyr.io/websocket/internal/test/assert" "nhooyr.io/websocket/internal/test/wstest" + "nhooyr.io/websocket/internal/util" ) var excludedAutobahnCases = []string{ @@ -37,8 +39,7 @@ var autobahnCases = []string{"*"} // Used to run individual test cases. autobahnCases runs only those cases matched // and not excluded by excludedAutobahnCases. Adding cases here means excludedAutobahnCases // is niled. -// TODO: -// var forceAutobahnCases = []string{} +var onlyAutobahnCases = []string{} func TestAutobahn(t *testing.T) { t.Parallel() @@ -54,10 +55,15 @@ func TestAutobahn(t *testing.T) { ) } + if len(onlyAutobahnCases) > 0 { + excludedAutobahnCases = []string{} + autobahnCases = onlyAutobahnCases + } + ctx, cancel := context.WithTimeout(context.Background(), time.Hour) defer cancel() - wstestURL, closeFn, err := wstestServer(ctx) + wstestURL, closeFn, err := wstestServer(t, ctx) assert.Success(t, err) defer func() { assert.Success(t, closeFn()) @@ -90,7 +96,7 @@ func TestAutobahn(t *testing.T) { assert.Success(t, err) c.Close(websocket.StatusNormalClosure, "") - checkWSTestIndex(t, "./ci/out/wstestClientReports/index.json") + checkWSTestIndex(t, "./ci/out/autobahn-report/index.json") } func waitWS(ctx context.Context, url string) error { @@ -109,9 +115,7 @@ func waitWS(ctx context.Context, url string) error { return ctx.Err() } -// TODO: Let docker pick the port and use docker port to find it. -// Does mean we can't use -i but that's fine. -func wstestServer(ctx context.Context) (url string, closeFn func() error, err error) { +func wstestServer(tb testing.TB, ctx context.Context) (url string, closeFn func() error, err error) { defer errd.Wrap(&err, "failed to start autobahn wstest server") serverAddr, err := unusedListenAddr() @@ -124,7 +128,7 @@ func wstestServer(ctx context.Context) (url string, closeFn func() error, err er } url = "ws://" + serverAddr - const outDir = "ci/out/wstestClientReports" + const outDir = "ci/out/autobahn-report" specFile, err := tempJSONFile(map[string]interface{}{ "url": url, @@ -144,9 +148,15 @@ func wstestServer(ctx context.Context) (url string, closeFn func() error, err er }() dockerPull := exec.CommandContext(ctx, "docker", "pull", "crossbario/autobahn-testsuite") - // TODO: log to *testing.T - dockerPull.Stdout = os.Stdout - dockerPull.Stderr = os.Stderr + dockerPull.Stdout = util.WriterFunc(func(p []byte) (int, error) { + tb.Log(string(p)) + return len(p), nil + }) + dockerPull.Stderr = util.WriterFunc(func(p []byte) (int, error) { + tb.Log(string(p)) + return len(p), nil + }) + tb.Log(dockerPull) err = dockerPull.Run() if err != nil { return "", nil, fmt.Errorf("failed to pull docker image: %w", err) @@ -169,23 +179,32 @@ func wstestServer(ctx context.Context) (url string, closeFn func() error, err er // See https://github.com/crossbario/autobahn-testsuite/blob/058db3a36b7c3a1edf68c282307c6b899ca4857f/autobahntestsuite/autobahntestsuite/wstest.py#L124 "--webport=0", ) - fmt.Println(strings.Join(args, " ")) wstest := exec.CommandContext(ctx, "docker", args...) - // TODO: log to *testing.T - wstest.Stdout = os.Stdout - wstest.Stderr = os.Stderr + wstest.Stdout = util.WriterFunc(func(p []byte) (int, error) { + tb.Log(string(p)) + return len(p), nil + }) + wstest.Stderr = util.WriterFunc(func(p []byte) (int, error) { + tb.Log(string(p)) + return len(p), nil + }) + tb.Log(wstest) err = wstest.Start() if err != nil { return "", nil, fmt.Errorf("failed to start wstest: %w", err) } - // TODO: kill return url, func() error { err = wstest.Process.Kill() if err != nil { return fmt.Errorf("failed to kill wstest: %w", err) } - return nil + err = wstest.Wait() + var ee *exec.ExitError + if errors.As(err, &ee) && ee.ExitCode() == -1 { + return nil + } + return err }, nil } From d22d1f39eaacd5e6560e9d62c0d364477ff51604 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 00:23:29 -0700 Subject: [PATCH 418/519] ci/test.sh: Always benchmark --- .github/workflows/ci.yml | 9 +++++++++ .github/workflows/daily.yml | 4 ++-- ci/test.sh | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b88e81c..3c650580 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,3 +36,12 @@ jobs: with: name: coverage.html path: ./ci/out/coverage.html + + bench: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v4 + with: + go-version-file: ./go.mod + - run: ./ci/bench.sh diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index b625fd68..b1e64fbc 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/setup-go@v4 with: go-version-file: ./go.mod - - run: AUTOBAHN=1 ./ci/test.sh -bench=. + - run: AUTOBAHN=1 ./ci/test.sh - uses: actions/upload-artifact@v3 with: name: coverage.html @@ -47,7 +47,7 @@ jobs: - uses: actions/setup-go@v4 with: go-version-file: ./go.mod - - run: AUTOBAHN=1 ./ci/test.sh -bench=. + - run: AUTOBAHN=1 ./ci/test.sh - uses: actions/upload-artifact@v3 with: name: coverage.html diff --git a/ci/test.sh b/ci/test.sh index 32bdcec1..eadfb9fe 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -3,7 +3,7 @@ set -eu cd -- "$(dirname "$0")/.." go install github.com/agnivade/wasmbrowsertest@latest -go test --race --timeout=1h --covermode=atomic --coverprofile=ci/out/coverage.prof --coverpkg=./... "$@" ./... +go test --race --bench=. --timeout=1h --covermode=atomic --coverprofile=ci/out/coverage.prof --coverpkg=./... "$@" ./... sed -i.bak '/stringer\.go/d' ci/out/coverage.prof sed -i.bak '/nhooyr.io\/websocket\/internal\/test/d' ci/out/coverage.prof sed -i.bak '/examples/d' ci/out/coverage.prof From 50952d771f238f37ad20bb69c1d8e7ea7cac4ee6 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 00:24:06 -0700 Subject: [PATCH 419/519] compress.go: Rewrite compression docs --- compress.go | 50 +++++++++++++++++++++++++----------------------- compress_test.go | 27 ++++++++++++++++++++++++++ write.go | 2 +- 3 files changed, 54 insertions(+), 25 deletions(-) diff --git a/compress.go b/compress.go index 81de751b..d7a40d3b 100644 --- a/compress.go +++ b/compress.go @@ -9,43 +9,45 @@ import ( "sync" ) -// CompressionMode represents the modes available to the deflate extension. +// CompressionMode represents the modes available to the permessage-deflate extension. // See https://tools.ietf.org/html/rfc7692 // -// Works in all browsers except Safari which does not implement the deflate extension. +// Works in all modern browsers except Safari which does not implement the permessage-deflate extension. +// +// Compression is only used if the peer supports the mode selected. type CompressionMode int const ( - // CompressionDisabled disables the deflate extension. - // - // Use this if you are using a predominantly binary protocol with very - // little duplication in between messages or CPU and memory are more - // important than bandwidth. + // CompressionDisabled disables the negotiation of the permessage-deflate extension. // - // This is the default. + // This is the default. Do not enable compression without benchmarking for your particular use case first. CompressionDisabled CompressionMode = iota - // CompressionContextTakeover uses a 32 kB sliding window and flate.Writer per connection. - // It reuses the sliding window from previous messages. - // As most WebSocket protocols are repetitive, this can be very efficient. - // It carries an overhead of 32 kB + 1.2 MB for every connection compared to CompressionNoContextTakeover. + // CompressionNoContextTakeover compresses each message greater than 512 bytes. Each message is compressed with + // a new 1.2 MB flate.Writer pulled from a sync.Pool. Each message is read with a 40 KB flate.Reader pulled from + // a sync.Pool. // - // Sometime in the future it will carry 65 kB overhead instead once https://github.com/golang/go/issues/36919 - // is fixed. + // This means less efficient compression as the sliding window from previous messages will not be used but the + // memory overhead will be lower as there will be no fixed cost for the flate.Writer nor the 32 KB sliding window. + // Especially if the connections are long lived and seldom written to. // - // If the peer negotiates NoContextTakeover on the client or server side, it will be - // used instead as this is required by the RFC. - CompressionContextTakeover + // Thus, it uses less memory than CompressionContextTakeover but compresses less efficiently. + // + // If the peer does not support CompressionNoContextTakeover then we will fall back to CompressionDisabled. + CompressionNoContextTakeover - // CompressionNoContextTakeover grabs a new flate.Reader and flate.Writer as needed - // for every message. This applies to both server and client side. + // CompressionContextTakeover compresses each message greater than 128 bytes reusing the 32 KB sliding window from + // previous messages. i.e compression context across messages is preserved. // - // This means less efficient compression as the sliding window from previous messages - // will not be used but the memory overhead will be lower if the connections - // are long lived and seldom used. + // As most WebSocket protocols are text based and repetitive, this compression mode can be very efficient. // - // The message will only be compressed if greater than 512 bytes. - CompressionNoContextTakeover + // The memory overhead is a fixed 32 KB sliding window, a fixed 1.2 MB flate.Writer and a sync.Pool of 40 KB flate.Reader's + // that are used when reading and then returned. + // + // Thus, it uses more memory than CompressionNoContextTakeover but compresses more efficiently. + // + // If the peer does not support CompressionContextTakeover then we will fall back to CompressionNoContextTakeover. + CompressionContextTakeover ) func (m CompressionMode) opts() *compressionOptions { diff --git a/compress_test.go b/compress_test.go index 7b0e3a68..667e1408 100644 --- a/compress_test.go +++ b/compress_test.go @@ -4,6 +4,9 @@ package websocket import ( + "bytes" + "compress/flate" + "io" "strings" "testing" @@ -33,3 +36,27 @@ func Test_slidingWindow(t *testing.T) { }) } } + +func BenchmarkFlateWriter(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + w, _ := flate.NewWriter(io.Discard, flate.BestSpeed) + // We have to write a byte to get the writer to allocate to its full extent. + w.Write([]byte{'a'}) + w.Flush() + } +} + +func BenchmarkFlateReader(b *testing.B) { + b.ReportAllocs() + + var buf bytes.Buffer + w, _ := flate.NewWriter(&buf, flate.BestSpeed) + w.Write([]byte{'a'}) + w.Flush() + + for i := 0; i < b.N; i++ { + r := flate.NewReader(bytes.NewReader(buf.Bytes())) + io.ReadAll(r) + } +} diff --git a/write.go b/write.go index 708d5a6a..a6a137d1 100644 --- a/write.go +++ b/write.go @@ -38,7 +38,7 @@ func (c *Conn) Writer(ctx context.Context, typ MessageType) (io.WriteCloser, err // // See the Writer method if you want to stream a message. // -// If compression is disabled or the threshold is not met, then it +// If compression is disabled or the compression threshold is not met, then it // will write the message in a single frame. func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { _, err := c.write(ctx, typ, p) From 9d9c9718e1e1a6822e7dfb51a38029416135838c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 00:37:16 -0700 Subject: [PATCH 420/519] Update docs --- README.md | 23 ++++++++++++----------- doc.go | 2 +- example_test.go | 14 +++++++------- internal/examples/chat/chat.go | 2 +- internal/examples/echo/server.go | 2 +- 5 files changed, 22 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index f1a45972..5d2fa1c5 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,9 @@ websocket is a minimal and idiomatic WebSocket library for Go. -> **note**: I haven't been responsive for questions/reports on the issue tracker but I do -> read through and there are no outstanding bugs. There are certainly some nice to haves -> that I should merge in/figure out but nothing critical. I haven't given up on adding new -> features and cleaning up the code further, just been busy. Should anything critical -> arise, I will fix it. - ## Install -```bash +```sh go get nhooyr.io/websocket ``` @@ -23,18 +17,23 @@ go get nhooyr.io/websocket - First class [context.Context](https://blog.golang.org/context) support - Fully passes the WebSocket [autobahn-testsuite](https://github.com/crossbario/autobahn-testsuite) - [Zero dependencies](https://pkg.go.dev/nhooyr.io/websocket?tab=imports) -- 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 +- JSON helpers in the [wsjson](https://pkg.go.dev/nhooyr.io/websocket/wsjson) subpackage - Zero alloc reads and writes - Concurrent writes - [Close handshake](https://pkg.go.dev/nhooyr.io/websocket#Conn.Close) - [net.Conn](https://pkg.go.dev/nhooyr.io/websocket#NetConn) wrapper - [Ping pong](https://pkg.go.dev/nhooyr.io/websocket#Conn.Ping) API - [RFC 7692](https://tools.ietf.org/html/rfc7692) permessage-deflate compression +- [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper for write only connections - Compile to [Wasm](https://pkg.go.dev/nhooyr.io/websocket#hdr-Wasm) ## Roadmap +- [ ] Ping pong heartbeat helper [#267](https://github.com/nhooyr/websocket/issues/267) +- [ ] Graceful shutdown helper [#209](https://github.com/nhooyr/websocket/issues/209) +- [ ] Assembly for WebSocket masking [#16](https://github.com/nhooyr/websocket/issues/16) - [ ] HTTP/2 [#4](https://github.com/nhooyr/websocket/issues/4) +- [ ] The holy grail [#402](https://github.com/nhooyr/websocket/issues/402) ## Examples @@ -51,7 +50,7 @@ http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) { if err != nil { // ... } - defer c.Close(websocket.StatusInternalError, "the sky is falling") + defer c.CloseNow() ctx, cancel := context.WithTimeout(r.Context(), time.Second*10) defer cancel() @@ -78,7 +77,7 @@ c, _, err := websocket.Dial(ctx, "ws://localhost:8080", nil) if err != nil { // ... } -defer c.Close(websocket.StatusInternalError, "the sky is falling") +defer c.CloseNow() err = wsjson.Write(ctx, c, "hi") if err != nil { @@ -110,12 +109,14 @@ Advantages of nhooyr.io/websocket: - Gorilla writes directly to a net.Conn and so duplicates features of net/http.Client. - Concurrent writes - Close handshake ([gorilla/websocket#448](https://github.com/gorilla/websocket/issues/448)) +- [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper for write only connections - Idiomatic [ping pong](https://pkg.go.dev/nhooyr.io/websocket#Conn.Ping) API - Gorilla requires registering a pong callback before sending a Ping - Can target Wasm ([gorilla/websocket#432](https://github.com/gorilla/websocket/issues/432)) -- 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 +- Transparent message buffer reuse with [wsjson](https://pkg.go.dev/nhooyr.io/websocket/wsjson) subpackage - [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster WebSocket masking implementation in pure Go - Gorilla's implementation is slower and uses [unsafe](https://golang.org/pkg/unsafe/). + Soon we'll have assembly and be 4.5x faster [#326](https://github.com/nhooyr/websocket/pull/326) - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support - Gorilla only supports no context takeover mode - [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492)) diff --git a/doc.go b/doc.go index a2b873c7..ea38aa34 100644 --- a/doc.go +++ b/doc.go @@ -13,7 +13,7 @@ // // The examples are the best way to understand how to correctly use the library. // -// The wsjson and wspb subpackages contain helpers for JSON and protobuf messages. +// The wsjson subpackage contain helpers for JSON and protobuf messages. // // More documentation at https://nhooyr.io/websocket. // diff --git a/example_test.go b/example_test.go index 2e55eb96..590c0411 100644 --- a/example_test.go +++ b/example_test.go @@ -20,7 +20,7 @@ func ExampleAccept() { log.Println(err) return } - defer c.Close(websocket.StatusInternalError, "the sky is falling") + defer c.CloseNow() ctx, cancel := context.WithTimeout(r.Context(), time.Second*10) defer cancel() @@ -50,7 +50,7 @@ func ExampleDial() { if err != nil { log.Fatal(err) } - defer c.Close(websocket.StatusInternalError, "the sky is falling") + defer c.CloseNow() err = wsjson.Write(ctx, c, "hi") if err != nil { @@ -71,7 +71,7 @@ func ExampleCloseStatus() { if err != nil { log.Fatal(err) } - defer c.Close(websocket.StatusInternalError, "the sky is falling") + defer c.CloseNow() _, _, err = c.Reader(ctx) if websocket.CloseStatus(err) != websocket.StatusNormalClosure { @@ -88,7 +88,7 @@ func Example_writeOnly() { log.Println(err) return } - defer c.Close(websocket.StatusInternalError, "the sky is falling") + defer c.CloseNow() ctx, cancel := context.WithTimeout(r.Context(), time.Minute*10) defer cancel() @@ -145,7 +145,7 @@ func ExampleConn_Ping() { if err != nil { log.Fatal(err) } - defer c.Close(websocket.StatusInternalError, "the sky is falling") + defer c.CloseNow() // Required to read the Pongs from the server. ctx = c.CloseRead(ctx) @@ -162,10 +162,10 @@ func ExampleConn_Ping() { // This example demonstrates full stack chat with an automated test. func Example_fullStackChat() { - // https://github.com/nhooyr/websocket/tree/master/examples/chat + // https://github.com/nhooyr/websocket/tree/master/internal/examples/chat } // This example demonstrates a echo server. func Example_echo() { - // https://github.com/nhooyr/websocket/tree/master/examples/echo + // https://github.com/nhooyr/websocket/tree/master/internal/examples/echo } diff --git a/internal/examples/chat/chat.go b/internal/examples/chat/chat.go index 9d393d87..78a5696a 100644 --- a/internal/examples/chat/chat.go +++ b/internal/examples/chat/chat.go @@ -74,7 +74,7 @@ func (cs *chatServer) subscribeHandler(w http.ResponseWriter, r *http.Request) { cs.logf("%v", err) return } - defer c.Close(websocket.StatusInternalError, "") + defer c.CloseNow() err = cs.subscribe(r.Context(), c) if errors.Is(err, context.Canceled) { diff --git a/internal/examples/echo/server.go b/internal/examples/echo/server.go index e9f70f03..246ad582 100644 --- a/internal/examples/echo/server.go +++ b/internal/examples/echo/server.go @@ -28,7 +28,7 @@ func (s echoServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.logf("%v", err) return } - defer c.Close(websocket.StatusInternalError, "the sky is falling") + defer c.CloseNow() if c.Subprotocol() != "echo" { c.Close(websocket.StatusPolicyViolation, "client must speak the echo subprotocol") From 25a5ca47d8d9c5edd0519f1c46d0bf1e685014a0 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 00:54:49 -0700 Subject: [PATCH 421/519] netconn.go: Fix panic on zero or negative deadline durations Glad no one ran into this in production. --- conn_test.go | 12 ++++++++++++ netconn.go | 12 ++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/conn_test.go b/conn_test.go index abc1c81d..17c52c32 100644 --- a/conn_test.go +++ b/conn_test.go @@ -236,6 +236,18 @@ func TestConn(t *testing.T) { assert.Equal(t, "read msg", s, string(b)) }) + t.Run("netConn/pastDeadline", func(t *testing.T) { + tt, c1, c2 := newConnTest(t, nil, nil) + + n1 := websocket.NetConn(tt.ctx, c1, websocket.MessageBinary) + n2 := websocket.NetConn(tt.ctx, c2, websocket.MessageBinary) + + n1.SetDeadline(time.Now().Add(-time.Minute)) + n2.SetDeadline(time.Now().Add(-time.Minute)) + + // No panic we're good. + }) + t.Run("wsjson", func(t *testing.T) { tt, c1, c2 := newConnTest(t, nil, nil) diff --git a/netconn.go b/netconn.go index e398b4f7..1667f45c 100644 --- a/netconn.go +++ b/netconn.go @@ -210,7 +210,11 @@ func (nc *netConn) SetWriteDeadline(t time.Time) error { if t.IsZero() { nc.writeTimer.Stop() } else { - nc.writeTimer.Reset(time.Until(t)) + dur := time.Until(t) + if dur <= 0 { + dur = 1 + } + nc.writeTimer.Reset(dur) } return nil } @@ -220,7 +224,11 @@ func (nc *netConn) SetReadDeadline(t time.Time) error { if t.IsZero() { nc.readTimer.Stop() } else { - nc.readTimer.Reset(time.Until(t)) + dur := time.Until(t) + if dur <= 0 { + dur = 1 + } + nc.readTimer.Reset(dur) } return nil } From cdeb9806656bd144fa49dcac6e717e88d6e919f0 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 00:55:44 -0700 Subject: [PATCH 422/519] ws_js.go: Add CloseNow --- doc.go | 1 + ws_js.go | 23 ++++++++++++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/doc.go b/doc.go index ea38aa34..2ab648a6 100644 --- a/doc.go +++ b/doc.go @@ -28,6 +28,7 @@ // // - Accept always errors out // - Conn.Ping is no-op +// - Conn.CloseNow is Close(StatusGoingAway, "") // - HTTPClient, HTTPHeader and CompressionMode in DialOptions are no-op // - *http.Response from Dial is &http.Response{} with a 101 status code on success package websocket // import "nhooyr.io/websocket" diff --git a/ws_js.go b/ws_js.go index 03919692..59bb685c 100644 --- a/ws_js.go +++ b/ws_js.go @@ -151,7 +151,7 @@ func (c *Conn) read(ctx context.Context) (MessageType, []byte, error) { return 0, nil, ctx.Err() case <-c.readSignal: case <-c.closed: - return 0, nil, c.closeErr + return 0, nil, errClosed } c.readBufMu.Lock() @@ -205,7 +205,7 @@ func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) error { if c.isClosed() { - return c.closeErr + return errClosed } switch typ { case MessageBinary: @@ -229,19 +229,28 @@ func (c *Conn) Close(code StatusCode, reason string) error { return nil } +// CloseNow closes the WebSocket connection without attempting a close handshake. +// Use When you do not want the overhead of the close handshake. +// +// note: No different from Close(StatusGoingAway, "") in WASM as there is no way to close +// a WebSocket without the close handshake. +func (c *Conn) CloseNow() error { + return c.Close(StatusGoingAway, "") +} + func (c *Conn) exportedClose(code StatusCode, reason string) error { c.closingMu.Lock() defer c.closingMu.Unlock() + if c.isClosed() { + return errClosed + } + ce := fmt.Errorf("sent close: %w", CloseError{ Code: code, Reason: reason, }) - if c.isClosed() { - return fmt.Errorf("tried to close with %q but connection already closed: %w", ce, c.closeErr) - } - c.setCloseErr(ce) err := c.ws.Close(int(code), reason) if err != nil { @@ -312,7 +321,7 @@ func dial(ctx context.Context, url string, opts *DialOptions) (*Conn, *http.Resp StatusCode: http.StatusSwitchingProtocols, }, nil case <-c.closed: - return nil, nil, c.closeErr + return nil, nil, errClosed } } From fb3b083efa5e72d35844426f20c8cfcdec00a57d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 00:56:56 -0700 Subject: [PATCH 423/519] close.go: Drop support for Go 1.13 --- close.go | 7 ++++--- close_go113.go | 9 --------- close_go116.go | 9 --------- conn.go | 7 ++++--- export_test.go | 8 ++++++-- read.go | 13 +++++++------ write.go | 9 +++++---- ws_js.go | 9 +++++---- 8 files changed, 31 insertions(+), 40 deletions(-) delete mode 100644 close_go113.go delete mode 100644 close_go116.go diff --git a/close.go b/close.go index fe1ced34..0abc864f 100644 --- a/close.go +++ b/close.go @@ -8,6 +8,7 @@ import ( "encoding/binary" "errors" "fmt" + "net" "time" "nhooyr.io/websocket/internal/errd" @@ -107,7 +108,7 @@ func (c *Conn) CloseNow() (err error) { defer errd.Wrap(&err, "failed to close WebSocket") if c.isClosed() { - return errClosed + return net.ErrClosed } c.close(nil) @@ -124,7 +125,7 @@ func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { return writeErr } - if CloseStatus(closeHandshakeErr) == -1 && !errors.Is(errClosed, closeHandshakeErr) { + if CloseStatus(closeHandshakeErr) == -1 && !errors.Is(net.ErrClosed, closeHandshakeErr) { return closeHandshakeErr } @@ -137,7 +138,7 @@ func (c *Conn) writeClose(code StatusCode, reason string) error { c.wroteClose = true c.closeMu.Unlock() if wroteClose { - return errClosed + return net.ErrClosed } ce := CloseError{ diff --git a/close_go113.go b/close_go113.go deleted file mode 100644 index caf1b89e..00000000 --- a/close_go113.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build !go1.16 && !js - -package websocket - -import ( - "errors" -) - -var errClosed = errors.New("use of closed network connection") diff --git a/close_go116.go b/close_go116.go deleted file mode 100644 index 9d986109..00000000 --- a/close_go116.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build go1.16 && !js - -package websocket - -import ( - "net" -) - -var errClosed = net.ErrClosed diff --git a/conn.go b/conn.go index 36662a93..3b3a9f98 100644 --- a/conn.go +++ b/conn.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "io" + "net" "runtime" "strconv" "sync" @@ -228,7 +229,7 @@ func (c *Conn) ping(ctx context.Context, p string) error { select { case <-c.closed: - return errClosed + return net.ErrClosed case <-ctx.Done(): err := fmt.Errorf("failed to wait for pong: %w", ctx.Err()) c.close(err) @@ -266,7 +267,7 @@ func (m *mu) tryLock() bool { func (m *mu) lock(ctx context.Context) error { select { case <-m.c.closed: - return errClosed + return net.ErrClosed case <-ctx.Done(): err := fmt.Errorf("failed to acquire lock: %w", ctx.Err()) m.c.close(err) @@ -279,7 +280,7 @@ func (m *mu) lock(ctx context.Context) error { case <-m.c.closed: // Make sure to release. m.unlock() - return errClosed + return net.ErrClosed default: } return nil diff --git a/export_test.go b/export_test.go index e322c36f..a644d8f0 100644 --- a/export_test.go +++ b/export_test.go @@ -3,7 +3,11 @@ package websocket -import "nhooyr.io/websocket/internal/util" +import ( + "net" + + "nhooyr.io/websocket/internal/util" +) func (c *Conn) RecordBytesWritten() *int { var bytesWritten int @@ -24,7 +28,7 @@ func (c *Conn) RecordBytesRead() *int { return &bytesRead } -var ErrClosed = errClosed +var ErrClosed = net.ErrClosed var ExportedDial = dial var SecWebSocketAccept = secWebSocketAccept diff --git a/read.go b/read.go index 72386088..9ab28812 100644 --- a/read.go +++ b/read.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "io" + "net" "strings" "time" @@ -206,7 +207,7 @@ func (c *Conn) readLoop(ctx context.Context) (header, error) { func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { select { case <-c.closed: - return header{}, errClosed + return header{}, net.ErrClosed case c.readTimeout <- ctx: } @@ -214,7 +215,7 @@ func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { if err != nil { select { case <-c.closed: - return header{}, errClosed + return header{}, net.ErrClosed case <-ctx.Done(): return header{}, ctx.Err() default: @@ -225,7 +226,7 @@ func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { select { case <-c.closed: - return header{}, errClosed + return header{}, net.ErrClosed case c.readTimeout <- context.Background(): } @@ -235,7 +236,7 @@ func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { select { case <-c.closed: - return 0, errClosed + return 0, net.ErrClosed case c.readTimeout <- ctx: } @@ -243,7 +244,7 @@ func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { if err != nil { select { case <-c.closed: - return n, errClosed + return n, net.ErrClosed case <-ctx.Done(): return n, ctx.Err() default: @@ -255,7 +256,7 @@ func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { select { case <-c.closed: - return n, errClosed + return n, net.ErrClosed case c.readTimeout <- context.Background(): } diff --git a/write.go b/write.go index a6a137d1..3d062656 100644 --- a/write.go +++ b/write.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "io" + "net" "time" "compress/flate" @@ -262,14 +263,14 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco case <-ctx.Done(): return 0, ctx.Err() case <-c.closed: - return 0, errClosed + return 0, net.ErrClosed } } defer c.writeFrameMu.unlock() select { case <-c.closed: - return 0, errClosed + return 0, net.ErrClosed case c.writeTimeout <- ctx: } @@ -277,7 +278,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco if err != nil { select { case <-c.closed: - err = errClosed + err = net.ErrClosed case <-ctx.Done(): err = ctx.Err() default: @@ -327,7 +328,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco if opcode == opClose { return n, nil } - return n, errClosed + return n, net.ErrClosed case c.writeTimeout <- context.Background(): } diff --git a/ws_js.go b/ws_js.go index 59bb685c..cae68bb6 100644 --- a/ws_js.go +++ b/ws_js.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "net" "net/http" "reflect" "runtime" @@ -151,7 +152,7 @@ func (c *Conn) read(ctx context.Context) (MessageType, []byte, error) { return 0, nil, ctx.Err() case <-c.readSignal: case <-c.closed: - return 0, nil, errClosed + return 0, nil, net.ErrClosed } c.readBufMu.Lock() @@ -205,7 +206,7 @@ func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) error { if c.isClosed() { - return errClosed + return net.ErrClosed } switch typ { case MessageBinary: @@ -243,7 +244,7 @@ func (c *Conn) exportedClose(code StatusCode, reason string) error { defer c.closingMu.Unlock() if c.isClosed() { - return errClosed + return net.ErrClosed } ce := fmt.Errorf("sent close: %w", CloseError{ @@ -321,7 +322,7 @@ func dial(ctx context.Context, url string, opts *DialOptions) (*Conn, *http.Resp StatusCode: http.StatusSwitchingProtocols, }, nil case <-c.closed: - return nil, nil, errClosed + return nil, nil, net.ErrClosed } } From 9fdcb5d7dd378874db50414680094c7443cb007a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 01:07:11 -0700 Subject: [PATCH 424/519] Misc fixes --- README.md | 2 +- ci/test.sh | 18 +++++++++--------- compress.go | 26 +++++++++++++------------- make.sh | 4 ++++ 4 files changed, 27 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 5d2fa1c5..40921d41 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # websocket [![godoc](https://godoc.org/nhooyr.io/websocket?status.svg)](https://pkg.go.dev/nhooyr.io/websocket) -[![coverage](https://img.shields.io/badge/coverage-86%25-success)](https://nhooyrio-websocket-coverage.netlify.app) +[![coverage](https://img.shields.io/badge/coverage-89%25-success)](https://nhooyr.io/websocket/coverage.html) websocket is a minimal and idiomatic WebSocket library for Go. diff --git a/ci/test.sh b/ci/test.sh index eadfb9fe..83bb9832 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -2,6 +2,15 @@ set -eu cd -- "$(dirname "$0")/.." +( + cd ./internal/examples + go test "$@" ./... +) +( + cd ./internal/thirdparty + go test "$@" ./... +) + go install github.com/agnivade/wasmbrowsertest@latest go test --race --bench=. --timeout=1h --covermode=atomic --coverprofile=ci/out/coverage.prof --coverpkg=./... "$@" ./... sed -i.bak '/stringer\.go/d' ci/out/coverage.prof @@ -12,12 +21,3 @@ sed -i.bak '/examples/d' ci/out/coverage.prof go tool cover -func ci/out/coverage.prof | tail -n1 go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html - -( - cd ./internal/examples - go test "$@" ./... -) -( - cd ./internal/thirdparty - go test "$@" ./... -) diff --git a/compress.go b/compress.go index d7a40d3b..1f3adcfb 100644 --- a/compress.go +++ b/compress.go @@ -23,19 +23,6 @@ const ( // This is the default. Do not enable compression without benchmarking for your particular use case first. CompressionDisabled CompressionMode = iota - // CompressionNoContextTakeover compresses each message greater than 512 bytes. Each message is compressed with - // a new 1.2 MB flate.Writer pulled from a sync.Pool. Each message is read with a 40 KB flate.Reader pulled from - // a sync.Pool. - // - // This means less efficient compression as the sliding window from previous messages will not be used but the - // memory overhead will be lower as there will be no fixed cost for the flate.Writer nor the 32 KB sliding window. - // Especially if the connections are long lived and seldom written to. - // - // Thus, it uses less memory than CompressionContextTakeover but compresses less efficiently. - // - // If the peer does not support CompressionNoContextTakeover then we will fall back to CompressionDisabled. - CompressionNoContextTakeover - // CompressionContextTakeover compresses each message greater than 128 bytes reusing the 32 KB sliding window from // previous messages. i.e compression context across messages is preserved. // @@ -48,6 +35,19 @@ const ( // // If the peer does not support CompressionContextTakeover then we will fall back to CompressionNoContextTakeover. CompressionContextTakeover + + // CompressionNoContextTakeover compresses each message greater than 512 bytes. Each message is compressed with + // a new 1.2 MB flate.Writer pulled from a sync.Pool. Each message is read with a 40 KB flate.Reader pulled from + // a sync.Pool. + // + // This means less efficient compression as the sliding window from previous messages will not be used but the + // memory overhead will be lower as there will be no fixed cost for the flate.Writer nor the 32 KB sliding window. + // Especially if the connections are long lived and seldom written to. + // + // Thus, it uses less memory than CompressionContextTakeover but compresses less efficiently. + // + // If the peer does not support CompressionNoContextTakeover then we will fall back to CompressionDisabled. + CompressionNoContextTakeover ) func (m CompressionMode) opts() *compressionOptions { diff --git a/make.sh b/make.sh index 81909d72..170d00a8 100755 --- a/make.sh +++ b/make.sh @@ -2,7 +2,11 @@ set -eu cd -- "$(dirname "$0")" +echo "=== fmt.sh" ./ci/fmt.sh +echo "=== lint.sh" ./ci/lint.sh +echo "=== test.sh" ./ci/test.sh "$@" +echo "=== bench.sh" ./ci/bench.sh From db79f72fc2efc3f1347fa61c7f0ecc5f3fdd47b0 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 01:20:57 -0700 Subject: [PATCH 425/519] Update README.md --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 40921d41..ba935586 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,15 @@ go get nhooyr.io/websocket ## Roadmap +See GitHub issues for minor issues but the major future enhancements are: + +- [ ] Perfect examples [#217](https://github.com/nhooyr/websocket/issues/217) +- [ ] wstest.Pipe for in memory testing [#340](https://github.com/nhooyr/websocket/issues/340) - [ ] Ping pong heartbeat helper [#267](https://github.com/nhooyr/websocket/issues/267) -- [ ] Graceful shutdown helper [#209](https://github.com/nhooyr/websocket/issues/209) +- [ ] Ping pong instrumentation callbacks [#246](https://github.com/nhooyr/websocket/issues/246) +- [ ] Graceful shutdown helpers [#209](https://github.com/nhooyr/websocket/issues/209) - [ ] Assembly for WebSocket masking [#16](https://github.com/nhooyr/websocket/issues/16) + - WIP at [#326](https://github.com/nhooyr/websocket/pull/326), about 3x faster - [ ] HTTP/2 [#4](https://github.com/nhooyr/websocket/issues/4) - [ ] The holy grail [#402](https://github.com/nhooyr/websocket/issues/402) From 108d137e4ce60d187d27c2455f6b056933ee83a8 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 01:37:27 -0700 Subject: [PATCH 426/519] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index ba935586..8850f511 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # websocket [![godoc](https://godoc.org/nhooyr.io/websocket?status.svg)](https://pkg.go.dev/nhooyr.io/websocket) -[![coverage](https://img.shields.io/badge/coverage-89%25-success)](https://nhooyr.io/websocket/coverage.html) +[![coverage](https://img.shields.io/badge/coverage-91%25-success)](https://nhooyr.io/websocket/coverage.html) websocket is a minimal and idiomatic WebSocket library for Go. @@ -126,7 +126,6 @@ Advantages of nhooyr.io/websocket: - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support - Gorilla only supports no context takeover mode - [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492)) -- Actively maintained ([gorilla/websocket#370](https://github.com/gorilla/websocket/issues/370)) #### golang.org/x/net/websocket From e6a7e0e8e6fe579058a23bef78e03a17172d6ed6 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 01:56:06 -0700 Subject: [PATCH 427/519] main_test.go: Add to detect goroutine leaks Updates #330 --- main_test.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 main_test.go diff --git a/main_test.go b/main_test.go new file mode 100644 index 00000000..336be71c --- /dev/null +++ b/main_test.go @@ -0,0 +1,17 @@ +package websocket_test + +import ( + "fmt" + "os" + "runtime" + "testing" +) + +func TestMain(m *testing.M) { + code := m.Run() + if runtime.NumGoroutine() != 1 { + fmt.Fprintf(os.Stderr, "goroutine leak detected, expected 1 but got %d goroutines\n", runtime.NumGoroutine()) + os.Exit(1) + } + os.Exit(code) +} From 6ed989afc10be2cf8139362ca006cad4a1cb98d8 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 02:51:36 -0700 Subject: [PATCH 428/519] Ensure no goroutines leak after Close Closes #330 --- conn.go | 34 ++++++++++++++++++++++++---------- conn_test.go | 17 +++++++++-------- dial_test.go | 3 ++- main_test.go | 15 ++++++++++++++- read.go | 5 +++++ write.go | 25 +++++++++++++++---------- ws_js.go | 2 +- 7 files changed, 70 insertions(+), 31 deletions(-) diff --git a/conn.go b/conn.go index 3b3a9f98..5084dce1 100644 --- a/conn.go +++ b/conn.go @@ -53,8 +53,10 @@ type Conn struct { br *bufio.Reader bw *bufio.Writer - readTimeout chan context.Context - writeTimeout chan context.Context + timeoutLoopCancel context.CancelFunc + timeoutLoopDone chan struct{} + readTimeout chan context.Context + writeTimeout chan context.Context // Read state. readMu *mu @@ -102,8 +104,9 @@ func newConn(cfg connConfig) *Conn { br: cfg.br, bw: cfg.bw, - readTimeout: make(chan context.Context), - writeTimeout: make(chan context.Context), + timeoutLoopDone: make(chan struct{}), + readTimeout: make(chan context.Context), + writeTimeout: make(chan context.Context), closed: make(chan struct{}), activePings: make(map[string]chan<- struct{}), @@ -130,7 +133,9 @@ func newConn(cfg connConfig) *Conn { c.close(errors.New("connection garbage collected")) }) - go c.timeoutLoop() + var ctx context.Context + ctx, c.timeoutLoopCancel = context.WithCancel(context.Background()) + go c.timeoutLoop(ctx) return c } @@ -152,6 +157,10 @@ func (c *Conn) close(err error) { err = c.rwc.Close() } c.setCloseErrLocked(err) + + c.timeoutLoopCancel() + <-c.timeoutLoopDone + close(c.closed) runtime.SetFinalizer(c, nil) @@ -160,18 +169,23 @@ func (c *Conn) close(err error) { // closeErr. c.rwc.Close() - go func() { - c.msgWriter.close() - c.msgReader.close() - }() + c.closeMu.Unlock() + defer c.closeMu.Lock() + + c.msgWriter.close() + c.msgReader.close() } -func (c *Conn) timeoutLoop() { +func (c *Conn) timeoutLoop(ctx context.Context) { + defer close(c.timeoutLoopDone) + readCtx := context.Background() writeCtx := context.Background() for { select { + case <-ctx.Done(): + return case <-c.closed: return diff --git a/conn_test.go b/conn_test.go index 17c52c32..97b172dc 100644 --- a/conn_test.go +++ b/conn_test.go @@ -399,10 +399,8 @@ func newConnTest(t testing.TB, dialOpts *websocket.DialOptions, acceptOpts *webs c1, c2 = c2, c1 } t.Cleanup(func() { - // We don't actually care whether this succeeds so we just run it in a separate goroutine to avoid - // blocking the test shutting down. - go c2.Close(websocket.StatusInternalError, "") - go c1.Close(websocket.StatusInternalError, "") + c2.CloseNow() + c1.CloseNow() }) return tt, c1, c2 @@ -596,16 +594,19 @@ func TestConcurrentClosePing(t *testing.T) { defer c2.CloseNow() c1.CloseRead(context.Background()) c2.CloseRead(context.Background()) - go func() { + errc := xsync.Go(func() error { for range time.Tick(time.Millisecond) { - if err := c1.Ping(context.Background()); err != nil { - return + err := c1.Ping(context.Background()) + if err != nil { + return err } } - }() + panic("unreachable") + }) time.Sleep(10 * time.Millisecond) assert.Success(t, c1.Close(websocket.StatusNormalClosure, "")) + <-errc }() } } diff --git a/dial_test.go b/dial_test.go index 63cb4be6..237a2874 100644 --- a/dial_test.go +++ b/dial_test.go @@ -164,11 +164,12 @@ func Test_verifyHostOverride(t *testing.T) { }, nil } - _, _, err := websocket.Dial(ctx, "ws://example.com", &websocket.DialOptions{ + c, _, err := websocket.Dial(ctx, "ws://example.com", &websocket.DialOptions{ HTTPClient: mockHTTPClient(rt), Host: tc.host, }) assert.Success(t, err) + c.CloseNow() }) } diff --git a/main_test.go b/main_test.go index 336be71c..2b93bb18 100644 --- a/main_test.go +++ b/main_test.go @@ -7,10 +7,23 @@ import ( "testing" ) +func goroutineStacks() []byte { + buf := make([]byte, 512) + for { + m := runtime.Stack(buf, true) + if m < len(buf) { + return buf[:m] + } + buf = make([]byte, len(buf)*2) + } +} + func TestMain(m *testing.M) { code := m.Run() - if runtime.NumGoroutine() != 1 { + if runtime.GOOS != "js" && runtime.NumGoroutine() != 1 || + runtime.GOOS == "js" && runtime.NumGoroutine() != 2 { fmt.Fprintf(os.Stderr, "goroutine leak detected, expected 1 but got %d goroutines\n", runtime.NumGoroutine()) + fmt.Fprintf(os.Stderr, "%s\n", goroutineStacks()) os.Exit(1) } os.Exit(code) diff --git a/read.go b/read.go index 9ab28812..5c180fba 100644 --- a/read.go +++ b/read.go @@ -219,6 +219,7 @@ func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { case <-ctx.Done(): return header{}, ctx.Err() default: + c.readMu.unlock() c.close(err) return header{}, err } @@ -249,6 +250,7 @@ func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { return n, ctx.Err() default: err = fmt.Errorf("failed to read frame payload: %w", err) + c.readMu.unlock() c.close(err) return n, err } @@ -319,6 +321,7 @@ func (c *Conn) handleControl(ctx context.Context, h header) (err error) { err = fmt.Errorf("received close frame: %w", ce) c.setCloseErr(err) c.writeClose(ce.Code, ce.Reason) + c.readMu.unlock() c.close(err) return err } @@ -334,6 +337,7 @@ func (c *Conn) reader(ctx context.Context) (_ MessageType, _ io.Reader, err erro if !c.msgReader.fin { err = errors.New("previous message not read to completion") + c.readMu.unlock() c.close(fmt.Errorf("failed to get reader: %w", err)) return 0, nil, err } @@ -409,6 +413,7 @@ func (mr *msgReader) Read(p []byte) (n int, err error) { } if err != nil { err = fmt.Errorf("failed to read: %w", err) + mr.c.readMu.unlock() mr.c.close(err) } return n, err diff --git a/write.go b/write.go index 3d062656..0fbfd9cd 100644 --- a/write.go +++ b/write.go @@ -109,7 +109,7 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error if !c.flate() { defer c.msgWriter.mu.unlock() - return c.writeFrame(ctx, true, false, c.msgWriter.opcode, p) + return c.writeFrame(true, ctx, true, false, c.msgWriter.opcode, p) } n, err := mw.Write(p) @@ -159,6 +159,7 @@ func (mw *msgWriter) Write(p []byte) (_ int, err error) { defer func() { if err != nil { err = fmt.Errorf("failed to write: %w", err) + mw.writeMu.unlock() mw.c.close(err) } }() @@ -179,7 +180,7 @@ func (mw *msgWriter) Write(p []byte) (_ int, err error) { } func (mw *msgWriter) write(p []byte) (int, error) { - n, err := mw.c.writeFrame(mw.ctx, false, mw.flate, mw.opcode, p) + n, err := mw.c.writeFrame(true, mw.ctx, false, mw.flate, mw.opcode, p) if err != nil { return n, fmt.Errorf("failed to write data frame: %w", err) } @@ -191,17 +192,17 @@ func (mw *msgWriter) write(p []byte) (int, error) { func (mw *msgWriter) Close() (err error) { defer errd.Wrap(&err, "failed to close writer") - if mw.closed { - return errors.New("writer already closed") - } - mw.closed = true - err = mw.writeMu.lock(mw.ctx) if err != nil { return err } defer mw.writeMu.unlock() + if mw.closed { + return errors.New("writer already closed") + } + mw.closed = true + if mw.flate { err = mw.flateWriter.Flush() if err != nil { @@ -209,7 +210,7 @@ func (mw *msgWriter) Close() (err error) { } } - _, err = mw.c.writeFrame(mw.ctx, true, mw.flate, mw.opcode, nil) + _, err = mw.c.writeFrame(true, mw.ctx, true, mw.flate, mw.opcode, nil) if err != nil { return fmt.Errorf("failed to write fin frame: %w", err) } @@ -235,7 +236,7 @@ func (c *Conn) writeControl(ctx context.Context, opcode opcode, p []byte) error ctx, cancel := context.WithTimeout(ctx, time.Second*5) defer cancel() - _, err := c.writeFrame(ctx, true, false, opcode, p) + _, err := c.writeFrame(false, ctx, true, false, opcode, p) if err != nil { return fmt.Errorf("failed to write control frame %v: %w", opcode, err) } @@ -243,7 +244,7 @@ func (c *Conn) writeControl(ctx context.Context, opcode opcode, p []byte) error } // frame handles all writes to the connection. -func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opcode, p []byte) (_ int, err error) { +func (c *Conn) writeFrame(msgWriter bool, ctx context.Context, fin bool, flate bool, opcode opcode, p []byte) (_ int, err error) { err = c.writeFrameMu.lock(ctx) if err != nil { return 0, err @@ -283,6 +284,10 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco err = ctx.Err() default: } + c.writeFrameMu.unlock() + if msgWriter { + c.msgWriter.writeMu.unlock() + } c.close(err) err = fmt.Errorf("failed to write frame: %w", err) } diff --git a/ws_js.go b/ws_js.go index cae68bb6..180d0564 100644 --- a/ws_js.go +++ b/ws_js.go @@ -231,7 +231,7 @@ func (c *Conn) Close(code StatusCode, reason string) error { } // CloseNow closes the WebSocket connection without attempting a close handshake. -// Use When you do not want the overhead of the close handshake. +// Use when you do not want the overhead of the close handshake. // // note: No different from Close(StatusGoingAway, "") in WASM as there is no way to close // a WebSocket without the close handshake. From d7a55cff33db1eebcd8eb4dcb42cb736b24d46a9 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 03:14:35 -0700 Subject: [PATCH 429/519] Ensure no goroutines leak after Close in a cleaner way Closes #330 --- close.go | 4 +++- conn.go | 51 +++++++++++++++++++++++++++------------------------ read.go | 8 +++----- write.go | 23 +++++++++-------------- 4 files changed, 42 insertions(+), 44 deletions(-) diff --git a/close.go b/close.go index 0abc864f..1053751c 100644 --- a/close.go +++ b/close.go @@ -99,12 +99,14 @@ func CloseStatus(err error) StatusCode { // Close will unblock all goroutines interacting with the connection once // complete. func (c *Conn) Close(code StatusCode, reason string) error { + defer c.wgWait() return c.closeHandshake(code, reason) } // CloseNow closes the WebSocket connection without attempting a close handshake. -// Use When you do not want the overhead of the close handshake. +// Use when you do not want the overhead of the close handshake. func (c *Conn) CloseNow() (err error) { + defer c.wgWait() defer errd.Wrap(&err, "failed to close WebSocket") if c.isClosed() { diff --git a/conn.go b/conn.go index 5084dce1..05531c3b 100644 --- a/conn.go +++ b/conn.go @@ -45,6 +45,8 @@ const ( type Conn struct { noCopy + wg sync.WaitGroup + subprotocol string rwc io.ReadWriteCloser client bool @@ -53,10 +55,8 @@ type Conn struct { br *bufio.Reader bw *bufio.Writer - timeoutLoopCancel context.CancelFunc - timeoutLoopDone chan struct{} - readTimeout chan context.Context - writeTimeout chan context.Context + readTimeout chan context.Context + writeTimeout chan context.Context // Read state. readMu *mu @@ -104,9 +104,8 @@ func newConn(cfg connConfig) *Conn { br: cfg.br, bw: cfg.bw, - timeoutLoopDone: make(chan struct{}), - readTimeout: make(chan context.Context), - writeTimeout: make(chan context.Context), + readTimeout: make(chan context.Context), + writeTimeout: make(chan context.Context), closed: make(chan struct{}), activePings: make(map[string]chan<- struct{}), @@ -133,9 +132,7 @@ func newConn(cfg connConfig) *Conn { c.close(errors.New("connection garbage collected")) }) - var ctx context.Context - ctx, c.timeoutLoopCancel = context.WithCancel(context.Background()) - go c.timeoutLoop(ctx) + c.wgGo(c.timeoutLoop) return c } @@ -158,9 +155,6 @@ func (c *Conn) close(err error) { } c.setCloseErrLocked(err) - c.timeoutLoopCancel() - <-c.timeoutLoopDone - close(c.closed) runtime.SetFinalizer(c, nil) @@ -169,23 +163,18 @@ func (c *Conn) close(err error) { // closeErr. c.rwc.Close() - c.closeMu.Unlock() - defer c.closeMu.Lock() - - c.msgWriter.close() - c.msgReader.close() + c.wgGo(func() { + c.msgWriter.close() + c.msgReader.close() + }) } -func (c *Conn) timeoutLoop(ctx context.Context) { - defer close(c.timeoutLoopDone) - +func (c *Conn) timeoutLoop() { readCtx := context.Background() writeCtx := context.Background() for { select { - case <-ctx.Done(): - return case <-c.closed: return @@ -194,7 +183,9 @@ func (c *Conn) timeoutLoop(ctx context.Context) { case <-readCtx.Done(): c.setCloseErr(fmt.Errorf("read timed out: %w", readCtx.Err())) - go c.writeError(StatusPolicyViolation, errors.New("timed out")) + c.wgGo(func() { + c.writeError(StatusPolicyViolation, errors.New("read timed out")) + }) case <-writeCtx.Done(): c.close(fmt.Errorf("write timed out: %w", writeCtx.Err())) return @@ -311,3 +302,15 @@ func (m *mu) unlock() { type noCopy struct{} func (*noCopy) Lock() {} + +func (c *Conn) wgGo(fn func()) { + c.wg.Add(1) + go func() { + defer c.wg.Done() + fn() + }() +} + +func (c *Conn) wgWait() { + c.wg.Wait() +} diff --git a/read.go b/read.go index 5c180fba..8742842e 100644 --- a/read.go +++ b/read.go @@ -62,8 +62,11 @@ func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { // frames are responded to. This means c.Ping and c.Close will still work as expected. func (c *Conn) CloseRead(ctx context.Context) context.Context { ctx, cancel := context.WithCancel(ctx) + + c.wg.Add(1) go func() { defer c.CloseNow() + defer c.wg.Done() defer cancel() _, _, err := c.Reader(ctx) if err == nil { @@ -219,7 +222,6 @@ func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { case <-ctx.Done(): return header{}, ctx.Err() default: - c.readMu.unlock() c.close(err) return header{}, err } @@ -250,7 +252,6 @@ func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { return n, ctx.Err() default: err = fmt.Errorf("failed to read frame payload: %w", err) - c.readMu.unlock() c.close(err) return n, err } @@ -321,7 +322,6 @@ func (c *Conn) handleControl(ctx context.Context, h header) (err error) { err = fmt.Errorf("received close frame: %w", ce) c.setCloseErr(err) c.writeClose(ce.Code, ce.Reason) - c.readMu.unlock() c.close(err) return err } @@ -337,7 +337,6 @@ func (c *Conn) reader(ctx context.Context) (_ MessageType, _ io.Reader, err erro if !c.msgReader.fin { err = errors.New("previous message not read to completion") - c.readMu.unlock() c.close(fmt.Errorf("failed to get reader: %w", err)) return 0, nil, err } @@ -413,7 +412,6 @@ func (mr *msgReader) Read(p []byte) (n int, err error) { } if err != nil { err = fmt.Errorf("failed to read: %w", err) - mr.c.readMu.unlock() mr.c.close(err) } return n, err diff --git a/write.go b/write.go index 0fbfd9cd..7b1152ce 100644 --- a/write.go +++ b/write.go @@ -109,7 +109,7 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error if !c.flate() { defer c.msgWriter.mu.unlock() - return c.writeFrame(true, ctx, true, false, c.msgWriter.opcode, p) + return c.writeFrame(ctx, true, false, c.msgWriter.opcode, p) } n, err := mw.Write(p) @@ -146,20 +146,19 @@ func (mw *msgWriter) putFlateWriter() { // Write writes the given bytes to the WebSocket connection. func (mw *msgWriter) Write(p []byte) (_ int, err error) { - if mw.closed { - return 0, errors.New("cannot use closed writer") - } - err = mw.writeMu.lock(mw.ctx) if err != nil { return 0, fmt.Errorf("failed to write: %w", err) } defer mw.writeMu.unlock() + if mw.closed { + return 0, errors.New("cannot use closed writer") + } + defer func() { if err != nil { err = fmt.Errorf("failed to write: %w", err) - mw.writeMu.unlock() mw.c.close(err) } }() @@ -180,7 +179,7 @@ func (mw *msgWriter) Write(p []byte) (_ int, err error) { } func (mw *msgWriter) write(p []byte) (int, error) { - n, err := mw.c.writeFrame(true, mw.ctx, false, mw.flate, mw.opcode, p) + n, err := mw.c.writeFrame(mw.ctx, false, mw.flate, mw.opcode, p) if err != nil { return n, fmt.Errorf("failed to write data frame: %w", err) } @@ -210,7 +209,7 @@ func (mw *msgWriter) Close() (err error) { } } - _, err = mw.c.writeFrame(true, mw.ctx, true, mw.flate, mw.opcode, nil) + _, err = mw.c.writeFrame(mw.ctx, true, mw.flate, mw.opcode, nil) if err != nil { return fmt.Errorf("failed to write fin frame: %w", err) } @@ -236,7 +235,7 @@ func (c *Conn) writeControl(ctx context.Context, opcode opcode, p []byte) error ctx, cancel := context.WithTimeout(ctx, time.Second*5) defer cancel() - _, err := c.writeFrame(false, ctx, true, false, opcode, p) + _, err := c.writeFrame(ctx, true, false, opcode, p) if err != nil { return fmt.Errorf("failed to write control frame %v: %w", opcode, err) } @@ -244,7 +243,7 @@ func (c *Conn) writeControl(ctx context.Context, opcode opcode, p []byte) error } // frame handles all writes to the connection. -func (c *Conn) writeFrame(msgWriter bool, ctx context.Context, fin bool, flate bool, opcode opcode, p []byte) (_ int, err error) { +func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opcode, p []byte) (_ int, err error) { err = c.writeFrameMu.lock(ctx) if err != nil { return 0, err @@ -284,10 +283,6 @@ func (c *Conn) writeFrame(msgWriter bool, ctx context.Context, fin bool, flate b err = ctx.Err() default: } - c.writeFrameMu.unlock() - if msgWriter { - c.msgWriter.writeMu.unlock() - } c.close(err) err = fmt.Errorf("failed to write frame: %w", err) } From 7b1a6bbaa14e56050770eee1161138ce58e5f39e Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 03:15:26 -0700 Subject: [PATCH 430/519] README.md formatting fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8850f511..ec5d2704 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ See GitHub issues for minor issues but the major future enhancements are: - [ ] Ping pong instrumentation callbacks [#246](https://github.com/nhooyr/websocket/issues/246) - [ ] Graceful shutdown helpers [#209](https://github.com/nhooyr/websocket/issues/209) - [ ] Assembly for WebSocket masking [#16](https://github.com/nhooyr/websocket/issues/16) - - WIP at [#326](https://github.com/nhooyr/websocket/pull/326), about 3x faster + - WIP at [#326](https://github.com/nhooyr/websocket/pull/326), about 3x faster - [ ] HTTP/2 [#4](https://github.com/nhooyr/websocket/issues/4) - [ ] The holy grail [#402](https://github.com/nhooyr/websocket/issues/402) From d91a2124e071bbf025abedb1cdb608a94e81985c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 03:19:53 -0700 Subject: [PATCH 431/519] wsjs: Ensure no goroutines leak after Close Closes #330 --- close.go | 4 ++-- conn.go | 33 ++++++++++++++------------------- ws_js.go | 12 ++++++++++-- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/close.go b/close.go index 1053751c..c3dee7e0 100644 --- a/close.go +++ b/close.go @@ -99,14 +99,14 @@ func CloseStatus(err error) StatusCode { // Close will unblock all goroutines interacting with the connection once // complete. func (c *Conn) Close(code StatusCode, reason string) error { - defer c.wgWait() + defer c.wg.Wait() return c.closeHandshake(code, reason) } // CloseNow closes the WebSocket connection without attempting a close handshake. // Use when you do not want the overhead of the close handshake. func (c *Conn) CloseNow() (err error) { - defer c.wgWait() + defer c.wg.Wait() defer errd.Wrap(&err, "failed to close WebSocket") if c.isClosed() { diff --git a/conn.go b/conn.go index 05531c3b..e133cd67 100644 --- a/conn.go +++ b/conn.go @@ -45,8 +45,6 @@ const ( type Conn struct { noCopy - wg sync.WaitGroup - subprotocol string rwc io.ReadWriteCloser client bool @@ -72,6 +70,7 @@ type Conn struct { writeHeaderBuf [8]byte writeHeader header + wg sync.WaitGroup closed chan struct{} closeMu sync.Mutex closeErr error @@ -132,7 +131,11 @@ func newConn(cfg connConfig) *Conn { c.close(errors.New("connection garbage collected")) }) - c.wgGo(c.timeoutLoop) + c.wg.Add(1) + go func() { + defer c.wg.Done() + c.timeoutLoop() + }() return c } @@ -163,10 +166,12 @@ func (c *Conn) close(err error) { // closeErr. c.rwc.Close() - c.wgGo(func() { + c.wg.Add(1) + go func() { + defer c.wg.Done() c.msgWriter.close() c.msgReader.close() - }) + }() } func (c *Conn) timeoutLoop() { @@ -183,9 +188,11 @@ func (c *Conn) timeoutLoop() { case <-readCtx.Done(): c.setCloseErr(fmt.Errorf("read timed out: %w", readCtx.Err())) - c.wgGo(func() { + c.wg.Add(1) + go func() { + defer c.wg.Done() c.writeError(StatusPolicyViolation, errors.New("read timed out")) - }) + }() case <-writeCtx.Done(): c.close(fmt.Errorf("write timed out: %w", writeCtx.Err())) return @@ -302,15 +309,3 @@ func (m *mu) unlock() { type noCopy struct{} func (*noCopy) Lock() {} - -func (c *Conn) wgGo(fn func()) { - c.wg.Add(1) - go func() { - defer c.wg.Done() - fn() - }() -} - -func (c *Conn) wgWait() { - c.wg.Wait() -} diff --git a/ws_js.go b/ws_js.go index 180d0564..b4011b5c 100644 --- a/ws_js.go +++ b/ws_js.go @@ -47,6 +47,7 @@ type Conn struct { // read limit for a message in bytes. msgReadLimit xsync.Int64 + wg sync.WaitGroup closingMu sync.Mutex isReadClosed xsync.Int64 closeOnce sync.Once @@ -223,6 +224,7 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) error { // or the connection is closed. // It thus performs the full WebSocket close handshake. func (c *Conn) Close(code StatusCode, reason string) error { + defer c.wg.Wait() err := c.exportedClose(code, reason) if err != nil { return fmt.Errorf("failed to close WebSocket: %w", err) @@ -236,6 +238,7 @@ func (c *Conn) Close(code StatusCode, reason string) error { // note: No different from Close(StatusGoingAway, "") in WASM as there is no way to close // a WebSocket without the close handshake. func (c *Conn) CloseNow() error { + defer c.wg.Wait() return c.Close(StatusGoingAway, "") } @@ -388,10 +391,15 @@ func (c *Conn) CloseRead(ctx context.Context) context.Context { c.isReadClosed.Store(1) ctx, cancel := context.WithCancel(ctx) + c.wg.Add(1) go func() { + defer c.CloseNow() + defer c.wg.Done() defer cancel() - c.read(ctx) - c.Close(StatusPolicyViolation, "unexpected data message") + _, _, err := c.read(ctx) + if err != nil { + c.Close(StatusPolicyViolation, "unexpected data message") + } }() return ctx } From 0caa99775940b191e81610fbb73acb5447401da9 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 03:26:56 -0700 Subject: [PATCH 432/519] Another README.md update --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ec5d2704..3bf51e56 100644 --- a/README.md +++ b/README.md @@ -140,4 +140,15 @@ to nhooyr.io/websocket. [gobwas/ws](https://github.com/gobwas/ws) has an extremely flexible API that allows it to be used in an event driven style for performance. See the author's [blog post](https://medium.freecodecamp.org/million-websockets-and-go-cc58418460bb). -However when writing idiomatic Go, nhooyr.io/websocket will be faster and easier to use. +However it is quite bloated. See https://pkg.go.dev/github.com/gobwas/ws + +When writing idiomatic Go, nhooyr.io/websocket will be faster and easier to use. + +#### lesismal/nbio + +[lesismal/nbio](https://github.com/lesismal/nbio) is similar to gobwas/ws in that the API is +event driven for performance reasons. + +However it is quite bloated. See https://pkg.go.dev/github.com/lesismal/nbio + +When writing idiomatic Go, nhooyr.io/websocket will be faster and easier to use. From 7d8ddbc72c3a58f29e211fa2b490fa1d38b3d666 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 05:07:07 -0700 Subject: [PATCH 433/519] Fix in README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 3bf51e56..7fa3177b 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,6 @@ Advantages of nhooyr.io/websocket: - Gorilla writes directly to a net.Conn and so duplicates features of net/http.Client. - Concurrent writes - Close handshake ([gorilla/websocket#448](https://github.com/gorilla/websocket/issues/448)) -- [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper for write only connections - Idiomatic [ping pong](https://pkg.go.dev/nhooyr.io/websocket#Conn.Ping) API - Gorilla requires registering a pong callback before sending a Ping - Can target Wasm ([gorilla/websocket#432](https://github.com/gorilla/websocket/issues/432)) @@ -125,7 +124,7 @@ Advantages of nhooyr.io/websocket: Soon we'll have assembly and be 4.5x faster [#326](https://github.com/nhooyr/websocket/pull/326) - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support - Gorilla only supports no context takeover mode -- [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492)) +- [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper for write only connections ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492)) #### golang.org/x/net/websocket From 535fd2c0516e074fbd5f8340eb3e0d345975bb24 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 06:46:50 -0700 Subject: [PATCH 434/519] go.sum: Delete No longer needed :) --- go.sum | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 go.sum diff --git a/go.sum b/go.sum deleted file mode 100644 index e69de29b..00000000 From 63c0405b4e4735ab744a8b1bf5bce15e4ff99689 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 07:32:34 -0700 Subject: [PATCH 435/519] ci/fmt.sh: Tidy internal module dependencies --- ci/fmt.sh | 2 ++ internal/examples/go.mod | 5 ++--- internal/examples/go.sum | 39 -------------------------------------- internal/thirdparty/go.mod | 2 +- internal/thirdparty/go.sum | 1 - 5 files changed, 5 insertions(+), 44 deletions(-) diff --git a/ci/fmt.sh b/ci/fmt.sh index 0d902732..6e5a68e4 100755 --- a/ci/fmt.sh +++ b/ci/fmt.sh @@ -3,6 +3,8 @@ set -eu cd -- "$(dirname "$0")/.." go mod tidy +(cd ./internal/thirdparty && go mod tidy) +(cd ./internal/examples && go mod tidy) gofmt -w -s . go run golang.org/x/tools/cmd/goimports@latest -w "-local=$(go list -m)" . diff --git a/internal/examples/go.mod b/internal/examples/go.mod index b5cdcc1d..c98b81ce 100644 --- a/internal/examples/go.mod +++ b/internal/examples/go.mod @@ -5,7 +5,6 @@ go 1.19 replace nhooyr.io/websocket => ../.. require ( - github.com/klauspost/compress v1.10.3 // indirect - golang.org/x/time v0.3.0 // indirect - nhooyr.io/websocket v1.8.7 // indirect + golang.org/x/time v0.3.0 + nhooyr.io/websocket v0.0.0-00010101000000-000000000000 ) diff --git a/internal/examples/go.sum b/internal/examples/go.sum index 03aa32c2..f8a07e82 100644 --- a/internal/examples/go.sum +++ b/internal/examples/go.sum @@ -1,41 +1,2 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= -github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= -github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= -nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= diff --git a/internal/thirdparty/go.mod b/internal/thirdparty/go.mod index e8c3e2c0..10eb45c1 100644 --- a/internal/thirdparty/go.mod +++ b/internal/thirdparty/go.mod @@ -8,7 +8,7 @@ require ( github.com/gin-gonic/gin v1.9.1 github.com/gobwas/ws v1.3.0 github.com/gorilla/websocket v1.5.0 - nhooyr.io/websocket v1.8.7 + nhooyr.io/websocket v0.0.0-00010101000000-000000000000 ) require ( diff --git a/internal/thirdparty/go.sum b/internal/thirdparty/go.sum index 80e4ad52..a9424b8d 100644 --- a/internal/thirdparty/go.sum +++ b/internal/thirdparty/go.sum @@ -14,7 +14,6 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= From 7ada24994a18ed4a3e59ca206ea9783f422e3718 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 07:47:45 -0700 Subject: [PATCH 436/519] Fix typo in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7fa3177b..1c5751d8 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ Advantages of nhooyr.io/websocket: - Transparent message buffer reuse with [wsjson](https://pkg.go.dev/nhooyr.io/websocket/wsjson) subpackage - [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster WebSocket masking implementation in pure Go - Gorilla's implementation is slower and uses [unsafe](https://golang.org/pkg/unsafe/). - Soon we'll have assembly and be 4.5x faster [#326](https://github.com/nhooyr/websocket/pull/326) + Soon we'll have assembly and be 3x faster [#326](https://github.com/nhooyr/websocket/pull/326) - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support - Gorilla only supports no context takeover mode - [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper for write only connections ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492)) From ff3ea39ba06d07d4980b64c0008d7d178e0d9411 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 14:39:43 -0700 Subject: [PATCH 437/519] ci/lint.sh: Remove golint Underscores in symbols are ok sometimes... --- ci/lint.sh | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ci/lint.sh b/ci/lint.sh index 80f309be..3cf8eee4 100755 --- a/ci/lint.sh +++ b/ci/lint.sh @@ -5,10 +5,6 @@ cd -- "$(dirname "$0")/.." go vet ./... GOOS=js GOARCH=wasm go vet ./... -go install golang.org/x/lint/golint@latest -golint -set_exit_status ./... -GOOS=js GOARCH=wasm golint -set_exit_status ./... - go install honnef.co/go/tools/cmd/staticcheck@latest staticcheck ./... GOOS=js GOARCH=wasm staticcheck ./... From af0fd9d45e6e56b045f8e8556aa8fe917cbc6259 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 15:19:48 -0700 Subject: [PATCH 438/519] examples/chat: Fix race condition Tricky tricky. --- internal/examples/chat/chat.go | 39 +++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/internal/examples/chat/chat.go b/internal/examples/chat/chat.go index 78a5696a..8b1e30c1 100644 --- a/internal/examples/chat/chat.go +++ b/internal/examples/chat/chat.go @@ -5,6 +5,7 @@ import ( "errors" "io" "log" + "net" "net/http" "sync" "time" @@ -69,14 +70,7 @@ func (cs *chatServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { // subscribeHandler accepts the WebSocket connection and then subscribes // it to all future messages. func (cs *chatServer) subscribeHandler(w http.ResponseWriter, r *http.Request) { - c, err := websocket.Accept(w, r, nil) - if err != nil { - cs.logf("%v", err) - return - } - defer c.CloseNow() - - err = cs.subscribe(r.Context(), c) + err := cs.subscribe(r.Context(), w, r) if errors.Is(err, context.Canceled) { return } @@ -117,18 +111,39 @@ func (cs *chatServer) publishHandler(w http.ResponseWriter, r *http.Request) { // // It uses CloseRead to keep reading from the connection to process control // messages and cancel the context if the connection drops. -func (cs *chatServer) subscribe(ctx context.Context, c *websocket.Conn) error { - ctx = c.CloseRead(ctx) - +func (cs *chatServer) subscribe(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + var mu sync.Mutex + var c *websocket.Conn + var closed bool s := &subscriber{ msgs: make(chan []byte, cs.subscriberMessageBuffer), closeSlow: func() { - c.Close(websocket.StatusPolicyViolation, "connection too slow to keep up with messages") + mu.Lock() + defer mu.Unlock() + closed = true + if c != nil { + c.Close(websocket.StatusPolicyViolation, "connection too slow to keep up with messages") + } }, } cs.addSubscriber(s) defer cs.deleteSubscriber(s) + c2, err := websocket.Accept(w, r, nil) + if err != nil { + return err + } + mu.Lock() + if closed { + mu.Unlock() + return net.ErrClosed + } + c = c2 + mu.Unlock() + defer c.CloseNow() + + ctx = c.CloseRead(ctx) + for { select { case msg := <-s.msgs: From 84d7ddcc862f618e5eb1cb0beb9599a30001eb4d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 20 Oct 2023 09:04:40 -0700 Subject: [PATCH 439/519] README.md: Update badges --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1c5751d8..85d2eb31 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # websocket -[![godoc](https://godoc.org/nhooyr.io/websocket?status.svg)](https://pkg.go.dev/nhooyr.io/websocket) -[![coverage](https://img.shields.io/badge/coverage-91%25-success)](https://nhooyr.io/websocket/coverage.html) +[![Go Reference](https://pkg.go.dev/badge/nhooyr.io/websocket.svg)](https://pkg.go.dev/nhooyr.io/websocket) +[![Go Coverage](https://img.shields.io/badge/coverage-91%25-success)](https://nhooyr.io/websocket/coverage.html) websocket is a minimal and idiomatic WebSocket library for Go. From b4e4f4f12ee47d9827cee352c52b612944bad6bf Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 20 Oct 2023 09:13:59 -0700 Subject: [PATCH 440/519] Don't embed noCopy... Whoops. --- conn.go | 2 +- ws_js.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/conn.go b/conn.go index e133cd67..ef4d62ad 100644 --- a/conn.go +++ b/conn.go @@ -43,7 +43,7 @@ const ( // This applies to context expirations as well unfortunately. // See https://github.com/nhooyr/websocket/issues/242#issuecomment-633182220 type Conn struct { - noCopy + noCopy noCopy subprotocol string rwc io.ReadWriteCloser diff --git a/ws_js.go b/ws_js.go index b4011b5c..cf119da7 100644 --- a/ws_js.go +++ b/ws_js.go @@ -41,7 +41,7 @@ const ( // Conn provides a wrapper around the browser WebSocket API. type Conn struct { - noCopy + noCopy noCopy ws wsjs.WebSocket // read limit for a message in bytes. From 454aee86997aeb75f06c6cecbc15c3355b5e8d30 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 25 Oct 2023 05:39:27 -0700 Subject: [PATCH 441/519] ws_js.go: Disable read limit on -1 Whoops, updates #254 and closes #410 --- ws_js.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ws_js.go b/ws_js.go index cf119da7..77d0d80f 100644 --- a/ws_js.go +++ b/ws_js.go @@ -42,7 +42,7 @@ const ( // Conn provides a wrapper around the browser WebSocket API. type Conn struct { noCopy noCopy - ws wsjs.WebSocket + ws wsjs.WebSocket // read limit for a message in bytes. msgReadLimit xsync.Int64 @@ -138,7 +138,8 @@ func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { if err != nil { return 0, nil, fmt.Errorf("failed to read: %w", err) } - if int64(len(p)) > c.msgReadLimit.Load() { + readLimit := c.msgReadLimit.Load() + if readLimit >= 0 && int64(len(p)) > readLimit { err := fmt.Errorf("read limited at %v bytes", c.msgReadLimit.Load()) c.Close(StatusMessageTooBig, err.Error()) return 0, nil, err From 8060f3a3b51679f8d2f48e04113ad596612bca50 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 25 Oct 2023 06:05:22 -0700 Subject: [PATCH 442/519] README.md: Mention gorilla advantage re no extra context cancellation goroutine Not sure how/when this was lost but an important disadvantage to note. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 85d2eb31..d093746d 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,8 @@ Advantages of [gorilla/websocket](https://github.com/gorilla/websocket): - Mature and widely used - [Prepared writes](https://pkg.go.dev/github.com/gorilla/websocket#PreparedMessage) - Configurable [buffer sizes](https://pkg.go.dev/github.com/gorilla/websocket#hdr-Buffers) +- No extra goroutine per connection to support cancellation with context.Context. This costs nhooyr.io/websocket 2 KB of memory per connection. + - Will be removed soon with [context.AfterFunc](https://github.com/golang/go/issues/57928). See [#411](https://github.com/nhooyr/websocket/issues/411) Advantages of nhooyr.io/websocket: From 52721a9fc36a5c1cadecc124dd0a08184e929681 Mon Sep 17 00:00:00 2001 From: Kunal Singh Date: Thu, 26 Oct 2023 00:03:11 +0530 Subject: [PATCH 443/519] Use ws:// over http:// in example logs --- internal/examples/chat/README.md | 2 +- internal/examples/chat/main.go | 2 +- internal/examples/echo/README.md | 2 +- internal/examples/echo/main.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/examples/chat/README.md b/internal/examples/chat/README.md index ca1024a0..574c6994 100644 --- a/internal/examples/chat/README.md +++ b/internal/examples/chat/README.md @@ -5,7 +5,7 @@ This directory contains a full stack example of a simple chat webapp using nhooy ```bash $ cd examples/chat $ go run . localhost:0 -listening on http://127.0.0.1:51055 +listening on ws://127.0.0.1:51055 ``` Visit the printed URL to submit and view broadcasted messages in a browser. diff --git a/internal/examples/chat/main.go b/internal/examples/chat/main.go index 3fcec6be..e3432984 100644 --- a/internal/examples/chat/main.go +++ b/internal/examples/chat/main.go @@ -31,7 +31,7 @@ func run() error { if err != nil { return err } - log.Printf("listening on http://%v", l.Addr()) + log.Printf("listening on ws://%v", l.Addr()) cs := newChatServer() s := &http.Server{ diff --git a/internal/examples/echo/README.md b/internal/examples/echo/README.md index 7f42c3c5..ac03f640 100644 --- a/internal/examples/echo/README.md +++ b/internal/examples/echo/README.md @@ -5,7 +5,7 @@ This directory contains a echo server example using nhooyr.io/websocket. ```bash $ cd examples/echo $ go run . localhost:0 -listening on http://127.0.0.1:51055 +listening on ws://127.0.0.1:51055 ``` You can use a WebSocket client like https://github.com/hashrocket/ws to connect. All messages diff --git a/internal/examples/echo/main.go b/internal/examples/echo/main.go index 16d78a79..47e30d05 100644 --- a/internal/examples/echo/main.go +++ b/internal/examples/echo/main.go @@ -31,7 +31,7 @@ func run() error { if err != nil { return err } - log.Printf("listening on http://%v", l.Addr()) + log.Printf("listening on ws://%v", l.Addr()) s := &http.Server{ Handler: echoServer{ From 5df0303d0a24d67de232a55f55ff3cbf9f8fc6ac Mon Sep 17 00:00:00 2001 From: wdvxdr Date: Mon, 24 Jan 2022 19:25:11 +0800 Subject: [PATCH 444/519] mask.go: Use SIMD masking for amd64 and arm64 goos: windows goarch: amd64 pkg: nhooyr.io/websocket cpu: Intel(R) Core(TM) i5-9300H CPU @ 2.40GHz Benchmark_mask/2/basic-8 425339004 2.795 ns/op 715.66 MB/s Benchmark_mask/2/nhooyr-8 379937766 3.186 ns/op 627.78 MB/s Benchmark_mask/2/gorilla-8 392164167 3.071 ns/op 651.24 MB/s Benchmark_mask/2/gobwas-8 310037222 3.880 ns/op 515.46 MB/s Benchmark_mask/3/basic-8 321408024 3.806 ns/op 788.32 MB/s Benchmark_mask/3/nhooyr-8 350726338 3.478 ns/op 862.58 MB/s Benchmark_mask/3/gorilla-8 332217727 3.634 ns/op 825.43 MB/s Benchmark_mask/3/gobwas-8 247376214 4.886 ns/op 614.01 MB/s Benchmark_mask/4/basic-8 261182472 4.582 ns/op 872.91 MB/s Benchmark_mask/4/nhooyr-8 381830712 3.262 ns/op 1226.05 MB/s Benchmark_mask/4/gorilla-8 272616304 4.395 ns/op 910.04 MB/s Benchmark_mask/4/gobwas-8 204574558 5.855 ns/op 683.19 MB/s Benchmark_mask/8/basic-8 191330037 6.162 ns/op 1298.24 MB/s Benchmark_mask/8/nhooyr-8 369694992 3.285 ns/op 2435.65 MB/s Benchmark_mask/8/gorilla-8 175388466 6.743 ns/op 1186.48 MB/s Benchmark_mask/8/gobwas-8 241719933 4.886 ns/op 1637.45 MB/s Benchmark_mask/16/basic-8 100000000 10.92 ns/op 1464.83 MB/s Benchmark_mask/16/nhooyr-8 272565096 4.436 ns/op 3606.98 MB/s Benchmark_mask/16/gorilla-8 100000000 11.20 ns/op 1428.53 MB/s Benchmark_mask/16/gobwas-8 221356798 5.405 ns/op 2960.45 MB/s Benchmark_mask/32/basic-8 61476984 20.40 ns/op 1568.80 MB/s Benchmark_mask/32/nhooyr-8 238665572 5.050 ns/op 6337.22 MB/s Benchmark_mask/32/gorilla-8 100000000 12.09 ns/op 2647.28 MB/s Benchmark_mask/32/gobwas-8 186077235 6.477 ns/op 4940.36 MB/s Benchmark_mask/128/basic-8 14629720 80.90 ns/op 1582.19 MB/s Benchmark_mask/128/nhooyr-8 181241968 6.565 ns/op 19497.98 MB/s Benchmark_mask/128/gorilla-8 68308342 16.76 ns/op 7639.37 MB/s Benchmark_mask/128/gobwas-8 94582026 12.97 ns/op 9872.11 MB/s Benchmark_mask/512/basic-8 3921001 305.6 ns/op 1675.55 MB/s Benchmark_mask/512/nhooyr-8 123102199 9.721 ns/op 52669.11 MB/s Benchmark_mask/512/gorilla-8 32355914 38.18 ns/op 13411.43 MB/s Benchmark_mask/512/gobwas-8 31528501 37.80 ns/op 13544.37 MB/s Benchmark_mask/4096/basic-8 491804 2381 ns/op 1720.39 MB/s Benchmark_mask/4096/nhooyr-8 26159691 46.98 ns/op 87187.73 MB/s Benchmark_mask/4096/gorilla-8 4898440 243.6 ns/op 16817.89 MB/s Benchmark_mask/4096/gobwas-8 4336398 277.2 ns/op 14776.40 MB/s Benchmark_mask/16384/basic-8 113842 9623 ns/op 1702.66 MB/s Benchmark_mask/16384/nhooyr-8 8088847 154.5 ns/op 106058.18 MB/s Benchmark_mask/16384/gorilla-8 1282993 933.6 ns/op 17549.90 MB/s Benchmark_mask/16384/gobwas-8 997347 1086 ns/op 15093.49 MB/s We're about 4-5x faster then gorilla now. --- frame.go | 2 +- go.mod | 2 + go.sum | 2 + mask_amd64.s | 152 ++++++++++++++++++++++++++++++++++++++++++++++++ mask_arm64.s | 74 +++++++++++++++++++++++ mask_asm.go | 19 ++++++ mask_generic.go | 7 +++ 7 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 go.sum create mode 100644 mask_amd64.s create mode 100644 mask_arm64.s create mode 100644 mask_asm.go create mode 100644 mask_generic.go diff --git a/frame.go b/frame.go index 351632fd..eec15bb9 100644 --- a/frame.go +++ b/frame.go @@ -184,7 +184,7 @@ func writeFrameHeader(h header, w *bufio.Writer, buf []byte) (err error) { // to be in little endian. // // See https://github.com/golang/go/issues/31586 -func mask(key uint32, b []byte) uint32 { +func maskGo(key uint32, b []byte) uint32 { if len(b) >= 8 { key64 := uint64(key)<<32 | uint64(key) diff --git a/go.mod b/go.mod index 715a9f7a..285b955f 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module nhooyr.io/websocket go 1.19 + +require golang.org/x/sys v0.13.0 diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..d4673ecf --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/mask_amd64.s b/mask_amd64.s new file mode 100644 index 00000000..caca53ec --- /dev/null +++ b/mask_amd64.s @@ -0,0 +1,152 @@ +#include "textflag.h" + +// func maskAsm(b *byte, len int, key uint32) +TEXT ·maskAsm(SB), NOSPLIT, $0-28 + // AX = b + // CX = len (left length) + // SI = key (uint32) + // DI = uint64(SI) | uint64(SI)<<32 + MOVQ b+0(FP), AX + MOVQ len+8(FP), CX + MOVL key+16(FP), SI + + // calculate the DI + // DI = SI<<32 | SI + MOVL SI, DI + MOVQ DI, DX + SHLQ $32, DI + ORQ DX, DI + + CMPQ CX, $15 + JLE less_than_16 + CMPQ CX, $63 + JLE less_than_64 + CMPQ CX, $128 + JLE sse + TESTQ $31, AX + JNZ unaligned + +aligned: + CMPB ·useAVX2(SB), $1 + JE avx2 + JMP sse + +unaligned_loop_1byte: + XORB SI, (AX) + INCQ AX + DECQ CX + ROLL $24, SI + TESTQ $7, AX + JNZ unaligned_loop_1byte + + // calculate DI again since SI was modified + // DI = SI<<32 | SI + MOVL SI, DI + MOVQ DI, DX + SHLQ $32, DI + ORQ DX, DI + + TESTQ $31, AX + JZ aligned + +unaligned: + TESTQ $7, AX // AND $7 & len, if not zero jump to loop_1b. + JNZ unaligned_loop_1byte + +unaligned_loop: + // we don't need to check the CX since we know it's above 128 + XORQ DI, (AX) + ADDQ $8, AX + SUBQ $8, CX + TESTQ $31, AX + JNZ unaligned_loop + JMP aligned + +avx2: + CMPQ CX, $0x80 + JL sse + VMOVQ DI, X0 + VPBROADCASTQ X0, Y0 + +avx2_loop: + VPXOR (AX), Y0, Y1 + VPXOR 32(AX), Y0, Y2 + VPXOR 64(AX), Y0, Y3 + VPXOR 96(AX), Y0, Y4 + VMOVDQU Y1, (AX) + VMOVDQU Y2, 32(AX) + VMOVDQU Y3, 64(AX) + VMOVDQU Y4, 96(AX) + ADDQ $0x80, AX + SUBQ $0x80, CX + CMPQ CX, $0x80 + JAE avx2_loop // loop if CX >= 0x80 + +sse: + CMPQ CX, $0x40 + JL less_than_64 + MOVQ DI, X0 + PUNPCKLQDQ X0, X0 + +sse_loop: + MOVOU 0*16(AX), X1 + MOVOU 1*16(AX), X2 + MOVOU 2*16(AX), X3 + MOVOU 3*16(AX), X4 + PXOR X0, X1 + PXOR X0, X2 + PXOR X0, X3 + PXOR X0, X4 + MOVOU X1, 0*16(AX) + MOVOU X2, 1*16(AX) + MOVOU X3, 2*16(AX) + MOVOU X4, 3*16(AX) + ADDQ $0x40, AX + SUBQ $0x40, CX + CMPQ CX, $0x40 + JAE sse_loop + +less_than_64: + TESTQ $32, CX + JZ less_than_32 + XORQ DI, (AX) + XORQ DI, 8(AX) + XORQ DI, 16(AX) + XORQ DI, 24(AX) + ADDQ $32, AX + +less_than_32: + TESTQ $16, CX + JZ less_than_16 + XORQ DI, (AX) + XORQ DI, 8(AX) + ADDQ $16, AX + +less_than_16: + TESTQ $8, CX + JZ less_than_8 + XORQ DI, (AX) + ADDQ $8, AX + +less_than_8: + TESTQ $4, CX + JZ less_than_4 + XORL SI, (AX) + ADDQ $4, AX + +less_than_4: + TESTQ $2, CX + JZ less_than_2 + XORW SI, (AX) + ROLL $16, SI + ADDQ $2, AX + +less_than_2: + TESTQ $1, CX + JZ done + XORB SI, (AX) + ROLL $24, SI + +done: + MOVL SI, ret+24(FP) + RET diff --git a/mask_arm64.s b/mask_arm64.s new file mode 100644 index 00000000..624cb720 --- /dev/null +++ b/mask_arm64.s @@ -0,0 +1,74 @@ +#include "textflag.h" + +// func maskAsm(b *byte,len, int, key uint32) +TEXT ·maskAsm(SB), NOSPLIT, $0-28 + // R0 = b + // R1 = len + // R2 = uint64(key)<<32 | uint64(key) + // R3 = key (uint32) + MOVD b_ptr+0(FP), R0 + MOVD b_len+8(FP), R1 + MOVWU key+16(FP), R3 + MOVD R3, R2 + ORR R2<<32, R2, R2 + VDUP R2, V0.D2 + CMP $64, R1 + BLT less_than_64 + + // todo: optimize unaligned case +loop_64: + VLD1 (R0), [V1.B16, V2.B16, V3.B16, V4.B16] + VEOR V1.B16, V0.B16, V1.B16 + VEOR V2.B16, V0.B16, V2.B16 + VEOR V3.B16, V0.B16, V3.B16 + VEOR V4.B16, V0.B16, V4.B16 + VST1.P [V1.B16, V2.B16, V3.B16, V4.B16], 64(R0) + SUBS $64, R1 + CMP $64, R1 + BGE loop_64 + +less_than_64: + // quick end + CBZ R1, end + TBZ $5, R1, less_than32 + VLD1 (R0), [V1.B16, V2.B16] + VEOR V1.B16, V0.B16, V1.B16 + VEOR V2.B16, V0.B16, V2.B16 + VST1.P [V1.B16, V2.B16], 32(R0) + +less_than32: + TBZ $4, R1, less_than16 + LDP (R0), (R11, R12) + EOR R11, R2, R11 + EOR R12, R2, R12 + STP.P (R11, R12), 16(R0) + +less_than16: + TBZ $3, R1, less_than8 + MOVD (R0), R11 + EOR R2, R11, R11 + MOVD.P R11, 8(R0) + +less_than8: + TBZ $2, R1, less_than4 + MOVWU (R0), R11 + EORW R2, R11, R11 + MOVWU.P R11, 4(R0) + +less_than4: + TBZ $1, R1, less_than2 + MOVHU (R0), R11 + EORW R3, R11, R11 + MOVHU.P R11, 2(R0) + RORW $16, R3 + +less_than2: + TBZ $0, R1, end + MOVBU (R0), R11 + EORW R3, R11, R11 + MOVBU.P R11, 1(R0) + RORW $8, R3 + +end: + MOVWU R3, ret+24(FP) + RET diff --git a/mask_asm.go b/mask_asm.go new file mode 100644 index 00000000..a18a20e5 --- /dev/null +++ b/mask_asm.go @@ -0,0 +1,19 @@ +//go:build !appengine && (amd64 || arm64) +// +build !appengine +// +build amd64 arm64 + +package websocket + +import "golang.org/x/sys/cpu" + +func mask(key uint32, b []byte) uint32 { + if len(b) > 0 { + return maskAsm(&b[0], len(b), key) + } + return key +} + +var useAVX2 = cpu.X86.HasAVX2 + +//go:noescape +func maskAsm(b *byte, len int, key uint32) uint32 diff --git a/mask_generic.go b/mask_generic.go new file mode 100644 index 00000000..6331b746 --- /dev/null +++ b/mask_generic.go @@ -0,0 +1,7 @@ +//go:build appengine || (!amd64 && !arm64 && !js) + +package websocket + +func mask(key uint32, b []byte) uint32 { + return maskGo(key, b) +} From cda2170e9b48e7a33e4b5401eb6db31cd61314d4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 07:41:22 -0700 Subject: [PATCH 445/519] Refactor and compile masking code again --- frame.go | 2 +- internal/examples/go.mod | 2 ++ internal/examples/go.sum | 2 ++ internal/thirdparty/go.mod | 2 +- internal/thirdparty/go.sum | 4 ++-- mask_generic.go => mask.go | 2 +- mask_asm.go | 6 ++---- 7 files changed, 11 insertions(+), 9 deletions(-) rename mask_generic.go => mask.go (63%) diff --git a/frame.go b/frame.go index eec15bb9..362a99e9 100644 --- a/frame.go +++ b/frame.go @@ -173,7 +173,7 @@ func writeFrameHeader(h header, w *bufio.Writer, buf []byte) (err error) { return nil } -// mask applies the WebSocket masking algorithm to p +// maskGo applies the WebSocket masking algorithm to p // with the given key. // See https://tools.ietf.org/html/rfc6455#section-5.3 // diff --git a/internal/examples/go.mod b/internal/examples/go.mod index c98b81ce..dfdb8cca 100644 --- a/internal/examples/go.mod +++ b/internal/examples/go.mod @@ -8,3 +8,5 @@ require ( golang.org/x/time v0.3.0 nhooyr.io/websocket v0.0.0-00010101000000-000000000000 ) + +require golang.org/x/sys v0.13.0 // indirect diff --git a/internal/examples/go.sum b/internal/examples/go.sum index f8a07e82..1931a8f2 100644 --- a/internal/examples/go.sum +++ b/internal/examples/go.sum @@ -1,2 +1,4 @@ +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/internal/thirdparty/go.mod b/internal/thirdparty/go.mod index 10eb45c1..3f32a416 100644 --- a/internal/thirdparty/go.mod +++ b/internal/thirdparty/go.mod @@ -34,7 +34,7 @@ require ( golang.org/x/arch v0.3.0 // indirect golang.org/x/crypto v0.9.0 // indirect golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.8.0 // indirect + golang.org/x/sys v0.13.0 // indirect golang.org/x/text v0.9.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/internal/thirdparty/go.sum b/internal/thirdparty/go.sum index a9424b8d..47f324bb 100644 --- a/internal/thirdparty/go.sum +++ b/internal/thirdparty/go.sum @@ -76,8 +76,8 @@ golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= diff --git a/mask_generic.go b/mask.go similarity index 63% rename from mask_generic.go rename to mask.go index 6331b746..7c9fe308 100644 --- a/mask_generic.go +++ b/mask.go @@ -1,4 +1,4 @@ -//go:build appengine || (!amd64 && !arm64 && !js) +//go:build !amd64 && !arm64 && !js package websocket diff --git a/mask_asm.go b/mask_asm.go index a18a20e5..9b370690 100644 --- a/mask_asm.go +++ b/mask_asm.go @@ -1,6 +1,4 @@ -//go:build !appengine && (amd64 || arm64) -// +build !appengine -// +build amd64 arm64 +//go:build amd64 || arm64 package websocket @@ -13,7 +11,7 @@ func mask(key uint32, b []byte) uint32 { return key } -var useAVX2 = cpu.X86.HasAVX2 +var useAVX2 = cpu.X86.HasAVX2 //lint:ignore U1000 mask_amd64.s //go:noescape func maskAsm(b *byte, len int, key uint32) uint32 From f5397ae3d1bfdf120ce8a598a0b0b0fe2b86f784 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 07:48:47 -0700 Subject: [PATCH 446/519] mask_asm.go: Disable AVX2 Slower for some reason than just SIMD. Also no dependency on cpu package is nice. --- go.mod | 2 -- go.sum | 2 -- internal/examples/go.mod | 2 -- internal/examples/go.sum | 2 -- mask_asm.go | 4 +--- 5 files changed, 1 insertion(+), 11 deletions(-) diff --git a/go.mod b/go.mod index 285b955f..715a9f7a 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,3 @@ module nhooyr.io/websocket go 1.19 - -require golang.org/x/sys v0.13.0 diff --git a/go.sum b/go.sum index d4673ecf..e69de29b 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +0,0 @@ -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/examples/go.mod b/internal/examples/go.mod index dfdb8cca..c98b81ce 100644 --- a/internal/examples/go.mod +++ b/internal/examples/go.mod @@ -8,5 +8,3 @@ require ( golang.org/x/time v0.3.0 nhooyr.io/websocket v0.0.0-00010101000000-000000000000 ) - -require golang.org/x/sys v0.13.0 // indirect diff --git a/internal/examples/go.sum b/internal/examples/go.sum index 1931a8f2..f8a07e82 100644 --- a/internal/examples/go.sum +++ b/internal/examples/go.sum @@ -1,4 +1,2 @@ -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/mask_asm.go b/mask_asm.go index 9b370690..946bc0f6 100644 --- a/mask_asm.go +++ b/mask_asm.go @@ -2,8 +2,6 @@ package websocket -import "golang.org/x/sys/cpu" - func mask(key uint32, b []byte) uint32 { if len(b) > 0 { return maskAsm(&b[0], len(b), key) @@ -11,7 +9,7 @@ func mask(key uint32, b []byte) uint32 { return key } -var useAVX2 = cpu.X86.HasAVX2 //lint:ignore U1000 mask_amd64.s +var useAVX2 = false //go:noescape func maskAsm(b *byte, len int, key uint32) uint32 From 14172e5b461e3b8376d79d2456800b7e100a044b Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 08:05:25 -0700 Subject: [PATCH 447/519] Benchmark pure go masking algorithm separately from assembly --- internal/thirdparty/frame_test.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/internal/thirdparty/frame_test.go b/internal/thirdparty/frame_test.go index 1a0ed125..dd0440db 100644 --- a/internal/thirdparty/frame_test.go +++ b/internal/thirdparty/frame_test.go @@ -26,6 +26,9 @@ func gorillaMaskBytes(key [4]byte, pos int, b []byte) int //go:linkname mask nhooyr.io/websocket.mask func mask(key32 uint32, b []byte) int +//go:linkname maskGo nhooyr.io/websocket.maskGo +func maskGo(key32 uint32, b []byte) int + func Benchmark_mask(b *testing.B) { sizes := []int{ 2, @@ -54,7 +57,18 @@ func Benchmark_mask(b *testing.B) { }, { - name: "nhooyr", + name: "nhooyr-go", + fn: func(b *testing.B, key [4]byte, p []byte) { + key32 := binary.LittleEndian.Uint32(key[:]) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + maskGo(key32, p) + } + }, + }, + { + name: "wdvxdr1123-asm", fn: func(b *testing.B, key [4]byte, p []byte) { key32 := binary.LittleEndian.Uint32(key[:]) b.ResetTimer() @@ -64,6 +78,7 @@ func Benchmark_mask(b *testing.B) { } }, }, + { name: "gorilla", fn: func(b *testing.B, key [4]byte, p []byte) { From 685a56e22e4ffe94d73c1da0d0e8746dcd36f165 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 08:08:37 -0700 Subject: [PATCH 448/519] Update README.md to indicate assembly websocket masking --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d093746d..4b2d828e 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ go get nhooyr.io/websocket - [RFC 7692](https://tools.ietf.org/html/rfc7692) permessage-deflate compression - [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper for write only connections - Compile to [Wasm](https://pkg.go.dev/nhooyr.io/websocket#hdr-Wasm) +- WebSocket masking implemented in assembly for amd64 and arm64 [#326](https://github.com/nhooyr/websocket/issues/326) ## Roadmap @@ -36,8 +37,6 @@ See GitHub issues for minor issues but the major future enhancements are: - [ ] Ping pong heartbeat helper [#267](https://github.com/nhooyr/websocket/issues/267) - [ ] Ping pong instrumentation callbacks [#246](https://github.com/nhooyr/websocket/issues/246) - [ ] Graceful shutdown helpers [#209](https://github.com/nhooyr/websocket/issues/209) -- [ ] Assembly for WebSocket masking [#16](https://github.com/nhooyr/websocket/issues/16) - - WIP at [#326](https://github.com/nhooyr/websocket/pull/326), about 3x faster - [ ] HTTP/2 [#4](https://github.com/nhooyr/websocket/issues/4) - [ ] The holy grail [#402](https://github.com/nhooyr/websocket/issues/402) @@ -121,7 +120,7 @@ Advantages of nhooyr.io/websocket: - Gorilla requires registering a pong callback before sending a Ping - Can target Wasm ([gorilla/websocket#432](https://github.com/gorilla/websocket/issues/432)) - Transparent message buffer reuse with [wsjson](https://pkg.go.dev/nhooyr.io/websocket/wsjson) subpackage -- [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster WebSocket masking implementation in pure Go +- [3x](https://github.com/nhooyr/websocket/pull/326) faster WebSocket masking implementation in assembly for amd64 and arm64 and [2x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster implementation in pure Go - Gorilla's implementation is slower and uses [unsafe](https://golang.org/pkg/unsafe/). Soon we'll have assembly and be 3x faster [#326](https://github.com/nhooyr/websocket/pull/326) - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support From cb7509ab70e9f9ca4ce47a2eb90ff86ebaee2d28 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 08:47:44 -0700 Subject: [PATCH 449/519] mask_amd64.s: Remove AVX2 fully --- mask_amd64.s | 29 ++--------------------------- mask_arm64.s | 1 - mask_asm.go | 2 -- 3 files changed, 2 insertions(+), 30 deletions(-) diff --git a/mask_amd64.s b/mask_amd64.s index caca53ec..bd42be31 100644 --- a/mask_amd64.s +++ b/mask_amd64.s @@ -26,11 +26,6 @@ TEXT ·maskAsm(SB), NOSPLIT, $0-28 TESTQ $31, AX JNZ unaligned -aligned: - CMPB ·useAVX2(SB), $1 - JE avx2 - JMP sse - unaligned_loop_1byte: XORB SI, (AX) INCQ AX @@ -47,7 +42,7 @@ unaligned_loop_1byte: ORQ DX, DI TESTQ $31, AX - JZ aligned + JZ sse unaligned: TESTQ $7, AX // AND $7 & len, if not zero jump to loop_1b. @@ -60,27 +55,7 @@ unaligned_loop: SUBQ $8, CX TESTQ $31, AX JNZ unaligned_loop - JMP aligned - -avx2: - CMPQ CX, $0x80 - JL sse - VMOVQ DI, X0 - VPBROADCASTQ X0, Y0 - -avx2_loop: - VPXOR (AX), Y0, Y1 - VPXOR 32(AX), Y0, Y2 - VPXOR 64(AX), Y0, Y3 - VPXOR 96(AX), Y0, Y4 - VMOVDQU Y1, (AX) - VMOVDQU Y2, 32(AX) - VMOVDQU Y3, 64(AX) - VMOVDQU Y4, 96(AX) - ADDQ $0x80, AX - SUBQ $0x80, CX - CMPQ CX, $0x80 - JAE avx2_loop // loop if CX >= 0x80 + JMP sse sse: CMPQ CX, $0x40 diff --git a/mask_arm64.s b/mask_arm64.s index 624cb720..b3d48e68 100644 --- a/mask_arm64.s +++ b/mask_arm64.s @@ -15,7 +15,6 @@ TEXT ·maskAsm(SB), NOSPLIT, $0-28 CMP $64, R1 BLT less_than_64 - // todo: optimize unaligned case loop_64: VLD1 (R0), [V1.B16, V2.B16, V3.B16, V4.B16] VEOR V1.B16, V0.B16, V1.B16 diff --git a/mask_asm.go b/mask_asm.go index 946bc0f6..34021fa7 100644 --- a/mask_asm.go +++ b/mask_asm.go @@ -9,7 +9,5 @@ func mask(key uint32, b []byte) uint32 { return key } -var useAVX2 = false - //go:noescape func maskAsm(b *byte, len int, key uint32) uint32 From 3f8c9e07bcaa0a223d092b618c34ca7dba3521db Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 09:20:31 -0700 Subject: [PATCH 450/519] mask_amd64.s: Minor improvements --- frame.go | 2 ++ mask_amd64.s | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/frame.go b/frame.go index 362a99e9..ff09ec26 100644 --- a/frame.go +++ b/frame.go @@ -184,6 +184,8 @@ func writeFrameHeader(h header, w *bufio.Writer, buf []byte) (err error) { // to be in little endian. // // See https://github.com/golang/go/issues/31586 +// +//lint:ignore U1000 mask.go func maskGo(key uint32, b []byte) uint32 { if len(b) >= 8 { key64 := uint64(key)<<32 | uint64(key) diff --git a/mask_amd64.s b/mask_amd64.s index bd42be31..935232fa 100644 --- a/mask_amd64.s +++ b/mask_amd64.s @@ -17,8 +17,8 @@ TEXT ·maskAsm(SB), NOSPLIT, $0-28 SHLQ $32, DI ORQ DX, DI - CMPQ CX, $15 - JLE less_than_16 + CMPQ CX, $7 + JLE less_than_8 CMPQ CX, $63 JLE less_than_64 CMPQ CX, $128 @@ -58,7 +58,7 @@ unaligned_loop: JMP sse sse: - CMPQ CX, $0x40 + CMPQ CX, $64 JL less_than_64 MOVQ DI, X0 PUNPCKLQDQ X0, X0 @@ -76,9 +76,9 @@ sse_loop: MOVOU X2, 1*16(AX) MOVOU X3, 2*16(AX) MOVOU X4, 3*16(AX) - ADDQ $0x40, AX - SUBQ $0x40, CX - CMPQ CX, $0x40 + ADDQ $64, AX + SUBQ $64, CX + CMPQ CX, $64 JAE sse_loop less_than_64: From 367743dc6fe48866a91ef88296699449cca17d4d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 15:57:47 -0700 Subject: [PATCH 451/519] mask_amd64.sh: Cleanup --- mask_amd64.s | 23 ++++++++++++----------- mask_arm64.s | 4 +++- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/mask_amd64.s b/mask_amd64.s index 935232fa..905d7e4a 100644 --- a/mask_amd64.s +++ b/mask_amd64.s @@ -10,18 +10,18 @@ TEXT ·maskAsm(SB), NOSPLIT, $0-28 MOVQ len+8(FP), CX MOVL key+16(FP), SI - // calculate the DI - // DI = SI<<32 | SI + // Calculate the DI aka the uint64 key. + // DI = uint64(SI) | uint64(SI)<<32 MOVL SI, DI MOVQ DI, DX SHLQ $32, DI ORQ DX, DI - CMPQ CX, $7 - JLE less_than_8 - CMPQ CX, $63 - JLE less_than_64 - CMPQ CX, $128 + CMPQ CX, $8 + JL less_than_8 + CMPQ CX, $64 + JL less_than_64 + CMPQ CX, $512 JLE sse TESTQ $31, AX JNZ unaligned @@ -34,8 +34,8 @@ unaligned_loop_1byte: TESTQ $7, AX JNZ unaligned_loop_1byte - // calculate DI again since SI was modified - // DI = SI<<32 | SI + // Calculate DI again since SI was modified. + // DI = uint64(SI) | uint64(SI)<<32 MOVL SI, DI MOVQ DI, DX SHLQ $32, DI @@ -45,11 +45,12 @@ unaligned_loop_1byte: JZ sse unaligned: - TESTQ $7, AX // AND $7 & len, if not zero jump to loop_1b. + // $7 & len, if not zero jump to loop_1b. + TESTQ $7, AX JNZ unaligned_loop_1byte unaligned_loop: - // we don't need to check the CX since we know it's above 128 + // We don't need to check the CX since we know it's above 512. XORQ DI, (AX) ADDQ $8, AX SUBQ $8, CX diff --git a/mask_arm64.s b/mask_arm64.s index b3d48e68..741b77a5 100644 --- a/mask_arm64.s +++ b/mask_arm64.s @@ -1,6 +1,6 @@ #include "textflag.h" -// func maskAsm(b *byte,len, int, key uint32) +// func maskAsm(b *byte, len int, key uint32) TEXT ·maskAsm(SB), NOSPLIT, $0-28 // R0 = b // R1 = len @@ -15,6 +15,8 @@ TEXT ·maskAsm(SB), NOSPLIT, $0-28 CMP $64, R1 BLT less_than_64 +// TODO: allign memory like amd64 + loop_64: VLD1 (R0), [V1.B16, V2.B16, V3.B16, V4.B16] VEOR V1.B16, V0.B16, V1.B16 From 27f80cb8b4515ffa660eaa962aa01cd370e4c48e Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 16:24:32 -0700 Subject: [PATCH 452/519] mask.go: Cleanup assembly and add nbio benchmark --- frame.go | 2 +- frame_test.go | 2 +- internal/thirdparty/frame_test.go | 50 ++++++++++++++++++++----------- internal/thirdparty/go.mod | 2 ++ internal/thirdparty/go.sum | 36 ++++++++++++++++++++++ mask.go | 4 +-- mask_amd64.s | 2 -- mask_arm64.s | 2 +- mask_asm.go | 2 +- read.go | 4 +-- write.go | 2 +- 11 files changed, 79 insertions(+), 29 deletions(-) diff --git a/frame.go b/frame.go index ff09ec26..5d826ea3 100644 --- a/frame.go +++ b/frame.go @@ -186,7 +186,7 @@ func writeFrameHeader(h header, w *bufio.Writer, buf []byte) (err error) { // See https://github.com/golang/go/issues/31586 // //lint:ignore U1000 mask.go -func maskGo(key uint32, b []byte) uint32 { +func maskGo(b []byte, key uint32) uint32 { if len(b) >= 8 { key64 := uint64(key)<<32 | uint64(key) diff --git a/frame_test.go b/frame_test.go index e697e198..bd626358 100644 --- a/frame_test.go +++ b/frame_test.go @@ -97,7 +97,7 @@ func Test_mask(t *testing.T) { key := []byte{0xa, 0xb, 0xc, 0xff} key32 := binary.LittleEndian.Uint32(key) p := []byte{0xa, 0xb, 0xc, 0xf2, 0xc} - gotKey32 := mask(key32, p) + gotKey32 := mask(p, key32) expP := []byte{0, 0, 0, 0x0d, 0x6} assert.Equal(t, "p", expP, p) diff --git a/internal/thirdparty/frame_test.go b/internal/thirdparty/frame_test.go index dd0440db..7202322d 100644 --- a/internal/thirdparty/frame_test.go +++ b/internal/thirdparty/frame_test.go @@ -8,11 +8,12 @@ import ( "github.com/gobwas/ws" _ "github.com/gorilla/websocket" + _ "github.com/lesismal/nbio/nbhttp/websocket" _ "nhooyr.io/websocket" ) -func basicMask(maskKey [4]byte, pos int, b []byte) int { +func basicMask(b []byte, maskKey [4]byte, pos int) int { for i := range b { b[i] ^= maskKey[pos&3] pos++ @@ -20,26 +21,30 @@ func basicMask(maskKey [4]byte, pos int, b []byte) int { return pos & 3 } -//go:linkname gorillaMaskBytes github.com/gorilla/websocket.maskBytes -func gorillaMaskBytes(key [4]byte, pos int, b []byte) int +//go:linkname maskGo nhooyr.io/websocket.maskGo +func maskGo(b []byte, key32 uint32) int -//go:linkname mask nhooyr.io/websocket.mask -func mask(key32 uint32, b []byte) int +//go:linkname maskAsm nhooyr.io/websocket.maskAsm +func maskAsm(b *byte, len int, key32 uint32) uint32 -//go:linkname maskGo nhooyr.io/websocket.maskGo -func maskGo(key32 uint32, b []byte) int +//go:linkname nbioMaskBytes github.com/lesismal/nbio/nbhttp/websocket.maskXOR +func nbioMaskBytes(b, key []byte) int + +//go:linkname gorillaMaskBytes github.com/gorilla/websocket.maskBytes +func gorillaMaskBytes(key [4]byte, pos int, b []byte) int func Benchmark_mask(b *testing.B) { sizes := []int{ - 2, - 3, - 4, 8, 16, 32, 128, + 256, 512, + 1024, + 2048, 4096, + 8192, 16384, } @@ -51,7 +56,7 @@ func Benchmark_mask(b *testing.B) { name: "basic", fn: func(b *testing.B, key [4]byte, p []byte) { for i := 0; i < b.N; i++ { - basicMask(key, 0, p) + basicMask(p, key, 0) } }, }, @@ -63,7 +68,7 @@ func Benchmark_mask(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - maskGo(key32, p) + maskGo(p, key32) } }, }, @@ -74,7 +79,7 @@ func Benchmark_mask(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - mask(key32, p) + maskAsm(&p[0], len(p), key32) } }, }, @@ -95,16 +100,25 @@ func Benchmark_mask(b *testing.B) { } }, }, + { + name: "nbio", + fn: func(b *testing.B, key [4]byte, p []byte) { + keyb := key[:] + for i := 0; i < b.N; i++ { + nbioMaskBytes(p, keyb) + } + }, + }, } key := [4]byte{1, 2, 3, 4} - for _, size := range sizes { - p := make([]byte, size) + for _, fn := range fns { + b.Run(fn.name, func(b *testing.B) { + for _, size := range sizes { + p := make([]byte, size) - b.Run(strconv.Itoa(size), func(b *testing.B) { - for _, fn := range fns { - b.Run(fn.name, func(b *testing.B) { + b.Run(strconv.Itoa(size), func(b *testing.B) { b.SetBytes(int64(size)) fn.fn(b, key, p) diff --git a/internal/thirdparty/go.mod b/internal/thirdparty/go.mod index 3f32a416..f418d288 100644 --- a/internal/thirdparty/go.mod +++ b/internal/thirdparty/go.mod @@ -8,6 +8,7 @@ require ( github.com/gin-gonic/gin v1.9.1 github.com/gobwas/ws v1.3.0 github.com/gorilla/websocket v1.5.0 + github.com/lesismal/nbio v1.3.18 nhooyr.io/websocket v0.0.0-00010101000000-000000000000 ) @@ -25,6 +26,7 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/leodido/go-urn v1.2.4 // indirect + github.com/lesismal/llib v1.1.12 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect diff --git a/internal/thirdparty/go.sum b/internal/thirdparty/go.sum index 47f324bb..658a4a7b 100644 --- a/internal/thirdparty/go.sum +++ b/internal/thirdparty/go.sum @@ -41,6 +41,10 @@ github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZX github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/lesismal/llib v1.1.12 h1:KJFB8bL02V+QGIvILEw/w7s6bKj9Ps9Px97MZP2EOk0= +github.com/lesismal/llib v1.1.12/go.mod h1:70tFXXe7P1FZ02AU9l8LgSOK7d7sRrpnkUr3rd3gKSg= +github.com/lesismal/nbio v1.3.18 h1:kmJZlxjQpVfuCPYcXdv0Biv9LHVViJZet5K99Xs3RAs= +github.com/lesismal/nbio v1.3.18/go.mod h1:KWlouFT5cgDdW5sMX8RsHASUMGniea9X0XIellZ0B38= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -67,19 +71,51 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210513122933-cd7d49e622d5/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= diff --git a/mask.go b/mask.go index 7c9fe308..b29435e9 100644 --- a/mask.go +++ b/mask.go @@ -2,6 +2,6 @@ package websocket -func mask(key uint32, b []byte) uint32 { - return maskGo(key, b) +func mask(b []byte, key uint32) uint32 { + return maskGo(b, key) } diff --git a/mask_amd64.s b/mask_amd64.s index 905d7e4a..73ae59b4 100644 --- a/mask_amd64.s +++ b/mask_amd64.s @@ -19,8 +19,6 @@ TEXT ·maskAsm(SB), NOSPLIT, $0-28 CMPQ CX, $8 JL less_than_8 - CMPQ CX, $64 - JL less_than_64 CMPQ CX, $512 JLE sse TESTQ $31, AX diff --git a/mask_arm64.s b/mask_arm64.s index 741b77a5..8fd49aa9 100644 --- a/mask_arm64.s +++ b/mask_arm64.s @@ -4,8 +4,8 @@ TEXT ·maskAsm(SB), NOSPLIT, $0-28 // R0 = b // R1 = len - // R2 = uint64(key)<<32 | uint64(key) // R3 = key (uint32) + // R2 = uint64(key)<<32 | uint64(key) MOVD b_ptr+0(FP), R0 MOVD b_len+8(FP), R1 MOVWU key+16(FP), R3 diff --git a/mask_asm.go b/mask_asm.go index 34021fa7..b8c4ee66 100644 --- a/mask_asm.go +++ b/mask_asm.go @@ -2,7 +2,7 @@ package websocket -func mask(key uint32, b []byte) uint32 { +func mask(b []byte, key uint32) uint32 { if len(b) > 0 { return maskAsm(&b[0], len(b), key) } diff --git a/read.go b/read.go index 8742842e..81b89831 100644 --- a/read.go +++ b/read.go @@ -289,7 +289,7 @@ func (c *Conn) handleControl(ctx context.Context, h header) (err error) { } if h.masked { - mask(h.maskKey, b) + mask(b, h.maskKey) } switch h.opcode { @@ -453,7 +453,7 @@ func (mr *msgReader) read(p []byte) (int, error) { mr.payloadLength -= int64(n) if !mr.c.client { - mr.maskKey = mask(mr.maskKey, p) + mr.maskKey = mask(p, mr.maskKey) } return n, nil diff --git a/write.go b/write.go index 7b1152ce..7ac7ce63 100644 --- a/write.go +++ b/write.go @@ -365,7 +365,7 @@ func (c *Conn) writeFramePayload(p []byte) (n int, err error) { return n, err } - maskKey = mask(maskKey, c.writeBuf[i:c.bw.Buffered()]) + maskKey = mask(c.writeBuf[i:c.bw.Buffered()], maskKey) p = p[j:] n += j From 369d641608eba0a8387f79eb204c56f364c2e31d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 17:04:52 -0700 Subject: [PATCH 453/519] mask_arm64.s: Cleanup --- mask_amd64.s | 4 ++-- mask_arm64.s | 24 +++++++++++------------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/mask_amd64.s b/mask_amd64.s index 73ae59b4..8464440b 100644 --- a/mask_amd64.s +++ b/mask_amd64.s @@ -117,10 +117,10 @@ less_than_4: less_than_2: TESTQ $1, CX - JZ done + JZ end XORB SI, (AX) ROLL $24, SI -done: +end: MOVL SI, ret+24(FP) RET diff --git a/mask_arm64.s b/mask_arm64.s index 8fd49aa9..42a1211f 100644 --- a/mask_arm64.s +++ b/mask_arm64.s @@ -15,7 +15,7 @@ TEXT ·maskAsm(SB), NOSPLIT, $0-28 CMP $64, R1 BLT less_than_64 -// TODO: allign memory like amd64 +// TODO: align memory like amd64 loop_64: VLD1 (R0), [V1.B16, V2.B16, V3.B16, V4.B16] @@ -29,41 +29,39 @@ loop_64: BGE loop_64 less_than_64: - // quick end - CBZ R1, end - TBZ $5, R1, less_than32 + TBZ $5, R1, less_than_32 VLD1 (R0), [V1.B16, V2.B16] VEOR V1.B16, V0.B16, V1.B16 VEOR V2.B16, V0.B16, V2.B16 VST1.P [V1.B16, V2.B16], 32(R0) -less_than32: - TBZ $4, R1, less_than16 +less_than_32: + TBZ $4, R1, less_than_16 LDP (R0), (R11, R12) EOR R11, R2, R11 EOR R12, R2, R12 STP.P (R11, R12), 16(R0) -less_than16: - TBZ $3, R1, less_than8 +less_than_16: + TBZ $3, R1, less_than_8 MOVD (R0), R11 EOR R2, R11, R11 MOVD.P R11, 8(R0) -less_than8: - TBZ $2, R1, less_than4 +less_than_8: + TBZ $2, R1, less_than_4 MOVWU (R0), R11 EORW R2, R11, R11 MOVWU.P R11, 4(R0) -less_than4: - TBZ $1, R1, less_than2 +less_than_4: + TBZ $1, R1, less_than_2 MOVHU (R0), R11 EORW R3, R11, R11 MOVHU.P R11, 2(R0) RORW $16, R3 -less_than2: +less_than_2: TBZ $0, R1, end MOVBU (R0), R11 EORW R3, R11, R11 From fb13df2dc30520f64bd4daa167ed9c7e739a98b7 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 19:46:00 -0700 Subject: [PATCH 454/519] ci/bench.sh: Benchmark masking on arm64 with QEMU --- ci/bench.sh | 3 +++ internal/thirdparty/frame_test.go | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/ci/bench.sh b/ci/bench.sh index a553b93a..6af59ecf 100755 --- a/ci/bench.sh +++ b/ci/bench.sh @@ -6,4 +6,7 @@ go test --run=^$ --bench=. --benchmem --memprofile ci/out/prof.mem --cpuprofile ( cd ./internal/thirdparty go test --run=^$ --bench=. --benchmem --memprofile ../../ci/out/prof-thirdparty.mem --cpuprofile ../../ci/out/prof-thirdparty.cpu -o ../../ci/out/thirdparty.test "$@" . + + GOARCH=arm64 go test -c -o ../../ci/out/thirdparty-arm64.test . + qemu-aarch64 ../../ci/out/thirdparty-arm64.test --test.run=^$ --test.bench=Benchmark_mask --test.benchmem --test.memprofile ../../ci/out/prof-thirdparty-arm64.mem --test.cpuprofile ../../ci/out/prof-thirdparty-arm64.cpu . ) diff --git a/internal/thirdparty/frame_test.go b/internal/thirdparty/frame_test.go index 7202322d..89042e53 100644 --- a/internal/thirdparty/frame_test.go +++ b/internal/thirdparty/frame_test.go @@ -2,6 +2,7 @@ package thirdparty import ( "encoding/binary" + "runtime" "strconv" "testing" _ "unsafe" @@ -34,6 +35,10 @@ func nbioMaskBytes(b, key []byte) int func gorillaMaskBytes(key [4]byte, pos int, b []byte) int func Benchmark_mask(b *testing.B) { + b.Run(runtime.GOARCH, benchmark_mask) +} + +func benchmark_mask(b *testing.B) { sizes := []int{ 8, 16, From ecf7dec40098cbc3b659d99b467c0d6c97f38c6c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 19:52:36 -0700 Subject: [PATCH 455/519] ci/bench.sh: Install QEMU on CI --- README.md | 3 +-- ci/bench.sh | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4b2d828e..0f286e63 100644 --- a/README.md +++ b/README.md @@ -120,9 +120,8 @@ Advantages of nhooyr.io/websocket: - Gorilla requires registering a pong callback before sending a Ping - Can target Wasm ([gorilla/websocket#432](https://github.com/gorilla/websocket/issues/432)) - Transparent message buffer reuse with [wsjson](https://pkg.go.dev/nhooyr.io/websocket/wsjson) subpackage -- [3x](https://github.com/nhooyr/websocket/pull/326) faster WebSocket masking implementation in assembly for amd64 and arm64 and [2x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster implementation in pure Go +- [4x](https://github.com/nhooyr/websocket/pull/326) faster WebSocket masking implementation in assembly for amd64 and arm64 and [2x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster implementation in pure Go - Gorilla's implementation is slower and uses [unsafe](https://golang.org/pkg/unsafe/). - Soon we'll have assembly and be 3x faster [#326](https://github.com/nhooyr/websocket/pull/326) - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support - Gorilla only supports no context takeover mode - [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper for write only connections ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492)) diff --git a/ci/bench.sh b/ci/bench.sh index 6af59ecf..afc2d825 100755 --- a/ci/bench.sh +++ b/ci/bench.sh @@ -8,5 +8,10 @@ go test --run=^$ --bench=. --benchmem --memprofile ci/out/prof.mem --cpuprofile go test --run=^$ --bench=. --benchmem --memprofile ../../ci/out/prof-thirdparty.mem --cpuprofile ../../ci/out/prof-thirdparty.cpu -o ../../ci/out/thirdparty.test "$@" . GOARCH=arm64 go test -c -o ../../ci/out/thirdparty-arm64.test . + if [ "${CI-}" ]; then + sudo apt-get update + sudo apt-get install -y qemu-user-static + alias qemu-aarch64=qemu-aarch64-static + fi qemu-aarch64 ../../ci/out/thirdparty-arm64.test --test.run=^$ --test.bench=Benchmark_mask --test.benchmem --test.memprofile ../../ci/out/prof-thirdparty-arm64.mem --test.cpuprofile ../../ci/out/prof-thirdparty-arm64.cpu . ) From d34e5d4b8e4c3d9ab9311d43492fe0bded560ccc Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 20 Oct 2023 07:47:25 -0700 Subject: [PATCH 456/519] wsjson: Add json.Encoder vs json.Marshal benchmark json.Encoder is 42% faster than json.Marshal thanks to the memory reuse. goos: linux goarch: amd64 pkg: nhooyr.io/websocket/wsjson cpu: 12th Gen Intel(R) Core(TM) i5-1235U BenchmarkJSON/json.Encoder-12 3517579 340.2 ns/op 24 B/op 1 allocs/op BenchmarkJSON/json.Marshal-12 2374086 484.3 ns/op 728 B/op 2 allocs/op Closes #409 --- wsjson/wsjson_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 wsjson/wsjson_test.go diff --git a/wsjson/wsjson_test.go b/wsjson/wsjson_test.go new file mode 100644 index 00000000..a70e808c --- /dev/null +++ b/wsjson/wsjson_test.go @@ -0,0 +1,24 @@ +package wsjson_test + +import ( + "encoding/json" + "io" + "strings" + "testing" +) + +func BenchmarkJSON(b *testing.B) { + msg := []byte(strings.Repeat("1234", 128)) + b.SetBytes(int64(len(msg))) + b.ReportAllocs() + b.Run("json.Encoder", func(b *testing.B) { + for i := 0; i < b.N; i++ { + json.NewEncoder(io.Discard).Encode(msg) + } + }) + b.Run("json.Marshal", func(b *testing.B) { + for i := 0; i < b.N; i++ { + json.Marshal(msg) + } + }) +} From e25d9681bd6cc0a9176a2fa35d1a1e16bdcd685d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 20 Oct 2023 07:55:15 -0700 Subject: [PATCH 457/519] ci/bench.sh: Don't profile by default --- ci/bench.sh | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/ci/bench.sh b/ci/bench.sh index afc2d825..afe0490b 100755 --- a/ci/bench.sh +++ b/ci/bench.sh @@ -2,16 +2,19 @@ set -eu cd -- "$(dirname "$0")/.." -go test --run=^$ --bench=. --benchmem --memprofile ci/out/prof.mem --cpuprofile ci/out/prof.cpu -o ci/out/websocket.test "$@" . +go test --run=^$ --bench=. --benchmem "$@" ./... +# For profiling add: --memprofile ci/out/prof.mem --cpuprofile ci/out/prof.cpu -o ci/out/websocket.test ( cd ./internal/thirdparty - go test --run=^$ --bench=. --benchmem --memprofile ../../ci/out/prof-thirdparty.mem --cpuprofile ../../ci/out/prof-thirdparty.cpu -o ../../ci/out/thirdparty.test "$@" . + go test --run=^$ --bench=. --benchmem "$@" . - GOARCH=arm64 go test -c -o ../../ci/out/thirdparty-arm64.test . - if [ "${CI-}" ]; then - sudo apt-get update - sudo apt-get install -y qemu-user-static - alias qemu-aarch64=qemu-aarch64-static + GOARCH=arm64 go test -c -o ../../ci/out/thirdparty-arm64.test "$@" . + if [ "$#" -eq 0 ]; then + if [ "${CI-}" ]; then + sudo apt-get update + sudo apt-get install -y qemu-user-static + alias qemu-aarch64=qemu-aarch64-static + fi + qemu-aarch64 ../../ci/out/thirdparty-arm64.test --test.run=^$ --test.bench=Benchmark_mask --test.benchmem fi - qemu-aarch64 ../../ci/out/thirdparty-arm64.test --test.run=^$ --test.bench=Benchmark_mask --test.benchmem --test.memprofile ../../ci/out/prof-thirdparty-arm64.mem --test.cpuprofile ../../ci/out/prof-thirdparty-arm64.cpu . ) From 640e3c25bcb716956df6452b85ef7ead53477040 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 20 Oct 2023 07:56:39 -0700 Subject: [PATCH 458/519] ci/bench.sh: Try function instead of alias --- ci/bench.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/bench.sh b/ci/bench.sh index afe0490b..5b1360d0 100755 --- a/ci/bench.sh +++ b/ci/bench.sh @@ -13,7 +13,7 @@ go test --run=^$ --bench=. --benchmem "$@" ./... if [ "${CI-}" ]; then sudo apt-get update sudo apt-get install -y qemu-user-static - alias qemu-aarch64=qemu-aarch64-static + qemu-aarch64() { qemu-aarch64-static "$@" } fi qemu-aarch64 ../../ci/out/thirdparty-arm64.test --test.run=^$ --test.bench=Benchmark_mask --test.benchmem fi From 0596e7a0e19d842ac2d9db0d598902dba6485cc4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 20 Oct 2023 08:04:36 -0700 Subject: [PATCH 459/519] wsjson: Extend benchmark with multiple sizes [qrvnl@dios ~/src/websocket] 130$ go test -bench=. ./wsjson/ goos: linux goarch: amd64 pkg: nhooyr.io/websocket/wsjson cpu: 12th Gen Intel(R) Core(TM) i5-1235U BenchmarkJSON/json.Encoder/8-12 14041426 72.59 ns/op 110.21 MB/s 16 B/op 1 allocs/op BenchmarkJSON/json.Encoder/16-12 13936426 86.99 ns/op 183.92 MB/s 16 B/op 1 allocs/op BenchmarkJSON/json.Encoder/32-12 11416401 115.3 ns/op 277.59 MB/s 16 B/op 1 allocs/op BenchmarkJSON/json.Encoder/128-12 4600574 264.7 ns/op 483.55 MB/s 16 B/op 1 allocs/op BenchmarkJSON/json.Encoder/256-12 2710398 433.9 ns/op 590.06 MB/s 16 B/op 1 allocs/op BenchmarkJSON/json.Encoder/512-12 1588930 717.3 ns/op 713.82 MB/s 16 B/op 1 allocs/op BenchmarkJSON/json.Encoder/1024-12 823138 1484 ns/op 689.80 MB/s 16 B/op 1 allocs/op BenchmarkJSON/json.Encoder/2048-12 402823 2875 ns/op 712.32 MB/s 16 B/op 1 allocs/op BenchmarkJSON/json.Encoder/4096-12 213926 5602 ns/op 731.14 MB/s 16 B/op 1 allocs/op BenchmarkJSON/json.Encoder/8192-12 92864 11281 ns/op 726.19 MB/s 16 B/op 1 allocs/op BenchmarkJSON/json.Encoder/16384-12 39318 29203 ns/op 561.04 MB/s 19 B/op 1 allocs/op BenchmarkJSON/json.Marshal/8-12 10768671 114.5 ns/op 69.89 MB/s 48 B/op 2 allocs/op BenchmarkJSON/json.Marshal/16-12 10140996 113.9 ns/op 140.51 MB/s 64 B/op 2 allocs/op BenchmarkJSON/json.Marshal/32-12 9211780 121.6 ns/op 263.06 MB/s 64 B/op 2 allocs/op BenchmarkJSON/json.Marshal/128-12 4632796 264.2 ns/op 484.53 MB/s 224 B/op 2 allocs/op BenchmarkJSON/json.Marshal/256-12 2441511 473.5 ns/op 540.65 MB/s 432 B/op 2 allocs/op BenchmarkJSON/json.Marshal/512-12 1298788 896.2 ns/op 571.27 MB/s 912 B/op 2 allocs/op BenchmarkJSON/json.Marshal/1024-12 602084 1866 ns/op 548.83 MB/s 1808 B/op 2 allocs/op BenchmarkJSON/json.Marshal/2048-12 341151 3817 ns/op 536.61 MB/s 3474 B/op 2 allocs/op BenchmarkJSON/json.Marshal/4096-12 175594 7034 ns/op 582.32 MB/s 6548 B/op 2 allocs/op BenchmarkJSON/json.Marshal/8192-12 83222 15023 ns/op 545.30 MB/s 13591 B/op 2 allocs/op BenchmarkJSON/json.Marshal/16384-12 33087 39348 ns/op 416.39 MB/s 27304 B/op 2 allocs/op PASS ok nhooyr.io/websocket/wsjson 32.934s --- wsjson/wsjson_test.go | 45 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/wsjson/wsjson_test.go b/wsjson/wsjson_test.go index a70e808c..080ab38d 100644 --- a/wsjson/wsjson_test.go +++ b/wsjson/wsjson_test.go @@ -3,22 +3,51 @@ package wsjson_test import ( "encoding/json" "io" - "strings" + "strconv" "testing" + + "nhooyr.io/websocket/internal/test/xrand" ) func BenchmarkJSON(b *testing.B) { - msg := []byte(strings.Repeat("1234", 128)) - b.SetBytes(int64(len(msg))) - b.ReportAllocs() + sizes := []int{ + 8, + 16, + 32, + 128, + 256, + 512, + 1024, + 2048, + 4096, + 8192, + 16384, + } + b.Run("json.Encoder", func(b *testing.B) { - for i := 0; i < b.N; i++ { - json.NewEncoder(io.Discard).Encode(msg) + for _, size := range sizes { + b.Run(strconv.Itoa(size), func(b *testing.B) { + msg := xrand.String(size) + b.SetBytes(int64(size)) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + json.NewEncoder(io.Discard).Encode(msg) + } + }) } }) b.Run("json.Marshal", func(b *testing.B) { - for i := 0; i < b.N; i++ { - json.Marshal(msg) + for _, size := range sizes { + b.Run(strconv.Itoa(size), func(b *testing.B) { + msg := xrand.String(size) + b.SetBytes(int64(size)) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + json.Marshal(msg) + } + }) } }) } From 30447a3e05b34bbf55ed531aebeb31192dacd251 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 20 Oct 2023 08:08:21 -0700 Subject: [PATCH 460/519] ci/bench.sh: Just symlink the expected qemu-aarch64 binary name --- ci/bench.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/bench.sh b/ci/bench.sh index 5b1360d0..30c06986 100755 --- a/ci/bench.sh +++ b/ci/bench.sh @@ -13,7 +13,7 @@ go test --run=^$ --bench=. --benchmem "$@" ./... if [ "${CI-}" ]; then sudo apt-get update sudo apt-get install -y qemu-user-static - qemu-aarch64() { qemu-aarch64-static "$@" } + ln -s /usr/bin/qemu-aarch64-static /usr/local/bin/qemu-aarch64 fi qemu-aarch64 ../../ci/out/thirdparty-arm64.test --test.run=^$ --test.bench=Benchmark_mask --test.benchmem fi From f4e61e5a124dfdc0df65ae401b6f7c134005fc5f Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 20 Oct 2023 21:45:28 -0700 Subject: [PATCH 461/519] ci/fmt.sh: Error if changes on CI --- ci/fmt.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ci/fmt.sh b/ci/fmt.sh index 6e5a68e4..31d0c15d 100755 --- a/ci/fmt.sh +++ b/ci/fmt.sh @@ -18,3 +18,7 @@ npx prettier@3.0.3 \ $(git ls-files "*.yml" "*.md" "*.js" "*.css" "*.html") go run golang.org/x/tools/cmd/stringer@latest -type=opcode,MessageType,StatusCode -output=stringer.go + +if [ "${CI-}" ]; then + git diff --exit-code +fi From f533f430c7d63e9e0bceb2dcbbd5d75602803b82 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 21 Oct 2023 05:45:11 -0700 Subject: [PATCH 462/519] mask.go: Reorganize --- frame.go | 125 ------------------------------------------------ mask.go | 131 +++++++++++++++++++++++++++++++++++++++++++++++++-- mask_amd64.s | 36 +++++++++++--- mask_asm.go | 2 + mask_go.go | 7 +++ 5 files changed, 166 insertions(+), 135 deletions(-) create mode 100644 mask_go.go diff --git a/frame.go b/frame.go index 5d826ea3..d5631863 100644 --- a/frame.go +++ b/frame.go @@ -8,7 +8,6 @@ import ( "fmt" "io" "math" - "math/bits" "nhooyr.io/websocket/internal/errd" ) @@ -172,127 +171,3 @@ func writeFrameHeader(h header, w *bufio.Writer, buf []byte) (err error) { return nil } - -// maskGo applies the WebSocket masking algorithm to p -// with the given key. -// See https://tools.ietf.org/html/rfc6455#section-5.3 -// -// The returned value is the correctly rotated key to -// to continue to mask/unmask the message. -// -// It is optimized for LittleEndian and expects the key -// to be in little endian. -// -// See https://github.com/golang/go/issues/31586 -// -//lint:ignore U1000 mask.go -func maskGo(b []byte, key uint32) uint32 { - if len(b) >= 8 { - key64 := uint64(key)<<32 | uint64(key) - - // At some point in the future we can clean these unrolled loops up. - // See https://github.com/golang/go/issues/31586#issuecomment-487436401 - - // Then we xor until b is less than 128 bytes. - for len(b) >= 128 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^key64) - v = binary.LittleEndian.Uint64(b[8:16]) - binary.LittleEndian.PutUint64(b[8:16], v^key64) - v = binary.LittleEndian.Uint64(b[16:24]) - binary.LittleEndian.PutUint64(b[16:24], v^key64) - v = binary.LittleEndian.Uint64(b[24:32]) - binary.LittleEndian.PutUint64(b[24:32], v^key64) - v = binary.LittleEndian.Uint64(b[32:40]) - binary.LittleEndian.PutUint64(b[32:40], v^key64) - v = binary.LittleEndian.Uint64(b[40:48]) - binary.LittleEndian.PutUint64(b[40:48], v^key64) - v = binary.LittleEndian.Uint64(b[48:56]) - binary.LittleEndian.PutUint64(b[48:56], v^key64) - v = binary.LittleEndian.Uint64(b[56:64]) - binary.LittleEndian.PutUint64(b[56:64], v^key64) - v = binary.LittleEndian.Uint64(b[64:72]) - binary.LittleEndian.PutUint64(b[64:72], v^key64) - v = binary.LittleEndian.Uint64(b[72:80]) - binary.LittleEndian.PutUint64(b[72:80], v^key64) - v = binary.LittleEndian.Uint64(b[80:88]) - binary.LittleEndian.PutUint64(b[80:88], v^key64) - v = binary.LittleEndian.Uint64(b[88:96]) - binary.LittleEndian.PutUint64(b[88:96], v^key64) - v = binary.LittleEndian.Uint64(b[96:104]) - binary.LittleEndian.PutUint64(b[96:104], v^key64) - v = binary.LittleEndian.Uint64(b[104:112]) - binary.LittleEndian.PutUint64(b[104:112], v^key64) - v = binary.LittleEndian.Uint64(b[112:120]) - binary.LittleEndian.PutUint64(b[112:120], v^key64) - v = binary.LittleEndian.Uint64(b[120:128]) - binary.LittleEndian.PutUint64(b[120:128], v^key64) - b = b[128:] - } - - // Then we xor until b is less than 64 bytes. - for len(b) >= 64 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^key64) - v = binary.LittleEndian.Uint64(b[8:16]) - binary.LittleEndian.PutUint64(b[8:16], v^key64) - v = binary.LittleEndian.Uint64(b[16:24]) - binary.LittleEndian.PutUint64(b[16:24], v^key64) - v = binary.LittleEndian.Uint64(b[24:32]) - binary.LittleEndian.PutUint64(b[24:32], v^key64) - v = binary.LittleEndian.Uint64(b[32:40]) - binary.LittleEndian.PutUint64(b[32:40], v^key64) - v = binary.LittleEndian.Uint64(b[40:48]) - binary.LittleEndian.PutUint64(b[40:48], v^key64) - v = binary.LittleEndian.Uint64(b[48:56]) - binary.LittleEndian.PutUint64(b[48:56], v^key64) - v = binary.LittleEndian.Uint64(b[56:64]) - binary.LittleEndian.PutUint64(b[56:64], v^key64) - b = b[64:] - } - - // Then we xor until b is less than 32 bytes. - for len(b) >= 32 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^key64) - v = binary.LittleEndian.Uint64(b[8:16]) - binary.LittleEndian.PutUint64(b[8:16], v^key64) - v = binary.LittleEndian.Uint64(b[16:24]) - binary.LittleEndian.PutUint64(b[16:24], v^key64) - v = binary.LittleEndian.Uint64(b[24:32]) - binary.LittleEndian.PutUint64(b[24:32], v^key64) - b = b[32:] - } - - // Then we xor until b is less than 16 bytes. - for len(b) >= 16 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^key64) - v = binary.LittleEndian.Uint64(b[8:16]) - binary.LittleEndian.PutUint64(b[8:16], v^key64) - b = b[16:] - } - - // Then we xor until b is less than 8 bytes. - for len(b) >= 8 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^key64) - b = b[8:] - } - } - - // Then we xor until b is less than 4 bytes. - for len(b) >= 4 { - v := binary.LittleEndian.Uint32(b) - binary.LittleEndian.PutUint32(b, v^key) - b = b[4:] - } - - // xor remaining bytes. - for i := range b { - b[i] ^= byte(key) - key = bits.RotateLeft32(key, -8) - } - - return key -} diff --git a/mask.go b/mask.go index b29435e9..5f0746dc 100644 --- a/mask.go +++ b/mask.go @@ -1,7 +1,130 @@ -//go:build !amd64 && !arm64 && !js - package websocket -func mask(b []byte, key uint32) uint32 { - return maskGo(b, key) +import ( + "encoding/binary" + "math/bits" +) + +// maskGo applies the WebSocket masking algorithm to p +// with the given key. +// See https://tools.ietf.org/html/rfc6455#section-5.3 +// +// The returned value is the correctly rotated key to +// to continue to mask/unmask the message. +// +// It is optimized for LittleEndian and expects the key +// to be in little endian. +// +// See https://github.com/golang/go/issues/31586 +// +//lint:ignore U1000 mask.go +func maskGo(b []byte, key uint32) uint32 { + if len(b) >= 8 { + key64 := uint64(key)<<32 | uint64(key) + + // At some point in the future we can clean these unrolled loops up. + // See https://github.com/golang/go/issues/31586#issuecomment-487436401 + + // Then we xor until b is less than 128 bytes. + for len(b) >= 128 { + v := binary.LittleEndian.Uint64(b) + binary.LittleEndian.PutUint64(b, v^key64) + v = binary.LittleEndian.Uint64(b[8:16]) + binary.LittleEndian.PutUint64(b[8:16], v^key64) + v = binary.LittleEndian.Uint64(b[16:24]) + binary.LittleEndian.PutUint64(b[16:24], v^key64) + v = binary.LittleEndian.Uint64(b[24:32]) + binary.LittleEndian.PutUint64(b[24:32], v^key64) + v = binary.LittleEndian.Uint64(b[32:40]) + binary.LittleEndian.PutUint64(b[32:40], v^key64) + v = binary.LittleEndian.Uint64(b[40:48]) + binary.LittleEndian.PutUint64(b[40:48], v^key64) + v = binary.LittleEndian.Uint64(b[48:56]) + binary.LittleEndian.PutUint64(b[48:56], v^key64) + v = binary.LittleEndian.Uint64(b[56:64]) + binary.LittleEndian.PutUint64(b[56:64], v^key64) + v = binary.LittleEndian.Uint64(b[64:72]) + binary.LittleEndian.PutUint64(b[64:72], v^key64) + v = binary.LittleEndian.Uint64(b[72:80]) + binary.LittleEndian.PutUint64(b[72:80], v^key64) + v = binary.LittleEndian.Uint64(b[80:88]) + binary.LittleEndian.PutUint64(b[80:88], v^key64) + v = binary.LittleEndian.Uint64(b[88:96]) + binary.LittleEndian.PutUint64(b[88:96], v^key64) + v = binary.LittleEndian.Uint64(b[96:104]) + binary.LittleEndian.PutUint64(b[96:104], v^key64) + v = binary.LittleEndian.Uint64(b[104:112]) + binary.LittleEndian.PutUint64(b[104:112], v^key64) + v = binary.LittleEndian.Uint64(b[112:120]) + binary.LittleEndian.PutUint64(b[112:120], v^key64) + v = binary.LittleEndian.Uint64(b[120:128]) + binary.LittleEndian.PutUint64(b[120:128], v^key64) + b = b[128:] + } + + // Then we xor until b is less than 64 bytes. + for len(b) >= 64 { + v := binary.LittleEndian.Uint64(b) + binary.LittleEndian.PutUint64(b, v^key64) + v = binary.LittleEndian.Uint64(b[8:16]) + binary.LittleEndian.PutUint64(b[8:16], v^key64) + v = binary.LittleEndian.Uint64(b[16:24]) + binary.LittleEndian.PutUint64(b[16:24], v^key64) + v = binary.LittleEndian.Uint64(b[24:32]) + binary.LittleEndian.PutUint64(b[24:32], v^key64) + v = binary.LittleEndian.Uint64(b[32:40]) + binary.LittleEndian.PutUint64(b[32:40], v^key64) + v = binary.LittleEndian.Uint64(b[40:48]) + binary.LittleEndian.PutUint64(b[40:48], v^key64) + v = binary.LittleEndian.Uint64(b[48:56]) + binary.LittleEndian.PutUint64(b[48:56], v^key64) + v = binary.LittleEndian.Uint64(b[56:64]) + binary.LittleEndian.PutUint64(b[56:64], v^key64) + b = b[64:] + } + + // Then we xor until b is less than 32 bytes. + for len(b) >= 32 { + v := binary.LittleEndian.Uint64(b) + binary.LittleEndian.PutUint64(b, v^key64) + v = binary.LittleEndian.Uint64(b[8:16]) + binary.LittleEndian.PutUint64(b[8:16], v^key64) + v = binary.LittleEndian.Uint64(b[16:24]) + binary.LittleEndian.PutUint64(b[16:24], v^key64) + v = binary.LittleEndian.Uint64(b[24:32]) + binary.LittleEndian.PutUint64(b[24:32], v^key64) + b = b[32:] + } + + // Then we xor until b is less than 16 bytes. + for len(b) >= 16 { + v := binary.LittleEndian.Uint64(b) + binary.LittleEndian.PutUint64(b, v^key64) + v = binary.LittleEndian.Uint64(b[8:16]) + binary.LittleEndian.PutUint64(b[8:16], v^key64) + b = b[16:] + } + + // Then we xor until b is less than 8 bytes. + for len(b) >= 8 { + v := binary.LittleEndian.Uint64(b) + binary.LittleEndian.PutUint64(b, v^key64) + b = b[8:] + } + } + + // Then we xor until b is less than 4 bytes. + for len(b) >= 4 { + v := binary.LittleEndian.Uint32(b) + binary.LittleEndian.PutUint32(b, v^key) + b = b[4:] + } + + // xor remaining bytes. + for i := range b { + b[i] ^= byte(key) + key = bits.RotateLeft32(key, -8) + } + + return key } diff --git a/mask_amd64.s b/mask_amd64.s index 8464440b..ba37731d 100644 --- a/mask_amd64.s +++ b/mask_amd64.s @@ -19,11 +19,16 @@ TEXT ·maskAsm(SB), NOSPLIT, $0-28 CMPQ CX, $8 JL less_than_8 - CMPQ CX, $512 + CMPQ CX, $128 JLE sse TESTQ $31, AX JNZ unaligned +aligned: + CMPB ·useAVX2(SB), $1 + JE avx2 + JMP sse + unaligned_loop_1byte: XORB SI, (AX) INCQ AX @@ -40,7 +45,7 @@ unaligned_loop_1byte: ORQ DX, DI TESTQ $31, AX - JZ sse + JZ aligned unaligned: // $7 & len, if not zero jump to loop_1b. @@ -54,8 +59,27 @@ unaligned_loop: SUBQ $8, CX TESTQ $31, AX JNZ unaligned_loop - JMP sse - + JMP aligned + +avx2: + CMPQ CX, $128 + JL sse + VMOVQ DI, X0 + VPBROADCASTQ X0, Y0 + +// TODO: shouldn't these be aligned movs now? +// TODO: should be 256? +avx2_loop: + VMOVDQU (AX), Y1 + VPXOR Y0, Y1, Y2 + VMOVDQU Y2, (AX) + ADDQ $128, AX + SUBQ $128, CX + CMPQ CX, $128 + // Loop if CX >= 128. + JAE avx2_loop + +// TODO: should be 128? sse: CMPQ CX, $64 JL less_than_64 @@ -63,8 +87,8 @@ sse: PUNPCKLQDQ X0, X0 sse_loop: - MOVOU 0*16(AX), X1 - MOVOU 1*16(AX), X2 + MOVOU (AX), X1 + MOVOU 16(AX), X2 MOVOU 2*16(AX), X3 MOVOU 3*16(AX), X4 PXOR X0, X1 diff --git a/mask_asm.go b/mask_asm.go index b8c4ee66..1f294982 100644 --- a/mask_asm.go +++ b/mask_asm.go @@ -9,5 +9,7 @@ func mask(b []byte, key uint32) uint32 { return key } +var useAVX2 = true + //go:noescape func maskAsm(b *byte, len int, key uint32) uint32 diff --git a/mask_go.go b/mask_go.go new file mode 100644 index 00000000..b29435e9 --- /dev/null +++ b/mask_go.go @@ -0,0 +1,7 @@ +//go:build !amd64 && !arm64 && !js + +package websocket + +func mask(b []byte, key uint32) uint32 { + return maskGo(b, key) +} From a1bb44194159a5ff19ddb3032b796d8466d64d7a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 6 Feb 2024 16:59:47 -0800 Subject: [PATCH 463/519] ci: Fix dev coverage output --- .github/workflows/daily.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index b1e64fbc..340de501 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -50,5 +50,5 @@ jobs: - run: AUTOBAHN=1 ./ci/test.sh - uses: actions/upload-artifact@v3 with: - name: coverage.html + name: coverage-dev.html path: ./ci/out/coverage.html From fee373961a0522e40495f989bdd408146390f7e0 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 7 Feb 2024 02:42:51 -0800 Subject: [PATCH 464/519] mask_asm: Note implementation may not be perfect --- go.mod | 2 ++ go.sum | 2 ++ mask_arm64.s | 3 +-- mask_asm.go | 9 ++++++++- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 715a9f7a..dbc4a5a7 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module nhooyr.io/websocket go 1.19 + +require golang.org/x/sys v0.17.0 // indirect diff --git a/go.sum b/go.sum index e69de29b..735d9a79 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/mask_arm64.s b/mask_arm64.s index 42a1211f..e494b43a 100644 --- a/mask_arm64.s +++ b/mask_arm64.s @@ -15,8 +15,6 @@ TEXT ·maskAsm(SB), NOSPLIT, $0-28 CMP $64, R1 BLT less_than_64 -// TODO: align memory like amd64 - loop_64: VLD1 (R0), [V1.B16, V2.B16, V3.B16, V4.B16] VEOR V1.B16, V0.B16, V1.B16 @@ -29,6 +27,7 @@ loop_64: BGE loop_64 less_than_64: + CBZ R1, end TBZ $5, R1, less_than_32 VLD1 (R0), [V1.B16, V2.B16] VEOR V1.B16, V0.B16, V1.B16 diff --git a/mask_asm.go b/mask_asm.go index 1f294982..865cd4b8 100644 --- a/mask_asm.go +++ b/mask_asm.go @@ -2,6 +2,8 @@ package websocket +import "golang.org/x/sys/cpu" + func mask(b []byte, key uint32) uint32 { if len(b) > 0 { return maskAsm(&b[0], len(b), key) @@ -9,7 +11,12 @@ func mask(b []byte, key uint32) uint32 { return key } -var useAVX2 = true +var useAVX2 = cpu.X86.HasAVX2 +// @nhooyr: I am not confident that the amd64 or the arm64 implementations of this +// function are perfect. There are almost certainly missing optimizations or +// opportunities for // simplification. I'm confident there are no bugs though. +// For example, the arm64 implementation doesn't align memory like the amd64. +// Or the amd64 implementation could use AVX512 instead of just AVX2. //go:noescape func maskAsm(b *byte, len int, key uint32) uint32 From 68fc887a3af8be45880f95f2a5f249a84b2b99b8 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 22 Feb 2024 04:51:47 -0800 Subject: [PATCH 465/519] mask.go: Revert my changes I'm just not good enough at assembly. I added tests to confirm that @wdvxdr's implementation works correctly and matches the output of the basic masking loop. --- go.mod | 2 +- internal/examples/go.mod | 2 ++ internal/examples/go.sum | 2 ++ internal/thirdparty/go.mod | 2 +- internal/thirdparty/go.sum | 4 +-- mask_amd64.s | 62 ++++++++++++++++---------------- mask_asm.go | 2 ++ mask_asm_test.go | 11 ++++++ mask_test.go | 73 ++++++++++++++++++++++++++++++++++++++ 9 files changed, 126 insertions(+), 34 deletions(-) create mode 100644 mask_asm_test.go create mode 100644 mask_test.go diff --git a/go.mod b/go.mod index dbc4a5a7..c6ec72cc 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,4 @@ module nhooyr.io/websocket go 1.19 -require golang.org/x/sys v0.17.0 // indirect +require golang.org/x/sys v0.17.0 diff --git a/internal/examples/go.mod b/internal/examples/go.mod index c98b81ce..50695945 100644 --- a/internal/examples/go.mod +++ b/internal/examples/go.mod @@ -8,3 +8,5 @@ require ( golang.org/x/time v0.3.0 nhooyr.io/websocket v0.0.0-00010101000000-000000000000 ) + +require golang.org/x/sys v0.17.0 // indirect diff --git a/internal/examples/go.sum b/internal/examples/go.sum index f8a07e82..06068548 100644 --- a/internal/examples/go.sum +++ b/internal/examples/go.sum @@ -1,2 +1,4 @@ +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/internal/thirdparty/go.mod b/internal/thirdparty/go.mod index f418d288..d991dd64 100644 --- a/internal/thirdparty/go.mod +++ b/internal/thirdparty/go.mod @@ -36,7 +36,7 @@ require ( golang.org/x/arch v0.3.0 // indirect golang.org/x/crypto v0.9.0 // indirect golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.13.0 // indirect + golang.org/x/sys v0.17.0 // indirect golang.org/x/text v0.9.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/internal/thirdparty/go.sum b/internal/thirdparty/go.sum index 658a4a7b..1f542103 100644 --- a/internal/thirdparty/go.sum +++ b/internal/thirdparty/go.sum @@ -100,8 +100,8 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= diff --git a/mask_amd64.s b/mask_amd64.s index ba37731d..caca53ec 100644 --- a/mask_amd64.s +++ b/mask_amd64.s @@ -10,15 +10,17 @@ TEXT ·maskAsm(SB), NOSPLIT, $0-28 MOVQ len+8(FP), CX MOVL key+16(FP), SI - // Calculate the DI aka the uint64 key. - // DI = uint64(SI) | uint64(SI)<<32 + // calculate the DI + // DI = SI<<32 | SI MOVL SI, DI MOVQ DI, DX SHLQ $32, DI ORQ DX, DI - CMPQ CX, $8 - JL less_than_8 + CMPQ CX, $15 + JLE less_than_16 + CMPQ CX, $63 + JLE less_than_64 CMPQ CX, $128 JLE sse TESTQ $31, AX @@ -37,8 +39,8 @@ unaligned_loop_1byte: TESTQ $7, AX JNZ unaligned_loop_1byte - // Calculate DI again since SI was modified. - // DI = uint64(SI) | uint64(SI)<<32 + // calculate DI again since SI was modified + // DI = SI<<32 | SI MOVL SI, DI MOVQ DI, DX SHLQ $32, DI @@ -48,12 +50,11 @@ unaligned_loop_1byte: JZ aligned unaligned: - // $7 & len, if not zero jump to loop_1b. - TESTQ $7, AX + TESTQ $7, AX // AND $7 & len, if not zero jump to loop_1b. JNZ unaligned_loop_1byte unaligned_loop: - // We don't need to check the CX since we know it's above 512. + // we don't need to check the CX since we know it's above 128 XORQ DI, (AX) ADDQ $8, AX SUBQ $8, CX @@ -62,33 +63,34 @@ unaligned_loop: JMP aligned avx2: - CMPQ CX, $128 + CMPQ CX, $0x80 JL sse VMOVQ DI, X0 VPBROADCASTQ X0, Y0 -// TODO: shouldn't these be aligned movs now? -// TODO: should be 256? avx2_loop: - VMOVDQU (AX), Y1 - VPXOR Y0, Y1, Y2 - VMOVDQU Y2, (AX) - ADDQ $128, AX - SUBQ $128, CX - CMPQ CX, $128 - // Loop if CX >= 128. - JAE avx2_loop - -// TODO: should be 128? + VPXOR (AX), Y0, Y1 + VPXOR 32(AX), Y0, Y2 + VPXOR 64(AX), Y0, Y3 + VPXOR 96(AX), Y0, Y4 + VMOVDQU Y1, (AX) + VMOVDQU Y2, 32(AX) + VMOVDQU Y3, 64(AX) + VMOVDQU Y4, 96(AX) + ADDQ $0x80, AX + SUBQ $0x80, CX + CMPQ CX, $0x80 + JAE avx2_loop // loop if CX >= 0x80 + sse: - CMPQ CX, $64 + CMPQ CX, $0x40 JL less_than_64 MOVQ DI, X0 PUNPCKLQDQ X0, X0 sse_loop: - MOVOU (AX), X1 - MOVOU 16(AX), X2 + MOVOU 0*16(AX), X1 + MOVOU 1*16(AX), X2 MOVOU 2*16(AX), X3 MOVOU 3*16(AX), X4 PXOR X0, X1 @@ -99,9 +101,9 @@ sse_loop: MOVOU X2, 1*16(AX) MOVOU X3, 2*16(AX) MOVOU X4, 3*16(AX) - ADDQ $64, AX - SUBQ $64, CX - CMPQ CX, $64 + ADDQ $0x40, AX + SUBQ $0x40, CX + CMPQ CX, $0x40 JAE sse_loop less_than_64: @@ -141,10 +143,10 @@ less_than_4: less_than_2: TESTQ $1, CX - JZ end + JZ done XORB SI, (AX) ROLL $24, SI -end: +done: MOVL SI, ret+24(FP) RET diff --git a/mask_asm.go b/mask_asm.go index 865cd4b8..bf4bb635 100644 --- a/mask_asm.go +++ b/mask_asm.go @@ -11,6 +11,7 @@ func mask(b []byte, key uint32) uint32 { return key } +//lint:ignore U1000 mask_*.s var useAVX2 = cpu.X86.HasAVX2 // @nhooyr: I am not confident that the amd64 or the arm64 implementations of this @@ -18,5 +19,6 @@ var useAVX2 = cpu.X86.HasAVX2 // opportunities for // simplification. I'm confident there are no bugs though. // For example, the arm64 implementation doesn't align memory like the amd64. // Or the amd64 implementation could use AVX512 instead of just AVX2. +// //go:noescape func maskAsm(b *byte, len int, key uint32) uint32 diff --git a/mask_asm_test.go b/mask_asm_test.go new file mode 100644 index 00000000..416cbc43 --- /dev/null +++ b/mask_asm_test.go @@ -0,0 +1,11 @@ +//go:build amd64 || arm64 + +package websocket + +import "testing" + +func TestMaskASM(t *testing.T) { + t.Parallel() + + testMask(t, "maskASM", mask) +} diff --git a/mask_test.go b/mask_test.go new file mode 100644 index 00000000..5c3d43c4 --- /dev/null +++ b/mask_test.go @@ -0,0 +1,73 @@ +package websocket + +import ( + "bytes" + "crypto/rand" + "encoding/binary" + "math/big" + "math/bits" + "testing" + + "nhooyr.io/websocket/internal/test/assert" +) + +func basicMask(b []byte, key uint32) uint32 { + for i := range b { + b[i] ^= byte(key) + key = bits.RotateLeft32(key, -8) + } + return key +} + +func basicMask2(b []byte, key uint32) uint32 { + keyb := binary.LittleEndian.AppendUint32(nil, key) + pos := 0 + for i := range b { + b[i] ^= keyb[pos&3] + pos++ + } + return bits.RotateLeft32(key, (pos&3)*-8) +} + +func TestMask(t *testing.T) { + t.Parallel() + + testMask(t, "basicMask", basicMask) + testMask(t, "maskGo", maskGo) + testMask(t, "basicMask2", basicMask2) +} + +func testMask(t *testing.T, name string, fn func(b []byte, key uint32) uint32) { + t.Run(name, func(t *testing.T) { + t.Parallel() + for i := 0; i < 9999; i++ { + keyb := make([]byte, 4) + _, err := rand.Read(keyb) + assert.Success(t, err) + key := binary.LittleEndian.Uint32(keyb) + + n, err := rand.Int(rand.Reader, big.NewInt(1<<16)) + assert.Success(t, err) + + b := make([]byte, 1+n.Int64()) + _, err = rand.Read(b) + assert.Success(t, err) + + b2 := make([]byte, len(b)) + copy(b2, b) + b3 := make([]byte, len(b)) + copy(b3, b) + + key2 := basicMask(b2, key) + key3 := fn(b3, key) + + if key2 != key3 { + t.Errorf("expected key %X but got %X", key2, key3) + } + if !bytes.Equal(b2, b3) { + t.Error("bad bytes") + return + } + } + }) +} From f62cef395d5228b955967e05a7e7cbf5a0ab8f93 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 22 Feb 2024 05:00:41 -0800 Subject: [PATCH 466/519] test.sh: Test assembly masking on arm64 --- ci/test.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/ci/test.sh b/ci/test.sh index 83bb9832..a3007614 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -11,6 +11,19 @@ cd -- "$(dirname "$0")/.." go test "$@" ./... ) +( + GOARCH=arm64 go test -c -o ./ci/out/websocket-arm64.test "$@" . + if [ "$#" -eq 0 ]; then + if [ "${CI-}" ]; then + sudo apt-get update + sudo apt-get install -y qemu-user-static + ln -s /usr/bin/qemu-aarch64-static /usr/local/bin/qemu-aarch64 + fi + qemu-aarch64 ./ci/out/websocket-arm64.test -test.run=TestMask + fi +) + + go install github.com/agnivade/wasmbrowsertest@latest go test --race --bench=. --timeout=1h --covermode=atomic --coverprofile=ci/out/coverage.prof --coverpkg=./... "$@" ./... sed -i.bak '/stringer\.go/d' ci/out/coverage.prof From 92acb74883ce505cd4eefd32841ef807de3e78f8 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 22 Feb 2024 05:06:05 -0800 Subject: [PATCH 467/519] internal/xcpu: Vendor golang.org/x/sys/cpu Standard library does this too. Unfortunate wish they just exposed it in the standard library. Perhaps we can isolate the specific code we need later. --- go.mod | 2 - go.sum | 2 - internal/examples/go.mod | 2 - internal/examples/go.sum | 2 - internal/xcpu/.gitattributes | 10 + internal/xcpu/.gitignore | 2 + internal/xcpu/README.md | 3 + internal/xcpu/asm_aix_ppc64.s | 17 ++ internal/xcpu/byteorder.go | 66 ++++++ internal/xcpu/cpu.go | 290 ++++++++++++++++++++++++++ internal/xcpu/cpu_aix.go | 33 +++ internal/xcpu/cpu_arm.go | 73 +++++++ internal/xcpu/cpu_arm64.go | 172 +++++++++++++++ internal/xcpu/cpu_arm64.s | 31 +++ internal/xcpu/cpu_gc_arm64.go | 11 + internal/xcpu/cpu_gc_s390x.go | 21 ++ internal/xcpu/cpu_gc_x86.go | 15 ++ internal/xcpu/cpu_gccgo_arm64.go | 11 + internal/xcpu/cpu_gccgo_s390x.go | 22 ++ internal/xcpu/cpu_gccgo_x86.c | 37 ++++ internal/xcpu/cpu_gccgo_x86.go | 31 +++ internal/xcpu/cpu_linux.go | 15 ++ internal/xcpu/cpu_linux_arm.go | 39 ++++ internal/xcpu/cpu_linux_arm64.go | 111 ++++++++++ internal/xcpu/cpu_linux_mips64x.go | 22 ++ internal/xcpu/cpu_linux_noinit.go | 9 + internal/xcpu/cpu_linux_ppc64x.go | 30 +++ internal/xcpu/cpu_linux_s390x.go | 40 ++++ internal/xcpu/cpu_loong64.go | 12 ++ internal/xcpu/cpu_mips64x.go | 15 ++ internal/xcpu/cpu_mipsx.go | 11 + internal/xcpu/cpu_netbsd_arm64.go | 173 +++++++++++++++ internal/xcpu/cpu_openbsd_arm64.go | 65 ++++++ internal/xcpu/cpu_openbsd_arm64.s | 11 + internal/xcpu/cpu_other_arm.go | 9 + internal/xcpu/cpu_other_arm64.go | 9 + internal/xcpu/cpu_other_mips64x.go | 11 + internal/xcpu/cpu_other_ppc64x.go | 12 ++ internal/xcpu/cpu_other_riscv64.go | 11 + internal/xcpu/cpu_ppc64x.go | 16 ++ internal/xcpu/cpu_riscv64.go | 11 + internal/xcpu/cpu_s390x.go | 172 +++++++++++++++ internal/xcpu/cpu_s390x.s | 57 +++++ internal/xcpu/cpu_wasm.go | 17 ++ internal/xcpu/cpu_x86.go | 151 ++++++++++++++ internal/xcpu/cpu_x86.s | 26 +++ internal/xcpu/cpu_zos.go | 10 + internal/xcpu/cpu_zos_s390x.go | 25 +++ internal/xcpu/endian_big.go | 10 + internal/xcpu/endian_little.go | 10 + internal/xcpu/hwcap_linux.go | 71 +++++++ internal/xcpu/parse.go | 43 ++++ internal/xcpu/proc_cpuinfo_linux.go | 53 +++++ internal/xcpu/runtime_auxv.go | 16 ++ internal/xcpu/runtime_auxv_go121.go | 18 ++ internal/xcpu/syscall_aix_gccgo.go | 26 +++ internal/xcpu/syscall_aix_ppc64_gc.go | 35 ++++ mask_asm.go | 4 +- mask_test.go | 46 ++-- 59 files changed, 2242 insertions(+), 33 deletions(-) create mode 100644 internal/xcpu/.gitattributes create mode 100644 internal/xcpu/.gitignore create mode 100644 internal/xcpu/README.md create mode 100644 internal/xcpu/asm_aix_ppc64.s create mode 100644 internal/xcpu/byteorder.go create mode 100644 internal/xcpu/cpu.go create mode 100644 internal/xcpu/cpu_aix.go create mode 100644 internal/xcpu/cpu_arm.go create mode 100644 internal/xcpu/cpu_arm64.go create mode 100644 internal/xcpu/cpu_arm64.s create mode 100644 internal/xcpu/cpu_gc_arm64.go create mode 100644 internal/xcpu/cpu_gc_s390x.go create mode 100644 internal/xcpu/cpu_gc_x86.go create mode 100644 internal/xcpu/cpu_gccgo_arm64.go create mode 100644 internal/xcpu/cpu_gccgo_s390x.go create mode 100644 internal/xcpu/cpu_gccgo_x86.c create mode 100644 internal/xcpu/cpu_gccgo_x86.go create mode 100644 internal/xcpu/cpu_linux.go create mode 100644 internal/xcpu/cpu_linux_arm.go create mode 100644 internal/xcpu/cpu_linux_arm64.go create mode 100644 internal/xcpu/cpu_linux_mips64x.go create mode 100644 internal/xcpu/cpu_linux_noinit.go create mode 100644 internal/xcpu/cpu_linux_ppc64x.go create mode 100644 internal/xcpu/cpu_linux_s390x.go create mode 100644 internal/xcpu/cpu_loong64.go create mode 100644 internal/xcpu/cpu_mips64x.go create mode 100644 internal/xcpu/cpu_mipsx.go create mode 100644 internal/xcpu/cpu_netbsd_arm64.go create mode 100644 internal/xcpu/cpu_openbsd_arm64.go create mode 100644 internal/xcpu/cpu_openbsd_arm64.s create mode 100644 internal/xcpu/cpu_other_arm.go create mode 100644 internal/xcpu/cpu_other_arm64.go create mode 100644 internal/xcpu/cpu_other_mips64x.go create mode 100644 internal/xcpu/cpu_other_ppc64x.go create mode 100644 internal/xcpu/cpu_other_riscv64.go create mode 100644 internal/xcpu/cpu_ppc64x.go create mode 100644 internal/xcpu/cpu_riscv64.go create mode 100644 internal/xcpu/cpu_s390x.go create mode 100644 internal/xcpu/cpu_s390x.s create mode 100644 internal/xcpu/cpu_wasm.go create mode 100644 internal/xcpu/cpu_x86.go create mode 100644 internal/xcpu/cpu_x86.s create mode 100644 internal/xcpu/cpu_zos.go create mode 100644 internal/xcpu/cpu_zos_s390x.go create mode 100644 internal/xcpu/endian_big.go create mode 100644 internal/xcpu/endian_little.go create mode 100644 internal/xcpu/hwcap_linux.go create mode 100644 internal/xcpu/parse.go create mode 100644 internal/xcpu/proc_cpuinfo_linux.go create mode 100644 internal/xcpu/runtime_auxv.go create mode 100644 internal/xcpu/runtime_auxv_go121.go create mode 100644 internal/xcpu/syscall_aix_gccgo.go create mode 100644 internal/xcpu/syscall_aix_ppc64_gc.go diff --git a/go.mod b/go.mod index c6ec72cc..715a9f7a 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,3 @@ module nhooyr.io/websocket go 1.19 - -require golang.org/x/sys v0.17.0 diff --git a/go.sum b/go.sum index 735d9a79..e69de29b 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +0,0 @@ -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/internal/examples/go.mod b/internal/examples/go.mod index 50695945..c98b81ce 100644 --- a/internal/examples/go.mod +++ b/internal/examples/go.mod @@ -8,5 +8,3 @@ require ( golang.org/x/time v0.3.0 nhooyr.io/websocket v0.0.0-00010101000000-000000000000 ) - -require golang.org/x/sys v0.17.0 // indirect diff --git a/internal/examples/go.sum b/internal/examples/go.sum index 06068548..f8a07e82 100644 --- a/internal/examples/go.sum +++ b/internal/examples/go.sum @@ -1,4 +1,2 @@ -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/internal/xcpu/.gitattributes b/internal/xcpu/.gitattributes new file mode 100644 index 00000000..d2f212e5 --- /dev/null +++ b/internal/xcpu/.gitattributes @@ -0,0 +1,10 @@ +# Treat all files in this repo as binary, with no git magic updating +# line endings. Windows users contributing to Go will need to use a +# modern version of git and editors capable of LF line endings. +# +# We'll prevent accidental CRLF line endings from entering the repo +# via the git-review gofmt checks. +# +# See golang.org/issue/9281 + +* -text diff --git a/internal/xcpu/.gitignore b/internal/xcpu/.gitignore new file mode 100644 index 00000000..5a9d62ef --- /dev/null +++ b/internal/xcpu/.gitignore @@ -0,0 +1,2 @@ +# Add no patterns to .gitignore except for files generated by the build. +last-change diff --git a/internal/xcpu/README.md b/internal/xcpu/README.md new file mode 100644 index 00000000..96a1a30f --- /dev/null +++ b/internal/xcpu/README.md @@ -0,0 +1,3 @@ +# cpu + +Vendored from https://github.com/golang/sys diff --git a/internal/xcpu/asm_aix_ppc64.s b/internal/xcpu/asm_aix_ppc64.s new file mode 100644 index 00000000..269e173c --- /dev/null +++ b/internal/xcpu/asm_aix_ppc64.s @@ -0,0 +1,17 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build gc + +#include "textflag.h" + +// +// System calls for ppc64, AIX are implemented in runtime/syscall_aix.go +// + +TEXT ·syscall6(SB),NOSPLIT,$0-88 + JMP syscall·syscall6(SB) + +TEXT ·rawSyscall6(SB),NOSPLIT,$0-88 + JMP syscall·rawSyscall6(SB) diff --git a/internal/xcpu/byteorder.go b/internal/xcpu/byteorder.go new file mode 100644 index 00000000..8f28d86c --- /dev/null +++ b/internal/xcpu/byteorder.go @@ -0,0 +1,66 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xcpu + +import ( + "runtime" +) + +// byteOrder is a subset of encoding/binary.ByteOrder. +type byteOrder interface { + Uint32([]byte) uint32 + Uint64([]byte) uint64 +} + +type littleEndian struct{} +type bigEndian struct{} + +func (littleEndian) Uint32(b []byte) uint32 { + _ = b[3] // bounds check hint to compiler; see golang.org/issue/14808 + return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24 +} + +func (littleEndian) Uint64(b []byte) uint64 { + _ = b[7] // bounds check hint to compiler; see golang.org/issue/14808 + return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 | + uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56 +} + +func (bigEndian) Uint32(b []byte) uint32 { + _ = b[3] // bounds check hint to compiler; see golang.org/issue/14808 + return uint32(b[3]) | uint32(b[2])<<8 | uint32(b[1])<<16 | uint32(b[0])<<24 +} + +func (bigEndian) Uint64(b []byte) uint64 { + _ = b[7] // bounds check hint to compiler; see golang.org/issue/14808 + return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 | + uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56 +} + +// hostByteOrder returns littleEndian on little-endian machines and +// bigEndian on big-endian machines. +func hostByteOrder() byteOrder { + switch runtime.GOARCH { + case "386", "amd64", "amd64p32", + "alpha", + "arm", "arm64", + "loong64", + "mipsle", "mips64le", "mips64p32le", + "nios2", + "ppc64le", + "riscv", "riscv64", + "sh": + return littleEndian{} + case "armbe", "arm64be", + "m68k", + "mips", "mips64", "mips64p32", + "ppc", "ppc64", + "s390", "s390x", + "shbe", + "sparc", "sparc64": + return bigEndian{} + } + panic("unknown architecture") +} diff --git a/internal/xcpu/cpu.go b/internal/xcpu/cpu.go new file mode 100644 index 00000000..5fc15019 --- /dev/null +++ b/internal/xcpu/cpu.go @@ -0,0 +1,290 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package cpu implements processor feature detection for +// various CPU architectures. +package xcpu + +import ( + "os" + "strings" +) + +// Initialized reports whether the CPU features were initialized. +// +// For some GOOS/GOARCH combinations initialization of the CPU features depends +// on reading an operating specific file, e.g. /proc/self/auxv on linux/arm +// Initialized will report false if reading the file fails. +var Initialized bool + +// CacheLinePad is used to pad structs to avoid false sharing. +type CacheLinePad struct{ _ [cacheLineSize]byte } + +// X86 contains the supported CPU features of the +// current X86/AMD64 platform. If the current platform +// is not X86/AMD64 then all feature flags are false. +// +// X86 is padded to avoid false sharing. Further the HasAVX +// and HasAVX2 are only set if the OS supports XMM and YMM +// registers in addition to the CPUID feature bit being set. +var X86 struct { + _ CacheLinePad + HasAES bool // AES hardware implementation (AES NI) + HasADX bool // Multi-precision add-carry instruction extensions + HasAVX bool // Advanced vector extension + HasAVX2 bool // Advanced vector extension 2 + HasAVX512 bool // Advanced vector extension 512 + HasAVX512F bool // Advanced vector extension 512 Foundation Instructions + HasAVX512CD bool // Advanced vector extension 512 Conflict Detection Instructions + HasAVX512ER bool // Advanced vector extension 512 Exponential and Reciprocal Instructions + HasAVX512PF bool // Advanced vector extension 512 Prefetch Instructions + HasAVX512VL bool // Advanced vector extension 512 Vector Length Extensions + HasAVX512BW bool // Advanced vector extension 512 Byte and Word Instructions + HasAVX512DQ bool // Advanced vector extension 512 Doubleword and Quadword Instructions + HasAVX512IFMA bool // Advanced vector extension 512 Integer Fused Multiply Add + HasAVX512VBMI bool // Advanced vector extension 512 Vector Byte Manipulation Instructions + HasAVX5124VNNIW bool // Advanced vector extension 512 Vector Neural Network Instructions Word variable precision + HasAVX5124FMAPS bool // Advanced vector extension 512 Fused Multiply Accumulation Packed Single precision + HasAVX512VPOPCNTDQ bool // Advanced vector extension 512 Double and quad word population count instructions + HasAVX512VPCLMULQDQ bool // Advanced vector extension 512 Vector carry-less multiply operations + HasAVX512VNNI bool // Advanced vector extension 512 Vector Neural Network Instructions + HasAVX512GFNI bool // Advanced vector extension 512 Galois field New Instructions + HasAVX512VAES bool // Advanced vector extension 512 Vector AES instructions + HasAVX512VBMI2 bool // Advanced vector extension 512 Vector Byte Manipulation Instructions 2 + HasAVX512BITALG bool // Advanced vector extension 512 Bit Algorithms + HasAVX512BF16 bool // Advanced vector extension 512 BFloat16 Instructions + HasAMXTile bool // Advanced Matrix Extension Tile instructions + HasAMXInt8 bool // Advanced Matrix Extension Int8 instructions + HasAMXBF16 bool // Advanced Matrix Extension BFloat16 instructions + HasBMI1 bool // Bit manipulation instruction set 1 + HasBMI2 bool // Bit manipulation instruction set 2 + HasCX16 bool // Compare and exchange 16 Bytes + HasERMS bool // Enhanced REP for MOVSB and STOSB + HasFMA bool // Fused-multiply-add instructions + HasOSXSAVE bool // OS supports XSAVE/XRESTOR for saving/restoring XMM registers. + HasPCLMULQDQ bool // PCLMULQDQ instruction - most often used for AES-GCM + HasPOPCNT bool // Hamming weight instruction POPCNT. + HasRDRAND bool // RDRAND instruction (on-chip random number generator) + HasRDSEED bool // RDSEED instruction (on-chip random number generator) + HasSSE2 bool // Streaming SIMD extension 2 (always available on amd64) + HasSSE3 bool // Streaming SIMD extension 3 + HasSSSE3 bool // Supplemental streaming SIMD extension 3 + HasSSE41 bool // Streaming SIMD extension 4 and 4.1 + HasSSE42 bool // Streaming SIMD extension 4 and 4.2 + _ CacheLinePad +} + +// ARM64 contains the supported CPU features of the +// current ARMv8(aarch64) platform. If the current platform +// is not arm64 then all feature flags are false. +var ARM64 struct { + _ CacheLinePad + HasFP bool // Floating-point instruction set (always available) + HasASIMD bool // Advanced SIMD (always available) + HasEVTSTRM bool // Event stream support + HasAES bool // AES hardware implementation + HasPMULL bool // Polynomial multiplication instruction set + HasSHA1 bool // SHA1 hardware implementation + HasSHA2 bool // SHA2 hardware implementation + HasCRC32 bool // CRC32 hardware implementation + HasATOMICS bool // Atomic memory operation instruction set + HasFPHP bool // Half precision floating-point instruction set + HasASIMDHP bool // Advanced SIMD half precision instruction set + HasCPUID bool // CPUID identification scheme registers + HasASIMDRDM bool // Rounding double multiply add/subtract instruction set + HasJSCVT bool // Javascript conversion from floating-point to integer + HasFCMA bool // Floating-point multiplication and addition of complex numbers + HasLRCPC bool // Release Consistent processor consistent support + HasDCPOP bool // Persistent memory support + HasSHA3 bool // SHA3 hardware implementation + HasSM3 bool // SM3 hardware implementation + HasSM4 bool // SM4 hardware implementation + HasASIMDDP bool // Advanced SIMD double precision instruction set + HasSHA512 bool // SHA512 hardware implementation + HasSVE bool // Scalable Vector Extensions + HasASIMDFHM bool // Advanced SIMD multiplication FP16 to FP32 + _ CacheLinePad +} + +// ARM contains the supported CPU features of the current ARM (32-bit) platform. +// All feature flags are false if: +// 1. the current platform is not arm, or +// 2. the current operating system is not Linux. +var ARM struct { + _ CacheLinePad + HasSWP bool // SWP instruction support + HasHALF bool // Half-word load and store support + HasTHUMB bool // ARM Thumb instruction set + Has26BIT bool // Address space limited to 26-bits + HasFASTMUL bool // 32-bit operand, 64-bit result multiplication support + HasFPA bool // Floating point arithmetic support + HasVFP bool // Vector floating point support + HasEDSP bool // DSP Extensions support + HasJAVA bool // Java instruction set + HasIWMMXT bool // Intel Wireless MMX technology support + HasCRUNCH bool // MaverickCrunch context switching and handling + HasTHUMBEE bool // Thumb EE instruction set + HasNEON bool // NEON instruction set + HasVFPv3 bool // Vector floating point version 3 support + HasVFPv3D16 bool // Vector floating point version 3 D8-D15 + HasTLS bool // Thread local storage support + HasVFPv4 bool // Vector floating point version 4 support + HasIDIVA bool // Integer divide instruction support in ARM mode + HasIDIVT bool // Integer divide instruction support in Thumb mode + HasVFPD32 bool // Vector floating point version 3 D15-D31 + HasLPAE bool // Large Physical Address Extensions + HasEVTSTRM bool // Event stream support + HasAES bool // AES hardware implementation + HasPMULL bool // Polynomial multiplication instruction set + HasSHA1 bool // SHA1 hardware implementation + HasSHA2 bool // SHA2 hardware implementation + HasCRC32 bool // CRC32 hardware implementation + _ CacheLinePad +} + +// MIPS64X contains the supported CPU features of the current mips64/mips64le +// platforms. If the current platform is not mips64/mips64le or the current +// operating system is not Linux then all feature flags are false. +var MIPS64X struct { + _ CacheLinePad + HasMSA bool // MIPS SIMD architecture + _ CacheLinePad +} + +// PPC64 contains the supported CPU features of the current ppc64/ppc64le platforms. +// If the current platform is not ppc64/ppc64le then all feature flags are false. +// +// For ppc64/ppc64le, it is safe to check only for ISA level starting on ISA v3.00, +// since there are no optional categories. There are some exceptions that also +// require kernel support to work (DARN, SCV), so there are feature bits for +// those as well. The struct is padded to avoid false sharing. +var PPC64 struct { + _ CacheLinePad + HasDARN bool // Hardware random number generator (requires kernel enablement) + HasSCV bool // Syscall vectored (requires kernel enablement) + IsPOWER8 bool // ISA v2.07 (POWER8) + IsPOWER9 bool // ISA v3.00 (POWER9), implies IsPOWER8 + _ CacheLinePad +} + +// S390X contains the supported CPU features of the current IBM Z +// (s390x) platform. If the current platform is not IBM Z then all +// feature flags are false. +// +// S390X is padded to avoid false sharing. Further HasVX is only set +// if the OS supports vector registers in addition to the STFLE +// feature bit being set. +var S390X struct { + _ CacheLinePad + HasZARCH bool // z/Architecture mode is active [mandatory] + HasSTFLE bool // store facility list extended + HasLDISP bool // long (20-bit) displacements + HasEIMM bool // 32-bit immediates + HasDFP bool // decimal floating point + HasETF3EH bool // ETF-3 enhanced + HasMSA bool // message security assist (CPACF) + HasAES bool // KM-AES{128,192,256} functions + HasAESCBC bool // KMC-AES{128,192,256} functions + HasAESCTR bool // KMCTR-AES{128,192,256} functions + HasAESGCM bool // KMA-GCM-AES{128,192,256} functions + HasGHASH bool // KIMD-GHASH function + HasSHA1 bool // K{I,L}MD-SHA-1 functions + HasSHA256 bool // K{I,L}MD-SHA-256 functions + HasSHA512 bool // K{I,L}MD-SHA-512 functions + HasSHA3 bool // K{I,L}MD-SHA3-{224,256,384,512} and K{I,L}MD-SHAKE-{128,256} functions + HasVX bool // vector facility + HasVXE bool // vector-enhancements facility 1 + _ CacheLinePad +} + +func init() { + archInit() + initOptions() + processOptions() +} + +// options contains the cpu debug options that can be used in GODEBUG. +// Options are arch dependent and are added by the arch specific initOptions functions. +// Features that are mandatory for the specific GOARCH should have the Required field set +// (e.g. SSE2 on amd64). +var options []option + +// Option names should be lower case. e.g. avx instead of AVX. +type option struct { + Name string + Feature *bool + Specified bool // whether feature value was specified in GODEBUG + Enable bool // whether feature should be enabled + Required bool // whether feature is mandatory and can not be disabled +} + +func processOptions() { + env := os.Getenv("GODEBUG") +field: + for env != "" { + field := "" + i := strings.IndexByte(env, ',') + if i < 0 { + field, env = env, "" + } else { + field, env = env[:i], env[i+1:] + } + if len(field) < 4 || field[:4] != "cpu." { + continue + } + i = strings.IndexByte(field, '=') + if i < 0 { + print("GODEBUG sys/cpu: no value specified for \"", field, "\"\n") + continue + } + key, value := field[4:i], field[i+1:] // e.g. "SSE2", "on" + + var enable bool + switch value { + case "on": + enable = true + case "off": + enable = false + default: + print("GODEBUG sys/cpu: value \"", value, "\" not supported for cpu option \"", key, "\"\n") + continue field + } + + if key == "all" { + for i := range options { + options[i].Specified = true + options[i].Enable = enable || options[i].Required + } + continue field + } + + for i := range options { + if options[i].Name == key { + options[i].Specified = true + options[i].Enable = enable + continue field + } + } + + print("GODEBUG sys/cpu: unknown cpu feature \"", key, "\"\n") + } + + for _, o := range options { + if !o.Specified { + continue + } + + if o.Enable && !*o.Feature { + print("GODEBUG sys/cpu: can not enable \"", o.Name, "\", missing CPU support\n") + continue + } + + if !o.Enable && o.Required { + print("GODEBUG sys/cpu: can not disable \"", o.Name, "\", required CPU feature\n") + continue + } + + *o.Feature = o.Enable + } +} diff --git a/internal/xcpu/cpu_aix.go b/internal/xcpu/cpu_aix.go new file mode 100644 index 00000000..5e6e2583 --- /dev/null +++ b/internal/xcpu/cpu_aix.go @@ -0,0 +1,33 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build aix + +package xcpu + +const ( + // getsystemcfg constants + _SC_IMPL = 2 + _IMPL_POWER8 = 0x10000 + _IMPL_POWER9 = 0x20000 +) + +func archInit() { + impl := getsystemcfg(_SC_IMPL) + if impl&_IMPL_POWER8 != 0 { + PPC64.IsPOWER8 = true + } + if impl&_IMPL_POWER9 != 0 { + PPC64.IsPOWER8 = true + PPC64.IsPOWER9 = true + } + + Initialized = true +} + +func getsystemcfg(label int) (n uint64) { + r0, _ := callgetsystemcfg(label) + n = uint64(r0) + return +} diff --git a/internal/xcpu/cpu_arm.go b/internal/xcpu/cpu_arm.go new file mode 100644 index 00000000..ff120458 --- /dev/null +++ b/internal/xcpu/cpu_arm.go @@ -0,0 +1,73 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xcpu + +const cacheLineSize = 32 + +// HWCAP/HWCAP2 bits. +// These are specific to Linux. +const ( + hwcap_SWP = 1 << 0 + hwcap_HALF = 1 << 1 + hwcap_THUMB = 1 << 2 + hwcap_26BIT = 1 << 3 + hwcap_FAST_MULT = 1 << 4 + hwcap_FPA = 1 << 5 + hwcap_VFP = 1 << 6 + hwcap_EDSP = 1 << 7 + hwcap_JAVA = 1 << 8 + hwcap_IWMMXT = 1 << 9 + hwcap_CRUNCH = 1 << 10 + hwcap_THUMBEE = 1 << 11 + hwcap_NEON = 1 << 12 + hwcap_VFPv3 = 1 << 13 + hwcap_VFPv3D16 = 1 << 14 + hwcap_TLS = 1 << 15 + hwcap_VFPv4 = 1 << 16 + hwcap_IDIVA = 1 << 17 + hwcap_IDIVT = 1 << 18 + hwcap_VFPD32 = 1 << 19 + hwcap_LPAE = 1 << 20 + hwcap_EVTSTRM = 1 << 21 + + hwcap2_AES = 1 << 0 + hwcap2_PMULL = 1 << 1 + hwcap2_SHA1 = 1 << 2 + hwcap2_SHA2 = 1 << 3 + hwcap2_CRC32 = 1 << 4 +) + +func initOptions() { + options = []option{ + {Name: "pmull", Feature: &ARM.HasPMULL}, + {Name: "sha1", Feature: &ARM.HasSHA1}, + {Name: "sha2", Feature: &ARM.HasSHA2}, + {Name: "swp", Feature: &ARM.HasSWP}, + {Name: "thumb", Feature: &ARM.HasTHUMB}, + {Name: "thumbee", Feature: &ARM.HasTHUMBEE}, + {Name: "tls", Feature: &ARM.HasTLS}, + {Name: "vfp", Feature: &ARM.HasVFP}, + {Name: "vfpd32", Feature: &ARM.HasVFPD32}, + {Name: "vfpv3", Feature: &ARM.HasVFPv3}, + {Name: "vfpv3d16", Feature: &ARM.HasVFPv3D16}, + {Name: "vfpv4", Feature: &ARM.HasVFPv4}, + {Name: "half", Feature: &ARM.HasHALF}, + {Name: "26bit", Feature: &ARM.Has26BIT}, + {Name: "fastmul", Feature: &ARM.HasFASTMUL}, + {Name: "fpa", Feature: &ARM.HasFPA}, + {Name: "edsp", Feature: &ARM.HasEDSP}, + {Name: "java", Feature: &ARM.HasJAVA}, + {Name: "iwmmxt", Feature: &ARM.HasIWMMXT}, + {Name: "crunch", Feature: &ARM.HasCRUNCH}, + {Name: "neon", Feature: &ARM.HasNEON}, + {Name: "idivt", Feature: &ARM.HasIDIVT}, + {Name: "idiva", Feature: &ARM.HasIDIVA}, + {Name: "lpae", Feature: &ARM.HasLPAE}, + {Name: "evtstrm", Feature: &ARM.HasEVTSTRM}, + {Name: "aes", Feature: &ARM.HasAES}, + {Name: "crc32", Feature: &ARM.HasCRC32}, + } + +} diff --git a/internal/xcpu/cpu_arm64.go b/internal/xcpu/cpu_arm64.go new file mode 100644 index 00000000..3d4113a5 --- /dev/null +++ b/internal/xcpu/cpu_arm64.go @@ -0,0 +1,172 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xcpu + +import "runtime" + +// cacheLineSize is used to prevent false sharing of cache lines. +// We choose 128 because Apple Silicon, a.k.a. M1, has 128-byte cache line size. +// It doesn't cost much and is much more future-proof. +const cacheLineSize = 128 + +func initOptions() { + options = []option{ + {Name: "fp", Feature: &ARM64.HasFP}, + {Name: "asimd", Feature: &ARM64.HasASIMD}, + {Name: "evstrm", Feature: &ARM64.HasEVTSTRM}, + {Name: "aes", Feature: &ARM64.HasAES}, + {Name: "fphp", Feature: &ARM64.HasFPHP}, + {Name: "jscvt", Feature: &ARM64.HasJSCVT}, + {Name: "lrcpc", Feature: &ARM64.HasLRCPC}, + {Name: "pmull", Feature: &ARM64.HasPMULL}, + {Name: "sha1", Feature: &ARM64.HasSHA1}, + {Name: "sha2", Feature: &ARM64.HasSHA2}, + {Name: "sha3", Feature: &ARM64.HasSHA3}, + {Name: "sha512", Feature: &ARM64.HasSHA512}, + {Name: "sm3", Feature: &ARM64.HasSM3}, + {Name: "sm4", Feature: &ARM64.HasSM4}, + {Name: "sve", Feature: &ARM64.HasSVE}, + {Name: "crc32", Feature: &ARM64.HasCRC32}, + {Name: "atomics", Feature: &ARM64.HasATOMICS}, + {Name: "asimdhp", Feature: &ARM64.HasASIMDHP}, + {Name: "cpuid", Feature: &ARM64.HasCPUID}, + {Name: "asimrdm", Feature: &ARM64.HasASIMDRDM}, + {Name: "fcma", Feature: &ARM64.HasFCMA}, + {Name: "dcpop", Feature: &ARM64.HasDCPOP}, + {Name: "asimddp", Feature: &ARM64.HasASIMDDP}, + {Name: "asimdfhm", Feature: &ARM64.HasASIMDFHM}, + } +} + +func archInit() { + switch runtime.GOOS { + case "freebsd": + readARM64Registers() + case "linux", "netbsd", "openbsd": + doinit() + default: + // Many platforms don't seem to allow reading these registers. + setMinimalFeatures() + } +} + +// setMinimalFeatures fakes the minimal ARM64 features expected by +// TestARM64minimalFeatures. +func setMinimalFeatures() { + ARM64.HasASIMD = true + ARM64.HasFP = true +} + +func readARM64Registers() { + Initialized = true + + parseARM64SystemRegisters(getisar0(), getisar1(), getpfr0()) +} + +func parseARM64SystemRegisters(isar0, isar1, pfr0 uint64) { + // ID_AA64ISAR0_EL1 + switch extractBits(isar0, 4, 7) { + case 1: + ARM64.HasAES = true + case 2: + ARM64.HasAES = true + ARM64.HasPMULL = true + } + + switch extractBits(isar0, 8, 11) { + case 1: + ARM64.HasSHA1 = true + } + + switch extractBits(isar0, 12, 15) { + case 1: + ARM64.HasSHA2 = true + case 2: + ARM64.HasSHA2 = true + ARM64.HasSHA512 = true + } + + switch extractBits(isar0, 16, 19) { + case 1: + ARM64.HasCRC32 = true + } + + switch extractBits(isar0, 20, 23) { + case 2: + ARM64.HasATOMICS = true + } + + switch extractBits(isar0, 28, 31) { + case 1: + ARM64.HasASIMDRDM = true + } + + switch extractBits(isar0, 32, 35) { + case 1: + ARM64.HasSHA3 = true + } + + switch extractBits(isar0, 36, 39) { + case 1: + ARM64.HasSM3 = true + } + + switch extractBits(isar0, 40, 43) { + case 1: + ARM64.HasSM4 = true + } + + switch extractBits(isar0, 44, 47) { + case 1: + ARM64.HasASIMDDP = true + } + + // ID_AA64ISAR1_EL1 + switch extractBits(isar1, 0, 3) { + case 1: + ARM64.HasDCPOP = true + } + + switch extractBits(isar1, 12, 15) { + case 1: + ARM64.HasJSCVT = true + } + + switch extractBits(isar1, 16, 19) { + case 1: + ARM64.HasFCMA = true + } + + switch extractBits(isar1, 20, 23) { + case 1: + ARM64.HasLRCPC = true + } + + // ID_AA64PFR0_EL1 + switch extractBits(pfr0, 16, 19) { + case 0: + ARM64.HasFP = true + case 1: + ARM64.HasFP = true + ARM64.HasFPHP = true + } + + switch extractBits(pfr0, 20, 23) { + case 0: + ARM64.HasASIMD = true + case 1: + ARM64.HasASIMD = true + ARM64.HasASIMDHP = true + } + + switch extractBits(pfr0, 32, 35) { + case 1: + ARM64.HasSVE = true + } +} + +func extractBits(data uint64, start, end uint) uint { + return (uint)(data>>start) & ((1 << (end - start + 1)) - 1) +} diff --git a/internal/xcpu/cpu_arm64.s b/internal/xcpu/cpu_arm64.s new file mode 100644 index 00000000..fcb9a388 --- /dev/null +++ b/internal/xcpu/cpu_arm64.s @@ -0,0 +1,31 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build gc + +#include "textflag.h" + +// func getisar0() uint64 +TEXT ·getisar0(SB),NOSPLIT,$0-8 + // get Instruction Set Attributes 0 into x0 + // mrs x0, ID_AA64ISAR0_EL1 = d5380600 + WORD $0xd5380600 + MOVD R0, ret+0(FP) + RET + +// func getisar1() uint64 +TEXT ·getisar1(SB),NOSPLIT,$0-8 + // get Instruction Set Attributes 1 into x0 + // mrs x0, ID_AA64ISAR1_EL1 = d5380620 + WORD $0xd5380620 + MOVD R0, ret+0(FP) + RET + +// func getpfr0() uint64 +TEXT ·getpfr0(SB),NOSPLIT,$0-8 + // get Processor Feature Register 0 into x0 + // mrs x0, ID_AA64PFR0_EL1 = d5380400 + WORD $0xd5380400 + MOVD R0, ret+0(FP) + RET diff --git a/internal/xcpu/cpu_gc_arm64.go b/internal/xcpu/cpu_gc_arm64.go new file mode 100644 index 00000000..26d3050d --- /dev/null +++ b/internal/xcpu/cpu_gc_arm64.go @@ -0,0 +1,11 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build gc + +package xcpu + +func getisar0() uint64 +func getisar1() uint64 +func getpfr0() uint64 diff --git a/internal/xcpu/cpu_gc_s390x.go b/internal/xcpu/cpu_gc_s390x.go new file mode 100644 index 00000000..34ca88b7 --- /dev/null +++ b/internal/xcpu/cpu_gc_s390x.go @@ -0,0 +1,21 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build gc + +package xcpu + +// haveAsmFunctions reports whether the other functions in this file can +// be safely called. +func haveAsmFunctions() bool { return true } + +// The following feature detection functions are defined in cpu_s390x.s. +// They are likely to be expensive to call so the results should be cached. +func stfle() facilityList +func kmQuery() queryResult +func kmcQuery() queryResult +func kmctrQuery() queryResult +func kmaQuery() queryResult +func kimdQuery() queryResult +func klmdQuery() queryResult diff --git a/internal/xcpu/cpu_gc_x86.go b/internal/xcpu/cpu_gc_x86.go new file mode 100644 index 00000000..9d6f61c2 --- /dev/null +++ b/internal/xcpu/cpu_gc_x86.go @@ -0,0 +1,15 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build (386 || amd64 || amd64p32) && gc + +package xcpu + +// cpuid is implemented in cpu_x86.s for gc compiler +// and in cpu_gccgo.c for gccgo. +func cpuid(eaxArg, ecxArg uint32) (eax, ebx, ecx, edx uint32) + +// xgetbv with ecx = 0 is implemented in cpu_x86.s for gc compiler +// and in cpu_gccgo.c for gccgo. +func xgetbv() (eax, edx uint32) diff --git a/internal/xcpu/cpu_gccgo_arm64.go b/internal/xcpu/cpu_gccgo_arm64.go new file mode 100644 index 00000000..d6c2a3a8 --- /dev/null +++ b/internal/xcpu/cpu_gccgo_arm64.go @@ -0,0 +1,11 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build gccgo + +package xcpu + +func getisar0() uint64 { return 0 } +func getisar1() uint64 { return 0 } +func getpfr0() uint64 { return 0 } diff --git a/internal/xcpu/cpu_gccgo_s390x.go b/internal/xcpu/cpu_gccgo_s390x.go new file mode 100644 index 00000000..4deec625 --- /dev/null +++ b/internal/xcpu/cpu_gccgo_s390x.go @@ -0,0 +1,22 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build gccgo + +package xcpu + +// haveAsmFunctions reports whether the other functions in this file can +// be safely called. +func haveAsmFunctions() bool { return false } + +// TODO(mundaym): the following feature detection functions are currently +// stubs. See https://golang.org/cl/162887 for how to fix this. +// They are likely to be expensive to call so the results should be cached. +func stfle() facilityList { panic("not implemented for gccgo") } +func kmQuery() queryResult { panic("not implemented for gccgo") } +func kmcQuery() queryResult { panic("not implemented for gccgo") } +func kmctrQuery() queryResult { panic("not implemented for gccgo") } +func kmaQuery() queryResult { panic("not implemented for gccgo") } +func kimdQuery() queryResult { panic("not implemented for gccgo") } +func klmdQuery() queryResult { panic("not implemented for gccgo") } diff --git a/internal/xcpu/cpu_gccgo_x86.c b/internal/xcpu/cpu_gccgo_x86.c new file mode 100644 index 00000000..3f73a05d --- /dev/null +++ b/internal/xcpu/cpu_gccgo_x86.c @@ -0,0 +1,37 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build (386 || amd64 || amd64p32) && gccgo + +#include +#include +#include + +// Need to wrap __get_cpuid_count because it's declared as static. +int +gccgoGetCpuidCount(uint32_t leaf, uint32_t subleaf, + uint32_t *eax, uint32_t *ebx, + uint32_t *ecx, uint32_t *edx) +{ + return __get_cpuid_count(leaf, subleaf, eax, ebx, ecx, edx); +} + +#pragma GCC diagnostic ignored "-Wunknown-pragmas" +#pragma GCC push_options +#pragma GCC target("xsave") +#pragma clang attribute push (__attribute__((target("xsave"))), apply_to=function) + +// xgetbv reads the contents of an XCR (Extended Control Register) +// specified in the ECX register into registers EDX:EAX. +// Currently, the only supported value for XCR is 0. +void +gccgoXgetbv(uint32_t *eax, uint32_t *edx) +{ + uint64_t v = _xgetbv(0); + *eax = v & 0xffffffff; + *edx = v >> 32; +} + +#pragma clang attribute pop +#pragma GCC pop_options diff --git a/internal/xcpu/cpu_gccgo_x86.go b/internal/xcpu/cpu_gccgo_x86.go new file mode 100644 index 00000000..e66c6ee9 --- /dev/null +++ b/internal/xcpu/cpu_gccgo_x86.go @@ -0,0 +1,31 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build (386 || amd64 || amd64p32) && gccgo + +package xcpu + +//extern gccgoGetCpuidCount +func gccgoGetCpuidCount(eaxArg, ecxArg uint32, eax, ebx, ecx, edx *uint32) + +func cpuid(eaxArg, ecxArg uint32) (eax, ebx, ecx, edx uint32) { + var a, b, c, d uint32 + gccgoGetCpuidCount(eaxArg, ecxArg, &a, &b, &c, &d) + return a, b, c, d +} + +//extern gccgoXgetbv +func gccgoXgetbv(eax, edx *uint32) + +func xgetbv() (eax, edx uint32) { + var a, d uint32 + gccgoXgetbv(&a, &d) + return a, d +} + +// gccgo doesn't build on Darwin, per: +// https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/gcc.rb#L76 +func darwinSupportsAVX512() bool { + return false +} diff --git a/internal/xcpu/cpu_linux.go b/internal/xcpu/cpu_linux.go new file mode 100644 index 00000000..10a48916 --- /dev/null +++ b/internal/xcpu/cpu_linux.go @@ -0,0 +1,15 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !386 && !amd64 && !amd64p32 && !arm64 + +package xcpu + +func archInit() { + if err := readHWCAP(); err != nil { + return + } + doinit() + Initialized = true +} diff --git a/internal/xcpu/cpu_linux_arm.go b/internal/xcpu/cpu_linux_arm.go new file mode 100644 index 00000000..28e32637 --- /dev/null +++ b/internal/xcpu/cpu_linux_arm.go @@ -0,0 +1,39 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xcpu + +func doinit() { + ARM.HasSWP = isSet(hwCap, hwcap_SWP) + ARM.HasHALF = isSet(hwCap, hwcap_HALF) + ARM.HasTHUMB = isSet(hwCap, hwcap_THUMB) + ARM.Has26BIT = isSet(hwCap, hwcap_26BIT) + ARM.HasFASTMUL = isSet(hwCap, hwcap_FAST_MULT) + ARM.HasFPA = isSet(hwCap, hwcap_FPA) + ARM.HasVFP = isSet(hwCap, hwcap_VFP) + ARM.HasEDSP = isSet(hwCap, hwcap_EDSP) + ARM.HasJAVA = isSet(hwCap, hwcap_JAVA) + ARM.HasIWMMXT = isSet(hwCap, hwcap_IWMMXT) + ARM.HasCRUNCH = isSet(hwCap, hwcap_CRUNCH) + ARM.HasTHUMBEE = isSet(hwCap, hwcap_THUMBEE) + ARM.HasNEON = isSet(hwCap, hwcap_NEON) + ARM.HasVFPv3 = isSet(hwCap, hwcap_VFPv3) + ARM.HasVFPv3D16 = isSet(hwCap, hwcap_VFPv3D16) + ARM.HasTLS = isSet(hwCap, hwcap_TLS) + ARM.HasVFPv4 = isSet(hwCap, hwcap_VFPv4) + ARM.HasIDIVA = isSet(hwCap, hwcap_IDIVA) + ARM.HasIDIVT = isSet(hwCap, hwcap_IDIVT) + ARM.HasVFPD32 = isSet(hwCap, hwcap_VFPD32) + ARM.HasLPAE = isSet(hwCap, hwcap_LPAE) + ARM.HasEVTSTRM = isSet(hwCap, hwcap_EVTSTRM) + ARM.HasAES = isSet(hwCap2, hwcap2_AES) + ARM.HasPMULL = isSet(hwCap2, hwcap2_PMULL) + ARM.HasSHA1 = isSet(hwCap2, hwcap2_SHA1) + ARM.HasSHA2 = isSet(hwCap2, hwcap2_SHA2) + ARM.HasCRC32 = isSet(hwCap2, hwcap2_CRC32) +} + +func isSet(hwc uint, value uint) bool { + return hwc&value != 0 +} diff --git a/internal/xcpu/cpu_linux_arm64.go b/internal/xcpu/cpu_linux_arm64.go new file mode 100644 index 00000000..481f450b --- /dev/null +++ b/internal/xcpu/cpu_linux_arm64.go @@ -0,0 +1,111 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xcpu + +import ( + "strings" + "syscall" +) + +// HWCAP/HWCAP2 bits. These are exposed by Linux. +const ( + hwcap_FP = 1 << 0 + hwcap_ASIMD = 1 << 1 + hwcap_EVTSTRM = 1 << 2 + hwcap_AES = 1 << 3 + hwcap_PMULL = 1 << 4 + hwcap_SHA1 = 1 << 5 + hwcap_SHA2 = 1 << 6 + hwcap_CRC32 = 1 << 7 + hwcap_ATOMICS = 1 << 8 + hwcap_FPHP = 1 << 9 + hwcap_ASIMDHP = 1 << 10 + hwcap_CPUID = 1 << 11 + hwcap_ASIMDRDM = 1 << 12 + hwcap_JSCVT = 1 << 13 + hwcap_FCMA = 1 << 14 + hwcap_LRCPC = 1 << 15 + hwcap_DCPOP = 1 << 16 + hwcap_SHA3 = 1 << 17 + hwcap_SM3 = 1 << 18 + hwcap_SM4 = 1 << 19 + hwcap_ASIMDDP = 1 << 20 + hwcap_SHA512 = 1 << 21 + hwcap_SVE = 1 << 22 + hwcap_ASIMDFHM = 1 << 23 +) + +// linuxKernelCanEmulateCPUID reports whether we're running +// on Linux 4.11+. Ideally we'd like to ask the question about +// whether the current kernel contains +// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=77c97b4ee21290f5f083173d957843b615abbff2 +// but the version number will have to do. +func linuxKernelCanEmulateCPUID() bool { + var un syscall.Utsname + syscall.Uname(&un) + var sb strings.Builder + for _, b := range un.Release[:] { + if b == 0 { + break + } + sb.WriteByte(byte(b)) + } + major, minor, _, ok := parseRelease(sb.String()) + return ok && (major > 4 || major == 4 && minor >= 11) +} + +func doinit() { + if err := readHWCAP(); err != nil { + // We failed to read /proc/self/auxv. This can happen if the binary has + // been given extra capabilities(7) with /bin/setcap. + // + // When this happens, we have two options. If the Linux kernel is new + // enough (4.11+), we can read the arm64 registers directly which'll + // trap into the kernel and then return back to userspace. + // + // But on older kernels, such as Linux 4.4.180 as used on many Synology + // devices, calling readARM64Registers (specifically getisar0) will + // cause a SIGILL and we'll die. So for older kernels, parse /proc/cpuinfo + // instead. + // + // See golang/go#57336. + if linuxKernelCanEmulateCPUID() { + readARM64Registers() + } else { + readLinuxProcCPUInfo() + } + return + } + + // HWCAP feature bits + ARM64.HasFP = isSet(hwCap, hwcap_FP) + ARM64.HasASIMD = isSet(hwCap, hwcap_ASIMD) + ARM64.HasEVTSTRM = isSet(hwCap, hwcap_EVTSTRM) + ARM64.HasAES = isSet(hwCap, hwcap_AES) + ARM64.HasPMULL = isSet(hwCap, hwcap_PMULL) + ARM64.HasSHA1 = isSet(hwCap, hwcap_SHA1) + ARM64.HasSHA2 = isSet(hwCap, hwcap_SHA2) + ARM64.HasCRC32 = isSet(hwCap, hwcap_CRC32) + ARM64.HasATOMICS = isSet(hwCap, hwcap_ATOMICS) + ARM64.HasFPHP = isSet(hwCap, hwcap_FPHP) + ARM64.HasASIMDHP = isSet(hwCap, hwcap_ASIMDHP) + ARM64.HasCPUID = isSet(hwCap, hwcap_CPUID) + ARM64.HasASIMDRDM = isSet(hwCap, hwcap_ASIMDRDM) + ARM64.HasJSCVT = isSet(hwCap, hwcap_JSCVT) + ARM64.HasFCMA = isSet(hwCap, hwcap_FCMA) + ARM64.HasLRCPC = isSet(hwCap, hwcap_LRCPC) + ARM64.HasDCPOP = isSet(hwCap, hwcap_DCPOP) + ARM64.HasSHA3 = isSet(hwCap, hwcap_SHA3) + ARM64.HasSM3 = isSet(hwCap, hwcap_SM3) + ARM64.HasSM4 = isSet(hwCap, hwcap_SM4) + ARM64.HasASIMDDP = isSet(hwCap, hwcap_ASIMDDP) + ARM64.HasSHA512 = isSet(hwCap, hwcap_SHA512) + ARM64.HasSVE = isSet(hwCap, hwcap_SVE) + ARM64.HasASIMDFHM = isSet(hwCap, hwcap_ASIMDFHM) +} + +func isSet(hwc uint, value uint) bool { + return hwc&value != 0 +} diff --git a/internal/xcpu/cpu_linux_mips64x.go b/internal/xcpu/cpu_linux_mips64x.go new file mode 100644 index 00000000..15fdee9c --- /dev/null +++ b/internal/xcpu/cpu_linux_mips64x.go @@ -0,0 +1,22 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build linux && (mips64 || mips64le) + +package xcpu + +// HWCAP bits. These are exposed by the Linux kernel 5.4. +const ( + // CPU features + hwcap_MIPS_MSA = 1 << 1 +) + +func doinit() { + // HWCAP feature bits + MIPS64X.HasMSA = isSet(hwCap, hwcap_MIPS_MSA) +} + +func isSet(hwc uint, value uint) bool { + return hwc&value != 0 +} diff --git a/internal/xcpu/cpu_linux_noinit.go b/internal/xcpu/cpu_linux_noinit.go new file mode 100644 index 00000000..878e56fb --- /dev/null +++ b/internal/xcpu/cpu_linux_noinit.go @@ -0,0 +1,9 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build linux && !arm && !arm64 && !mips64 && !mips64le && !ppc64 && !ppc64le && !s390x + +package xcpu + +func doinit() {} diff --git a/internal/xcpu/cpu_linux_ppc64x.go b/internal/xcpu/cpu_linux_ppc64x.go new file mode 100644 index 00000000..6a8ea12a --- /dev/null +++ b/internal/xcpu/cpu_linux_ppc64x.go @@ -0,0 +1,30 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build linux && (ppc64 || ppc64le) + +package xcpu + +// HWCAP/HWCAP2 bits. These are exposed by the kernel. +const ( + // ISA Level + _PPC_FEATURE2_ARCH_2_07 = 0x80000000 + _PPC_FEATURE2_ARCH_3_00 = 0x00800000 + + // CPU features + _PPC_FEATURE2_DARN = 0x00200000 + _PPC_FEATURE2_SCV = 0x00100000 +) + +func doinit() { + // HWCAP2 feature bits + PPC64.IsPOWER8 = isSet(hwCap2, _PPC_FEATURE2_ARCH_2_07) + PPC64.IsPOWER9 = isSet(hwCap2, _PPC_FEATURE2_ARCH_3_00) + PPC64.HasDARN = isSet(hwCap2, _PPC_FEATURE2_DARN) + PPC64.HasSCV = isSet(hwCap2, _PPC_FEATURE2_SCV) +} + +func isSet(hwc uint, value uint) bool { + return hwc&value != 0 +} diff --git a/internal/xcpu/cpu_linux_s390x.go b/internal/xcpu/cpu_linux_s390x.go new file mode 100644 index 00000000..ff0ca7f4 --- /dev/null +++ b/internal/xcpu/cpu_linux_s390x.go @@ -0,0 +1,40 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xcpu + +const ( + // bit mask values from /usr/include/bits/hwcap.h + hwcap_ZARCH = 2 + hwcap_STFLE = 4 + hwcap_MSA = 8 + hwcap_LDISP = 16 + hwcap_EIMM = 32 + hwcap_DFP = 64 + hwcap_ETF3EH = 256 + hwcap_VX = 2048 + hwcap_VXE = 8192 +) + +func initS390Xbase() { + // test HWCAP bit vector + has := func(featureMask uint) bool { + return hwCap&featureMask == featureMask + } + + // mandatory + S390X.HasZARCH = has(hwcap_ZARCH) + + // optional + S390X.HasSTFLE = has(hwcap_STFLE) + S390X.HasLDISP = has(hwcap_LDISP) + S390X.HasEIMM = has(hwcap_EIMM) + S390X.HasETF3EH = has(hwcap_ETF3EH) + S390X.HasDFP = has(hwcap_DFP) + S390X.HasMSA = has(hwcap_MSA) + S390X.HasVX = has(hwcap_VX) + if S390X.HasVX { + S390X.HasVXE = has(hwcap_VXE) + } +} diff --git a/internal/xcpu/cpu_loong64.go b/internal/xcpu/cpu_loong64.go new file mode 100644 index 00000000..fdb21c60 --- /dev/null +++ b/internal/xcpu/cpu_loong64.go @@ -0,0 +1,12 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build loong64 + +package xcpu + +const cacheLineSize = 64 + +func initOptions() { +} diff --git a/internal/xcpu/cpu_mips64x.go b/internal/xcpu/cpu_mips64x.go new file mode 100644 index 00000000..447fee98 --- /dev/null +++ b/internal/xcpu/cpu_mips64x.go @@ -0,0 +1,15 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build mips64 || mips64le + +package xcpu + +const cacheLineSize = 32 + +func initOptions() { + options = []option{ + {Name: "msa", Feature: &MIPS64X.HasMSA}, + } +} diff --git a/internal/xcpu/cpu_mipsx.go b/internal/xcpu/cpu_mipsx.go new file mode 100644 index 00000000..6efa1917 --- /dev/null +++ b/internal/xcpu/cpu_mipsx.go @@ -0,0 +1,11 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build mips || mipsle + +package xcpu + +const cacheLineSize = 32 + +func initOptions() {} diff --git a/internal/xcpu/cpu_netbsd_arm64.go b/internal/xcpu/cpu_netbsd_arm64.go new file mode 100644 index 00000000..b84b4408 --- /dev/null +++ b/internal/xcpu/cpu_netbsd_arm64.go @@ -0,0 +1,173 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xcpu + +import ( + "syscall" + "unsafe" +) + +// Minimal copy of functionality from x/sys/unix so the cpu package can call +// sysctl without depending on x/sys/unix. + +const ( + _CTL_QUERY = -2 + + _SYSCTL_VERS_1 = 0x1000000 +) + +var _zero uintptr + +func sysctl(mib []int32, old *byte, oldlen *uintptr, new *byte, newlen uintptr) (err error) { + var _p0 unsafe.Pointer + if len(mib) > 0 { + _p0 = unsafe.Pointer(&mib[0]) + } else { + _p0 = unsafe.Pointer(&_zero) + } + _, _, errno := syscall.Syscall6( + syscall.SYS___SYSCTL, + uintptr(_p0), + uintptr(len(mib)), + uintptr(unsafe.Pointer(old)), + uintptr(unsafe.Pointer(oldlen)), + uintptr(unsafe.Pointer(new)), + uintptr(newlen)) + if errno != 0 { + return errno + } + return nil +} + +type sysctlNode struct { + Flags uint32 + Num int32 + Name [32]int8 + Ver uint32 + __rsvd uint32 + Un [16]byte + _sysctl_size [8]byte + _sysctl_func [8]byte + _sysctl_parent [8]byte + _sysctl_desc [8]byte +} + +func sysctlNodes(mib []int32) ([]sysctlNode, error) { + var olen uintptr + + // Get a list of all sysctl nodes below the given MIB by performing + // a sysctl for the given MIB with CTL_QUERY appended. + mib = append(mib, _CTL_QUERY) + qnode := sysctlNode{Flags: _SYSCTL_VERS_1} + qp := (*byte)(unsafe.Pointer(&qnode)) + sz := unsafe.Sizeof(qnode) + if err := sysctl(mib, nil, &olen, qp, sz); err != nil { + return nil, err + } + + // Now that we know the size, get the actual nodes. + nodes := make([]sysctlNode, olen/sz) + np := (*byte)(unsafe.Pointer(&nodes[0])) + if err := sysctl(mib, np, &olen, qp, sz); err != nil { + return nil, err + } + + return nodes, nil +} + +func nametomib(name string) ([]int32, error) { + // Split name into components. + var parts []string + last := 0 + for i := 0; i < len(name); i++ { + if name[i] == '.' { + parts = append(parts, name[last:i]) + last = i + 1 + } + } + parts = append(parts, name[last:]) + + mib := []int32{} + // Discover the nodes and construct the MIB OID. + for partno, part := range parts { + nodes, err := sysctlNodes(mib) + if err != nil { + return nil, err + } + for _, node := range nodes { + n := make([]byte, 0) + for i := range node.Name { + if node.Name[i] != 0 { + n = append(n, byte(node.Name[i])) + } + } + if string(n) == part { + mib = append(mib, int32(node.Num)) + break + } + } + if len(mib) != partno+1 { + return nil, err + } + } + + return mib, nil +} + +// aarch64SysctlCPUID is struct aarch64_sysctl_cpu_id from NetBSD's +type aarch64SysctlCPUID struct { + midr uint64 /* Main ID Register */ + revidr uint64 /* Revision ID Register */ + mpidr uint64 /* Multiprocessor Affinity Register */ + aa64dfr0 uint64 /* A64 Debug Feature Register 0 */ + aa64dfr1 uint64 /* A64 Debug Feature Register 1 */ + aa64isar0 uint64 /* A64 Instruction Set Attribute Register 0 */ + aa64isar1 uint64 /* A64 Instruction Set Attribute Register 1 */ + aa64mmfr0 uint64 /* A64 Memory Model Feature Register 0 */ + aa64mmfr1 uint64 /* A64 Memory Model Feature Register 1 */ + aa64mmfr2 uint64 /* A64 Memory Model Feature Register 2 */ + aa64pfr0 uint64 /* A64 Processor Feature Register 0 */ + aa64pfr1 uint64 /* A64 Processor Feature Register 1 */ + aa64zfr0 uint64 /* A64 SVE Feature ID Register 0 */ + mvfr0 uint32 /* Media and VFP Feature Register 0 */ + mvfr1 uint32 /* Media and VFP Feature Register 1 */ + mvfr2 uint32 /* Media and VFP Feature Register 2 */ + pad uint32 + clidr uint64 /* Cache Level ID Register */ + ctr uint64 /* Cache Type Register */ +} + +func sysctlCPUID(name string) (*aarch64SysctlCPUID, error) { + mib, err := nametomib(name) + if err != nil { + return nil, err + } + + out := aarch64SysctlCPUID{} + n := unsafe.Sizeof(out) + _, _, errno := syscall.Syscall6( + syscall.SYS___SYSCTL, + uintptr(unsafe.Pointer(&mib[0])), + uintptr(len(mib)), + uintptr(unsafe.Pointer(&out)), + uintptr(unsafe.Pointer(&n)), + uintptr(0), + uintptr(0)) + if errno != 0 { + return nil, errno + } + return &out, nil +} + +func doinit() { + cpuid, err := sysctlCPUID("machdep.cpu0.cpu_id") + if err != nil { + setMinimalFeatures() + return + } + parseARM64SystemRegisters(cpuid.aa64isar0, cpuid.aa64isar1, cpuid.aa64pfr0) + + Initialized = true +} diff --git a/internal/xcpu/cpu_openbsd_arm64.go b/internal/xcpu/cpu_openbsd_arm64.go new file mode 100644 index 00000000..2459a486 --- /dev/null +++ b/internal/xcpu/cpu_openbsd_arm64.go @@ -0,0 +1,65 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xcpu + +import ( + "syscall" + "unsafe" +) + +// Minimal copy of functionality from x/sys/unix so the cpu package can call +// sysctl without depending on x/sys/unix. + +const ( + // From OpenBSD's sys/sysctl.h. + _CTL_MACHDEP = 7 + + // From OpenBSD's machine/cpu.h. + _CPU_ID_AA64ISAR0 = 2 + _CPU_ID_AA64ISAR1 = 3 +) + +// Implemented in the runtime package (runtime/sys_openbsd3.go) +func syscall_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno) + +//go:linkname syscall_syscall6 syscall.syscall6 + +func sysctl(mib []uint32, old *byte, oldlen *uintptr, new *byte, newlen uintptr) (err error) { + _, _, errno := syscall_syscall6(libc_sysctl_trampoline_addr, uintptr(unsafe.Pointer(&mib[0])), uintptr(len(mib)), uintptr(unsafe.Pointer(old)), uintptr(unsafe.Pointer(oldlen)), uintptr(unsafe.Pointer(new)), uintptr(newlen)) + if errno != 0 { + return errno + } + return nil +} + +var libc_sysctl_trampoline_addr uintptr + +//go:cgo_import_dynamic libc_sysctl sysctl "libc.so" + +func sysctlUint64(mib []uint32) (uint64, bool) { + var out uint64 + nout := unsafe.Sizeof(out) + if err := sysctl(mib, (*byte)(unsafe.Pointer(&out)), &nout, nil, 0); err != nil { + return 0, false + } + return out, true +} + +func doinit() { + setMinimalFeatures() + + // Get ID_AA64ISAR0 and ID_AA64ISAR1 from sysctl. + isar0, ok := sysctlUint64([]uint32{_CTL_MACHDEP, _CPU_ID_AA64ISAR0}) + if !ok { + return + } + isar1, ok := sysctlUint64([]uint32{_CTL_MACHDEP, _CPU_ID_AA64ISAR1}) + if !ok { + return + } + parseARM64SystemRegisters(isar0, isar1, 0) + + Initialized = true +} diff --git a/internal/xcpu/cpu_openbsd_arm64.s b/internal/xcpu/cpu_openbsd_arm64.s new file mode 100644 index 00000000..054ba05d --- /dev/null +++ b/internal/xcpu/cpu_openbsd_arm64.s @@ -0,0 +1,11 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +#include "textflag.h" + +TEXT libc_sysctl_trampoline<>(SB),NOSPLIT,$0-0 + JMP libc_sysctl(SB) + +GLOBL ·libc_sysctl_trampoline_addr(SB), RODATA, $8 +DATA ·libc_sysctl_trampoline_addr(SB)/8, $libc_sysctl_trampoline<>(SB) diff --git a/internal/xcpu/cpu_other_arm.go b/internal/xcpu/cpu_other_arm.go new file mode 100644 index 00000000..e3247948 --- /dev/null +++ b/internal/xcpu/cpu_other_arm.go @@ -0,0 +1,9 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !linux && arm + +package xcpu + +func archInit() {} diff --git a/internal/xcpu/cpu_other_arm64.go b/internal/xcpu/cpu_other_arm64.go new file mode 100644 index 00000000..5257a0b6 --- /dev/null +++ b/internal/xcpu/cpu_other_arm64.go @@ -0,0 +1,9 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !linux && !netbsd && !openbsd && arm64 + +package xcpu + +func doinit() {} diff --git a/internal/xcpu/cpu_other_mips64x.go b/internal/xcpu/cpu_other_mips64x.go new file mode 100644 index 00000000..b1ddc9d5 --- /dev/null +++ b/internal/xcpu/cpu_other_mips64x.go @@ -0,0 +1,11 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !linux && (mips64 || mips64le) + +package xcpu + +func archInit() { + Initialized = true +} diff --git a/internal/xcpu/cpu_other_ppc64x.go b/internal/xcpu/cpu_other_ppc64x.go new file mode 100644 index 00000000..00a08baa --- /dev/null +++ b/internal/xcpu/cpu_other_ppc64x.go @@ -0,0 +1,12 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !aix && !linux && (ppc64 || ppc64le) + +package xcpu + +func archInit() { + PPC64.IsPOWER8 = true + Initialized = true +} diff --git a/internal/xcpu/cpu_other_riscv64.go b/internal/xcpu/cpu_other_riscv64.go new file mode 100644 index 00000000..7f8fd1fc --- /dev/null +++ b/internal/xcpu/cpu_other_riscv64.go @@ -0,0 +1,11 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !linux && riscv64 + +package xcpu + +func archInit() { + Initialized = true +} diff --git a/internal/xcpu/cpu_ppc64x.go b/internal/xcpu/cpu_ppc64x.go new file mode 100644 index 00000000..22afeec2 --- /dev/null +++ b/internal/xcpu/cpu_ppc64x.go @@ -0,0 +1,16 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build ppc64 || ppc64le + +package xcpu + +const cacheLineSize = 128 + +func initOptions() { + options = []option{ + {Name: "darn", Feature: &PPC64.HasDARN}, + {Name: "scv", Feature: &PPC64.HasSCV}, + } +} diff --git a/internal/xcpu/cpu_riscv64.go b/internal/xcpu/cpu_riscv64.go new file mode 100644 index 00000000..28e57b68 --- /dev/null +++ b/internal/xcpu/cpu_riscv64.go @@ -0,0 +1,11 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build riscv64 + +package xcpu + +const cacheLineSize = 64 + +func initOptions() {} diff --git a/internal/xcpu/cpu_s390x.go b/internal/xcpu/cpu_s390x.go new file mode 100644 index 00000000..e85a8c5d --- /dev/null +++ b/internal/xcpu/cpu_s390x.go @@ -0,0 +1,172 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xcpu + +const cacheLineSize = 256 + +func initOptions() { + options = []option{ + {Name: "zarch", Feature: &S390X.HasZARCH, Required: true}, + {Name: "stfle", Feature: &S390X.HasSTFLE, Required: true}, + {Name: "ldisp", Feature: &S390X.HasLDISP, Required: true}, + {Name: "eimm", Feature: &S390X.HasEIMM, Required: true}, + {Name: "dfp", Feature: &S390X.HasDFP}, + {Name: "etf3eh", Feature: &S390X.HasETF3EH}, + {Name: "msa", Feature: &S390X.HasMSA}, + {Name: "aes", Feature: &S390X.HasAES}, + {Name: "aescbc", Feature: &S390X.HasAESCBC}, + {Name: "aesctr", Feature: &S390X.HasAESCTR}, + {Name: "aesgcm", Feature: &S390X.HasAESGCM}, + {Name: "ghash", Feature: &S390X.HasGHASH}, + {Name: "sha1", Feature: &S390X.HasSHA1}, + {Name: "sha256", Feature: &S390X.HasSHA256}, + {Name: "sha3", Feature: &S390X.HasSHA3}, + {Name: "sha512", Feature: &S390X.HasSHA512}, + {Name: "vx", Feature: &S390X.HasVX}, + {Name: "vxe", Feature: &S390X.HasVXE}, + } +} + +// bitIsSet reports whether the bit at index is set. The bit index +// is in big endian order, so bit index 0 is the leftmost bit. +func bitIsSet(bits []uint64, index uint) bool { + return bits[index/64]&((1<<63)>>(index%64)) != 0 +} + +// facility is a bit index for the named facility. +type facility uint8 + +const ( + // mandatory facilities + zarch facility = 1 // z architecture mode is active + stflef facility = 7 // store-facility-list-extended + ldisp facility = 18 // long-displacement + eimm facility = 21 // extended-immediate + + // miscellaneous facilities + dfp facility = 42 // decimal-floating-point + etf3eh facility = 30 // extended-translation 3 enhancement + + // cryptography facilities + msa facility = 17 // message-security-assist + msa3 facility = 76 // message-security-assist extension 3 + msa4 facility = 77 // message-security-assist extension 4 + msa5 facility = 57 // message-security-assist extension 5 + msa8 facility = 146 // message-security-assist extension 8 + msa9 facility = 155 // message-security-assist extension 9 + + // vector facilities + vx facility = 129 // vector facility + vxe facility = 135 // vector-enhancements 1 + vxe2 facility = 148 // vector-enhancements 2 +) + +// facilityList contains the result of an STFLE call. +// Bits are numbered in big endian order so the +// leftmost bit (the MSB) is at index 0. +type facilityList struct { + bits [4]uint64 +} + +// Has reports whether the given facilities are present. +func (s *facilityList) Has(fs ...facility) bool { + if len(fs) == 0 { + panic("no facility bits provided") + } + for _, f := range fs { + if !bitIsSet(s.bits[:], uint(f)) { + return false + } + } + return true +} + +// function is the code for the named cryptographic function. +type function uint8 + +const ( + // KM{,A,C,CTR} function codes + aes128 function = 18 // AES-128 + aes192 function = 19 // AES-192 + aes256 function = 20 // AES-256 + + // K{I,L}MD function codes + sha1 function = 1 // SHA-1 + sha256 function = 2 // SHA-256 + sha512 function = 3 // SHA-512 + sha3_224 function = 32 // SHA3-224 + sha3_256 function = 33 // SHA3-256 + sha3_384 function = 34 // SHA3-384 + sha3_512 function = 35 // SHA3-512 + shake128 function = 36 // SHAKE-128 + shake256 function = 37 // SHAKE-256 + + // KLMD function codes + ghash function = 65 // GHASH +) + +// queryResult contains the result of a Query function +// call. Bits are numbered in big endian order so the +// leftmost bit (the MSB) is at index 0. +type queryResult struct { + bits [2]uint64 +} + +// Has reports whether the given functions are present. +func (q *queryResult) Has(fns ...function) bool { + if len(fns) == 0 { + panic("no function codes provided") + } + for _, f := range fns { + if !bitIsSet(q.bits[:], uint(f)) { + return false + } + } + return true +} + +func doinit() { + initS390Xbase() + + // We need implementations of stfle, km and so on + // to detect cryptographic features. + if !haveAsmFunctions() { + return + } + + // optional cryptographic functions + if S390X.HasMSA { + aes := []function{aes128, aes192, aes256} + + // cipher message + km, kmc := kmQuery(), kmcQuery() + S390X.HasAES = km.Has(aes...) + S390X.HasAESCBC = kmc.Has(aes...) + if S390X.HasSTFLE { + facilities := stfle() + if facilities.Has(msa4) { + kmctr := kmctrQuery() + S390X.HasAESCTR = kmctr.Has(aes...) + } + if facilities.Has(msa8) { + kma := kmaQuery() + S390X.HasAESGCM = kma.Has(aes...) + } + } + + // compute message digest + kimd := kimdQuery() // intermediate (no padding) + klmd := klmdQuery() // last (padding) + S390X.HasSHA1 = kimd.Has(sha1) && klmd.Has(sha1) + S390X.HasSHA256 = kimd.Has(sha256) && klmd.Has(sha256) + S390X.HasSHA512 = kimd.Has(sha512) && klmd.Has(sha512) + S390X.HasGHASH = kimd.Has(ghash) // KLMD-GHASH does not exist + sha3 := []function{ + sha3_224, sha3_256, sha3_384, sha3_512, + shake128, shake256, + } + S390X.HasSHA3 = kimd.Has(sha3...) && klmd.Has(sha3...) + } +} diff --git a/internal/xcpu/cpu_s390x.s b/internal/xcpu/cpu_s390x.s new file mode 100644 index 00000000..1fb4b701 --- /dev/null +++ b/internal/xcpu/cpu_s390x.s @@ -0,0 +1,57 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build gc + +#include "textflag.h" + +// func stfle() facilityList +TEXT ·stfle(SB), NOSPLIT|NOFRAME, $0-32 + MOVD $ret+0(FP), R1 + MOVD $3, R0 // last doubleword index to store + XC $32, (R1), (R1) // clear 4 doublewords (32 bytes) + WORD $0xb2b01000 // store facility list extended (STFLE) + RET + +// func kmQuery() queryResult +TEXT ·kmQuery(SB), NOSPLIT|NOFRAME, $0-16 + MOVD $0, R0 // set function code to 0 (KM-Query) + MOVD $ret+0(FP), R1 // address of 16-byte return value + WORD $0xB92E0024 // cipher message (KM) + RET + +// func kmcQuery() queryResult +TEXT ·kmcQuery(SB), NOSPLIT|NOFRAME, $0-16 + MOVD $0, R0 // set function code to 0 (KMC-Query) + MOVD $ret+0(FP), R1 // address of 16-byte return value + WORD $0xB92F0024 // cipher message with chaining (KMC) + RET + +// func kmctrQuery() queryResult +TEXT ·kmctrQuery(SB), NOSPLIT|NOFRAME, $0-16 + MOVD $0, R0 // set function code to 0 (KMCTR-Query) + MOVD $ret+0(FP), R1 // address of 16-byte return value + WORD $0xB92D4024 // cipher message with counter (KMCTR) + RET + +// func kmaQuery() queryResult +TEXT ·kmaQuery(SB), NOSPLIT|NOFRAME, $0-16 + MOVD $0, R0 // set function code to 0 (KMA-Query) + MOVD $ret+0(FP), R1 // address of 16-byte return value + WORD $0xb9296024 // cipher message with authentication (KMA) + RET + +// func kimdQuery() queryResult +TEXT ·kimdQuery(SB), NOSPLIT|NOFRAME, $0-16 + MOVD $0, R0 // set function code to 0 (KIMD-Query) + MOVD $ret+0(FP), R1 // address of 16-byte return value + WORD $0xB93E0024 // compute intermediate message digest (KIMD) + RET + +// func klmdQuery() queryResult +TEXT ·klmdQuery(SB), NOSPLIT|NOFRAME, $0-16 + MOVD $0, R0 // set function code to 0 (KLMD-Query) + MOVD $ret+0(FP), R1 // address of 16-byte return value + WORD $0xB93F0024 // compute last message digest (KLMD) + RET diff --git a/internal/xcpu/cpu_wasm.go b/internal/xcpu/cpu_wasm.go new file mode 100644 index 00000000..230aaab4 --- /dev/null +++ b/internal/xcpu/cpu_wasm.go @@ -0,0 +1,17 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build wasm + +package xcpu + +// We're compiling the cpu package for an unknown (software-abstracted) CPU. +// Make CacheLinePad an empty struct and hope that the usual struct alignment +// rules are good enough. + +const cacheLineSize = 0 + +func initOptions() {} + +func archInit() {} diff --git a/internal/xcpu/cpu_x86.go b/internal/xcpu/cpu_x86.go new file mode 100644 index 00000000..d2f83468 --- /dev/null +++ b/internal/xcpu/cpu_x86.go @@ -0,0 +1,151 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build 386 || amd64 || amd64p32 + +package xcpu + +import "runtime" + +const cacheLineSize = 64 + +func initOptions() { + options = []option{ + {Name: "adx", Feature: &X86.HasADX}, + {Name: "aes", Feature: &X86.HasAES}, + {Name: "avx", Feature: &X86.HasAVX}, + {Name: "avx2", Feature: &X86.HasAVX2}, + {Name: "avx512", Feature: &X86.HasAVX512}, + {Name: "avx512f", Feature: &X86.HasAVX512F}, + {Name: "avx512cd", Feature: &X86.HasAVX512CD}, + {Name: "avx512er", Feature: &X86.HasAVX512ER}, + {Name: "avx512pf", Feature: &X86.HasAVX512PF}, + {Name: "avx512vl", Feature: &X86.HasAVX512VL}, + {Name: "avx512bw", Feature: &X86.HasAVX512BW}, + {Name: "avx512dq", Feature: &X86.HasAVX512DQ}, + {Name: "avx512ifma", Feature: &X86.HasAVX512IFMA}, + {Name: "avx512vbmi", Feature: &X86.HasAVX512VBMI}, + {Name: "avx512vnniw", Feature: &X86.HasAVX5124VNNIW}, + {Name: "avx5124fmaps", Feature: &X86.HasAVX5124FMAPS}, + {Name: "avx512vpopcntdq", Feature: &X86.HasAVX512VPOPCNTDQ}, + {Name: "avx512vpclmulqdq", Feature: &X86.HasAVX512VPCLMULQDQ}, + {Name: "avx512vnni", Feature: &X86.HasAVX512VNNI}, + {Name: "avx512gfni", Feature: &X86.HasAVX512GFNI}, + {Name: "avx512vaes", Feature: &X86.HasAVX512VAES}, + {Name: "avx512vbmi2", Feature: &X86.HasAVX512VBMI2}, + {Name: "avx512bitalg", Feature: &X86.HasAVX512BITALG}, + {Name: "avx512bf16", Feature: &X86.HasAVX512BF16}, + {Name: "amxtile", Feature: &X86.HasAMXTile}, + {Name: "amxint8", Feature: &X86.HasAMXInt8}, + {Name: "amxbf16", Feature: &X86.HasAMXBF16}, + {Name: "bmi1", Feature: &X86.HasBMI1}, + {Name: "bmi2", Feature: &X86.HasBMI2}, + {Name: "cx16", Feature: &X86.HasCX16}, + {Name: "erms", Feature: &X86.HasERMS}, + {Name: "fma", Feature: &X86.HasFMA}, + {Name: "osxsave", Feature: &X86.HasOSXSAVE}, + {Name: "pclmulqdq", Feature: &X86.HasPCLMULQDQ}, + {Name: "popcnt", Feature: &X86.HasPOPCNT}, + {Name: "rdrand", Feature: &X86.HasRDRAND}, + {Name: "rdseed", Feature: &X86.HasRDSEED}, + {Name: "sse3", Feature: &X86.HasSSE3}, + {Name: "sse41", Feature: &X86.HasSSE41}, + {Name: "sse42", Feature: &X86.HasSSE42}, + {Name: "ssse3", Feature: &X86.HasSSSE3}, + + // These capabilities should always be enabled on amd64: + {Name: "sse2", Feature: &X86.HasSSE2, Required: runtime.GOARCH == "amd64"}, + } +} + +func archInit() { + + Initialized = true + + maxID, _, _, _ := cpuid(0, 0) + + if maxID < 1 { + return + } + + _, _, ecx1, edx1 := cpuid(1, 0) + X86.HasSSE2 = isSet(26, edx1) + + X86.HasSSE3 = isSet(0, ecx1) + X86.HasPCLMULQDQ = isSet(1, ecx1) + X86.HasSSSE3 = isSet(9, ecx1) + X86.HasFMA = isSet(12, ecx1) + X86.HasCX16 = isSet(13, ecx1) + X86.HasSSE41 = isSet(19, ecx1) + X86.HasSSE42 = isSet(20, ecx1) + X86.HasPOPCNT = isSet(23, ecx1) + X86.HasAES = isSet(25, ecx1) + X86.HasOSXSAVE = isSet(27, ecx1) + X86.HasRDRAND = isSet(30, ecx1) + + var osSupportsAVX, osSupportsAVX512 bool + // For XGETBV, OSXSAVE bit is required and sufficient. + if X86.HasOSXSAVE { + eax, _ := xgetbv() + // Check if XMM and YMM registers have OS support. + osSupportsAVX = isSet(1, eax) && isSet(2, eax) + + if runtime.GOOS == "darwin" { + // Darwin doesn't save/restore AVX-512 mask registers correctly across signal handlers. + // Since users can't rely on mask register contents, let's not advertise AVX-512 support. + // See issue 49233. + osSupportsAVX512 = false + } else { + // Check if OPMASK and ZMM registers have OS support. + osSupportsAVX512 = osSupportsAVX && isSet(5, eax) && isSet(6, eax) && isSet(7, eax) + } + } + + X86.HasAVX = isSet(28, ecx1) && osSupportsAVX + + if maxID < 7 { + return + } + + _, ebx7, ecx7, edx7 := cpuid(7, 0) + X86.HasBMI1 = isSet(3, ebx7) + X86.HasAVX2 = isSet(5, ebx7) && osSupportsAVX + X86.HasBMI2 = isSet(8, ebx7) + X86.HasERMS = isSet(9, ebx7) + X86.HasRDSEED = isSet(18, ebx7) + X86.HasADX = isSet(19, ebx7) + + X86.HasAVX512 = isSet(16, ebx7) && osSupportsAVX512 // Because avx-512 foundation is the core required extension + if X86.HasAVX512 { + X86.HasAVX512F = true + X86.HasAVX512CD = isSet(28, ebx7) + X86.HasAVX512ER = isSet(27, ebx7) + X86.HasAVX512PF = isSet(26, ebx7) + X86.HasAVX512VL = isSet(31, ebx7) + X86.HasAVX512BW = isSet(30, ebx7) + X86.HasAVX512DQ = isSet(17, ebx7) + X86.HasAVX512IFMA = isSet(21, ebx7) + X86.HasAVX512VBMI = isSet(1, ecx7) + X86.HasAVX5124VNNIW = isSet(2, edx7) + X86.HasAVX5124FMAPS = isSet(3, edx7) + X86.HasAVX512VPOPCNTDQ = isSet(14, ecx7) + X86.HasAVX512VPCLMULQDQ = isSet(10, ecx7) + X86.HasAVX512VNNI = isSet(11, ecx7) + X86.HasAVX512GFNI = isSet(8, ecx7) + X86.HasAVX512VAES = isSet(9, ecx7) + X86.HasAVX512VBMI2 = isSet(6, ecx7) + X86.HasAVX512BITALG = isSet(12, ecx7) + + eax71, _, _, _ := cpuid(7, 1) + X86.HasAVX512BF16 = isSet(5, eax71) + } + + X86.HasAMXTile = isSet(24, edx7) + X86.HasAMXInt8 = isSet(25, edx7) + X86.HasAMXBF16 = isSet(22, edx7) +} + +func isSet(bitpos uint, value uint32) bool { + return value&(1<> 63)) +) + +// For those platforms don't have a 'cpuid' equivalent we use HWCAP/HWCAP2 +// These are initialized in cpu_$GOARCH.go +// and should not be changed after they are initialized. +var hwCap uint +var hwCap2 uint + +func readHWCAP() error { + // For Go 1.21+, get auxv from the Go runtime. + if a := getAuxv(); len(a) > 0 { + for len(a) >= 2 { + tag, val := a[0], uint(a[1]) + a = a[2:] + switch tag { + case _AT_HWCAP: + hwCap = val + case _AT_HWCAP2: + hwCap2 = val + } + } + return nil + } + + buf, err := os.ReadFile(procAuxv) + if err != nil { + // e.g. on android /proc/self/auxv is not accessible, so silently + // ignore the error and leave Initialized = false. On some + // architectures (e.g. arm64) doinit() implements a fallback + // readout and will set Initialized = true again. + return err + } + bo := hostByteOrder() + for len(buf) >= 2*(uintSize/8) { + var tag, val uint + switch uintSize { + case 32: + tag = uint(bo.Uint32(buf[0:])) + val = uint(bo.Uint32(buf[4:])) + buf = buf[8:] + case 64: + tag = uint(bo.Uint64(buf[0:])) + val = uint(bo.Uint64(buf[8:])) + buf = buf[16:] + } + switch tag { + case _AT_HWCAP: + hwCap = val + case _AT_HWCAP2: + hwCap2 = val + } + } + return nil +} diff --git a/internal/xcpu/parse.go b/internal/xcpu/parse.go new file mode 100644 index 00000000..be30b60f --- /dev/null +++ b/internal/xcpu/parse.go @@ -0,0 +1,43 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xcpu + +import "strconv" + +// parseRelease parses a dot-separated version number. It follows the semver +// syntax, but allows the minor and patch versions to be elided. +// +// This is a copy of the Go runtime's parseRelease from +// https://golang.org/cl/209597. +func parseRelease(rel string) (major, minor, patch int, ok bool) { + // Strip anything after a dash or plus. + for i := 0; i < len(rel); i++ { + if rel[i] == '-' || rel[i] == '+' { + rel = rel[:i] + break + } + } + + next := func() (int, bool) { + for i := 0; i < len(rel); i++ { + if rel[i] == '.' { + ver, err := strconv.Atoi(rel[:i]) + rel = rel[i+1:] + return ver, err == nil + } + } + ver, err := strconv.Atoi(rel) + rel = "" + return ver, err == nil + } + if major, ok = next(); !ok || rel == "" { + return + } + if minor, ok = next(); !ok || rel == "" { + return + } + patch, ok = next() + return +} diff --git a/internal/xcpu/proc_cpuinfo_linux.go b/internal/xcpu/proc_cpuinfo_linux.go new file mode 100644 index 00000000..9c88d24e --- /dev/null +++ b/internal/xcpu/proc_cpuinfo_linux.go @@ -0,0 +1,53 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build linux && arm64 + +package xcpu + +import ( + "errors" + "io" + "os" + "strings" +) + +func readLinuxProcCPUInfo() error { + f, err := os.Open("/proc/cpuinfo") + if err != nil { + return err + } + defer f.Close() + + var buf [1 << 10]byte // enough for first CPU + n, err := io.ReadFull(f, buf[:]) + if err != nil && err != io.ErrUnexpectedEOF { + return err + } + in := string(buf[:n]) + const features = "\nFeatures : " + i := strings.Index(in, features) + if i == -1 { + return errors.New("no CPU features found") + } + in = in[i+len(features):] + if i := strings.Index(in, "\n"); i != -1 { + in = in[:i] + } + m := map[string]*bool{} + + initOptions() // need it early here; it's harmless to call twice + for _, o := range options { + m[o.Name] = o.Feature + } + // The EVTSTRM field has alias "evstrm" in Go, but Linux calls it "evtstrm". + m["evtstrm"] = &ARM64.HasEVTSTRM + + for _, f := range strings.Fields(in) { + if p, ok := m[f]; ok { + *p = true + } + } + return nil +} diff --git a/internal/xcpu/runtime_auxv.go b/internal/xcpu/runtime_auxv.go new file mode 100644 index 00000000..b842842e --- /dev/null +++ b/internal/xcpu/runtime_auxv.go @@ -0,0 +1,16 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xcpu + +// getAuxvFn is non-nil on Go 1.21+ (via runtime_auxv_go121.go init) +// on platforms that use auxv. +var getAuxvFn func() []uintptr + +func getAuxv() []uintptr { + if getAuxvFn == nil { + return nil + } + return getAuxvFn() +} diff --git a/internal/xcpu/runtime_auxv_go121.go b/internal/xcpu/runtime_auxv_go121.go new file mode 100644 index 00000000..b4dba06a --- /dev/null +++ b/internal/xcpu/runtime_auxv_go121.go @@ -0,0 +1,18 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build go1.21 + +package xcpu + +import ( + _ "unsafe" // for linkname +) + +//go:linkname runtime_getAuxv runtime.getAuxv +func runtime_getAuxv() []uintptr + +func init() { + getAuxvFn = runtime_getAuxv +} diff --git a/internal/xcpu/syscall_aix_gccgo.go b/internal/xcpu/syscall_aix_gccgo.go new file mode 100644 index 00000000..905566fe --- /dev/null +++ b/internal/xcpu/syscall_aix_gccgo.go @@ -0,0 +1,26 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Recreate a getsystemcfg syscall handler instead of +// using the one provided by x/sys/unix to avoid having +// the dependency between them. (See golang.org/issue/32102) +// Moreover, this file will be used during the building of +// gccgo's libgo and thus must not used a CGo method. + +//go:build aix && gccgo + +package xcpu + +import ( + "syscall" +) + +//extern getsystemcfg +func gccgoGetsystemcfg(label uint32) (r uint64) + +func callgetsystemcfg(label int) (r1 uintptr, e1 syscall.Errno) { + r1 = uintptr(gccgoGetsystemcfg(uint32(label))) + e1 = syscall.GetErrno() + return +} diff --git a/internal/xcpu/syscall_aix_ppc64_gc.go b/internal/xcpu/syscall_aix_ppc64_gc.go new file mode 100644 index 00000000..18837396 --- /dev/null +++ b/internal/xcpu/syscall_aix_ppc64_gc.go @@ -0,0 +1,35 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Minimal copy of x/sys/unix so the cpu package can make a +// system call on AIX without depending on x/sys/unix. +// (See golang.org/issue/32102) + +//go:build aix && ppc64 && gc + +package xcpu + +import ( + "syscall" + "unsafe" +) + +//go:cgo_import_dynamic libc_getsystemcfg getsystemcfg "libc.a/shr_64.o" + +//go:linkname libc_getsystemcfg libc_getsystemcfg + +type syscallFunc uintptr + +var libc_getsystemcfg syscallFunc + +type errno = syscall.Errno + +// Implemented in runtime/syscall_aix.go. +func rawSyscall6(trap, nargs, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err errno) +func syscall6(trap, nargs, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err errno) + +func callgetsystemcfg(label int) (r1 uintptr, e1 errno) { + r1, _, e1 = syscall6(uintptr(unsafe.Pointer(&libc_getsystemcfg)), 1, uintptr(label), 0, 0, 0, 0, 0) + return +} diff --git a/mask_asm.go b/mask_asm.go index bf4bb635..3b1ee517 100644 --- a/mask_asm.go +++ b/mask_asm.go @@ -2,7 +2,7 @@ package websocket -import "golang.org/x/sys/cpu" +import "nhooyr.io/websocket/internal/xcpu" func mask(b []byte, key uint32) uint32 { if len(b) > 0 { @@ -12,7 +12,7 @@ func mask(b []byte, key uint32) uint32 { } //lint:ignore U1000 mask_*.s -var useAVX2 = cpu.X86.HasAVX2 +var useAVX2 = xcpu.X86.HasAVX2 // @nhooyr: I am not confident that the amd64 or the arm64 implementations of this // function are perfect. There are almost certainly missing optimizations or diff --git a/mask_test.go b/mask_test.go index 5c3d43c4..54f55e43 100644 --- a/mask_test.go +++ b/mask_test.go @@ -40,34 +40,34 @@ func TestMask(t *testing.T) { func testMask(t *testing.T, name string, fn func(b []byte, key uint32) uint32) { t.Run(name, func(t *testing.T) { t.Parallel() - for i := 0; i < 9999; i++ { - keyb := make([]byte, 4) - _, err := rand.Read(keyb) - assert.Success(t, err) - key := binary.LittleEndian.Uint32(keyb) + for i := 0; i < 9999; i++ { + keyb := make([]byte, 4) + _, err := rand.Read(keyb) + assert.Success(t, err) + key := binary.LittleEndian.Uint32(keyb) - n, err := rand.Int(rand.Reader, big.NewInt(1<<16)) - assert.Success(t, err) + n, err := rand.Int(rand.Reader, big.NewInt(1<<16)) + assert.Success(t, err) - b := make([]byte, 1+n.Int64()) - _, err = rand.Read(b) - assert.Success(t, err) + b := make([]byte, 1+n.Int64()) + _, err = rand.Read(b) + assert.Success(t, err) - b2 := make([]byte, len(b)) - copy(b2, b) - b3 := make([]byte, len(b)) - copy(b3, b) + b2 := make([]byte, len(b)) + copy(b2, b) + b3 := make([]byte, len(b)) + copy(b3, b) - key2 := basicMask(b2, key) - key3 := fn(b3, key) + key2 := basicMask(b2, key) + key3 := fn(b3, key) - if key2 != key3 { - t.Errorf("expected key %X but got %X", key2, key3) + if key2 != key3 { + t.Errorf("expected key %X but got %X", key2, key3) + } + if !bytes.Equal(b2, b3) { + t.Error("bad bytes") + return + } } - if !bytes.Equal(b2, b3) { - t.Error("bad bytes") - return - } - } }) } From 17e1b864a276ee7a45d6b14f4ed8445a05de543c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 22 Feb 2024 05:14:52 -0800 Subject: [PATCH 468/519] mask_asm: Disable AVX2 See https://github.com/nhooyr/websocket/pull/326#issuecomment-1771138049 --- README.md | 2 +- internal/xcpu/.gitattributes | 10 - internal/xcpu/.gitignore | 2 - internal/xcpu/README.md | 3 - internal/xcpu/asm_aix_ppc64.s | 17 -- internal/xcpu/byteorder.go | 66 ------ internal/xcpu/cpu.go | 290 -------------------------- internal/xcpu/cpu_aix.go | 33 --- internal/xcpu/cpu_arm.go | 73 ------- internal/xcpu/cpu_arm64.go | 172 --------------- internal/xcpu/cpu_arm64.s | 31 --- internal/xcpu/cpu_gc_arm64.go | 11 - internal/xcpu/cpu_gc_s390x.go | 21 -- internal/xcpu/cpu_gc_x86.go | 15 -- internal/xcpu/cpu_gccgo_arm64.go | 11 - internal/xcpu/cpu_gccgo_s390x.go | 22 -- internal/xcpu/cpu_gccgo_x86.c | 37 ---- internal/xcpu/cpu_gccgo_x86.go | 31 --- internal/xcpu/cpu_linux.go | 15 -- internal/xcpu/cpu_linux_arm.go | 39 ---- internal/xcpu/cpu_linux_arm64.go | 111 ---------- internal/xcpu/cpu_linux_mips64x.go | 22 -- internal/xcpu/cpu_linux_noinit.go | 9 - internal/xcpu/cpu_linux_ppc64x.go | 30 --- internal/xcpu/cpu_linux_s390x.go | 40 ---- internal/xcpu/cpu_loong64.go | 12 -- internal/xcpu/cpu_mips64x.go | 15 -- internal/xcpu/cpu_mipsx.go | 11 - internal/xcpu/cpu_netbsd_arm64.go | 173 --------------- internal/xcpu/cpu_openbsd_arm64.go | 65 ------ internal/xcpu/cpu_openbsd_arm64.s | 11 - internal/xcpu/cpu_other_arm.go | 9 - internal/xcpu/cpu_other_arm64.go | 9 - internal/xcpu/cpu_other_mips64x.go | 11 - internal/xcpu/cpu_other_ppc64x.go | 12 -- internal/xcpu/cpu_other_riscv64.go | 11 - internal/xcpu/cpu_ppc64x.go | 16 -- internal/xcpu/cpu_riscv64.go | 11 - internal/xcpu/cpu_s390x.go | 172 --------------- internal/xcpu/cpu_s390x.s | 57 ----- internal/xcpu/cpu_wasm.go | 17 -- internal/xcpu/cpu_x86.go | 151 -------------- internal/xcpu/cpu_x86.s | 26 --- internal/xcpu/cpu_zos.go | 10 - internal/xcpu/cpu_zos_s390x.go | 25 --- internal/xcpu/endian_big.go | 10 - internal/xcpu/endian_little.go | 10 - internal/xcpu/hwcap_linux.go | 71 ------- internal/xcpu/parse.go | 43 ---- internal/xcpu/proc_cpuinfo_linux.go | 53 ----- internal/xcpu/runtime_auxv.go | 16 -- internal/xcpu/runtime_auxv_go121.go | 18 -- internal/xcpu/syscall_aix_gccgo.go | 26 --- internal/xcpu/syscall_aix_ppc64_gc.go | 35 ---- mask_amd64.s | 29 +-- mask_asm.go | 9 +- 56 files changed, 6 insertions(+), 2251 deletions(-) delete mode 100644 internal/xcpu/.gitattributes delete mode 100644 internal/xcpu/.gitignore delete mode 100644 internal/xcpu/README.md delete mode 100644 internal/xcpu/asm_aix_ppc64.s delete mode 100644 internal/xcpu/byteorder.go delete mode 100644 internal/xcpu/cpu.go delete mode 100644 internal/xcpu/cpu_aix.go delete mode 100644 internal/xcpu/cpu_arm.go delete mode 100644 internal/xcpu/cpu_arm64.go delete mode 100644 internal/xcpu/cpu_arm64.s delete mode 100644 internal/xcpu/cpu_gc_arm64.go delete mode 100644 internal/xcpu/cpu_gc_s390x.go delete mode 100644 internal/xcpu/cpu_gc_x86.go delete mode 100644 internal/xcpu/cpu_gccgo_arm64.go delete mode 100644 internal/xcpu/cpu_gccgo_s390x.go delete mode 100644 internal/xcpu/cpu_gccgo_x86.c delete mode 100644 internal/xcpu/cpu_gccgo_x86.go delete mode 100644 internal/xcpu/cpu_linux.go delete mode 100644 internal/xcpu/cpu_linux_arm.go delete mode 100644 internal/xcpu/cpu_linux_arm64.go delete mode 100644 internal/xcpu/cpu_linux_mips64x.go delete mode 100644 internal/xcpu/cpu_linux_noinit.go delete mode 100644 internal/xcpu/cpu_linux_ppc64x.go delete mode 100644 internal/xcpu/cpu_linux_s390x.go delete mode 100644 internal/xcpu/cpu_loong64.go delete mode 100644 internal/xcpu/cpu_mips64x.go delete mode 100644 internal/xcpu/cpu_mipsx.go delete mode 100644 internal/xcpu/cpu_netbsd_arm64.go delete mode 100644 internal/xcpu/cpu_openbsd_arm64.go delete mode 100644 internal/xcpu/cpu_openbsd_arm64.s delete mode 100644 internal/xcpu/cpu_other_arm.go delete mode 100644 internal/xcpu/cpu_other_arm64.go delete mode 100644 internal/xcpu/cpu_other_mips64x.go delete mode 100644 internal/xcpu/cpu_other_ppc64x.go delete mode 100644 internal/xcpu/cpu_other_riscv64.go delete mode 100644 internal/xcpu/cpu_ppc64x.go delete mode 100644 internal/xcpu/cpu_riscv64.go delete mode 100644 internal/xcpu/cpu_s390x.go delete mode 100644 internal/xcpu/cpu_s390x.s delete mode 100644 internal/xcpu/cpu_wasm.go delete mode 100644 internal/xcpu/cpu_x86.go delete mode 100644 internal/xcpu/cpu_x86.s delete mode 100644 internal/xcpu/cpu_zos.go delete mode 100644 internal/xcpu/cpu_zos_s390x.go delete mode 100644 internal/xcpu/endian_big.go delete mode 100644 internal/xcpu/endian_little.go delete mode 100644 internal/xcpu/hwcap_linux.go delete mode 100644 internal/xcpu/parse.go delete mode 100644 internal/xcpu/proc_cpuinfo_linux.go delete mode 100644 internal/xcpu/runtime_auxv.go delete mode 100644 internal/xcpu/runtime_auxv_go121.go delete mode 100644 internal/xcpu/syscall_aix_gccgo.go delete mode 100644 internal/xcpu/syscall_aix_ppc64_gc.go diff --git a/README.md b/README.md index 0f286e63..3dead855 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ Advantages of nhooyr.io/websocket: - Gorilla requires registering a pong callback before sending a Ping - Can target Wasm ([gorilla/websocket#432](https://github.com/gorilla/websocket/issues/432)) - Transparent message buffer reuse with [wsjson](https://pkg.go.dev/nhooyr.io/websocket/wsjson) subpackage -- [4x](https://github.com/nhooyr/websocket/pull/326) faster WebSocket masking implementation in assembly for amd64 and arm64 and [2x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster implementation in pure Go +- [3-4x](https://github.com/nhooyr/websocket/pull/326) faster WebSocket masking implementation in assembly for amd64 and arm64 and [2x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster implementation in pure Go - Gorilla's implementation is slower and uses [unsafe](https://golang.org/pkg/unsafe/). - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support - Gorilla only supports no context takeover mode diff --git a/internal/xcpu/.gitattributes b/internal/xcpu/.gitattributes deleted file mode 100644 index d2f212e5..00000000 --- a/internal/xcpu/.gitattributes +++ /dev/null @@ -1,10 +0,0 @@ -# Treat all files in this repo as binary, with no git magic updating -# line endings. Windows users contributing to Go will need to use a -# modern version of git and editors capable of LF line endings. -# -# We'll prevent accidental CRLF line endings from entering the repo -# via the git-review gofmt checks. -# -# See golang.org/issue/9281 - -* -text diff --git a/internal/xcpu/.gitignore b/internal/xcpu/.gitignore deleted file mode 100644 index 5a9d62ef..00000000 --- a/internal/xcpu/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Add no patterns to .gitignore except for files generated by the build. -last-change diff --git a/internal/xcpu/README.md b/internal/xcpu/README.md deleted file mode 100644 index 96a1a30f..00000000 --- a/internal/xcpu/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# cpu - -Vendored from https://github.com/golang/sys diff --git a/internal/xcpu/asm_aix_ppc64.s b/internal/xcpu/asm_aix_ppc64.s deleted file mode 100644 index 269e173c..00000000 --- a/internal/xcpu/asm_aix_ppc64.s +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build gc - -#include "textflag.h" - -// -// System calls for ppc64, AIX are implemented in runtime/syscall_aix.go -// - -TEXT ·syscall6(SB),NOSPLIT,$0-88 - JMP syscall·syscall6(SB) - -TEXT ·rawSyscall6(SB),NOSPLIT,$0-88 - JMP syscall·rawSyscall6(SB) diff --git a/internal/xcpu/byteorder.go b/internal/xcpu/byteorder.go deleted file mode 100644 index 8f28d86c..00000000 --- a/internal/xcpu/byteorder.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package xcpu - -import ( - "runtime" -) - -// byteOrder is a subset of encoding/binary.ByteOrder. -type byteOrder interface { - Uint32([]byte) uint32 - Uint64([]byte) uint64 -} - -type littleEndian struct{} -type bigEndian struct{} - -func (littleEndian) Uint32(b []byte) uint32 { - _ = b[3] // bounds check hint to compiler; see golang.org/issue/14808 - return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24 -} - -func (littleEndian) Uint64(b []byte) uint64 { - _ = b[7] // bounds check hint to compiler; see golang.org/issue/14808 - return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 | - uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56 -} - -func (bigEndian) Uint32(b []byte) uint32 { - _ = b[3] // bounds check hint to compiler; see golang.org/issue/14808 - return uint32(b[3]) | uint32(b[2])<<8 | uint32(b[1])<<16 | uint32(b[0])<<24 -} - -func (bigEndian) Uint64(b []byte) uint64 { - _ = b[7] // bounds check hint to compiler; see golang.org/issue/14808 - return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 | - uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56 -} - -// hostByteOrder returns littleEndian on little-endian machines and -// bigEndian on big-endian machines. -func hostByteOrder() byteOrder { - switch runtime.GOARCH { - case "386", "amd64", "amd64p32", - "alpha", - "arm", "arm64", - "loong64", - "mipsle", "mips64le", "mips64p32le", - "nios2", - "ppc64le", - "riscv", "riscv64", - "sh": - return littleEndian{} - case "armbe", "arm64be", - "m68k", - "mips", "mips64", "mips64p32", - "ppc", "ppc64", - "s390", "s390x", - "shbe", - "sparc", "sparc64": - return bigEndian{} - } - panic("unknown architecture") -} diff --git a/internal/xcpu/cpu.go b/internal/xcpu/cpu.go deleted file mode 100644 index 5fc15019..00000000 --- a/internal/xcpu/cpu.go +++ /dev/null @@ -1,290 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Package cpu implements processor feature detection for -// various CPU architectures. -package xcpu - -import ( - "os" - "strings" -) - -// Initialized reports whether the CPU features were initialized. -// -// For some GOOS/GOARCH combinations initialization of the CPU features depends -// on reading an operating specific file, e.g. /proc/self/auxv on linux/arm -// Initialized will report false if reading the file fails. -var Initialized bool - -// CacheLinePad is used to pad structs to avoid false sharing. -type CacheLinePad struct{ _ [cacheLineSize]byte } - -// X86 contains the supported CPU features of the -// current X86/AMD64 platform. If the current platform -// is not X86/AMD64 then all feature flags are false. -// -// X86 is padded to avoid false sharing. Further the HasAVX -// and HasAVX2 are only set if the OS supports XMM and YMM -// registers in addition to the CPUID feature bit being set. -var X86 struct { - _ CacheLinePad - HasAES bool // AES hardware implementation (AES NI) - HasADX bool // Multi-precision add-carry instruction extensions - HasAVX bool // Advanced vector extension - HasAVX2 bool // Advanced vector extension 2 - HasAVX512 bool // Advanced vector extension 512 - HasAVX512F bool // Advanced vector extension 512 Foundation Instructions - HasAVX512CD bool // Advanced vector extension 512 Conflict Detection Instructions - HasAVX512ER bool // Advanced vector extension 512 Exponential and Reciprocal Instructions - HasAVX512PF bool // Advanced vector extension 512 Prefetch Instructions - HasAVX512VL bool // Advanced vector extension 512 Vector Length Extensions - HasAVX512BW bool // Advanced vector extension 512 Byte and Word Instructions - HasAVX512DQ bool // Advanced vector extension 512 Doubleword and Quadword Instructions - HasAVX512IFMA bool // Advanced vector extension 512 Integer Fused Multiply Add - HasAVX512VBMI bool // Advanced vector extension 512 Vector Byte Manipulation Instructions - HasAVX5124VNNIW bool // Advanced vector extension 512 Vector Neural Network Instructions Word variable precision - HasAVX5124FMAPS bool // Advanced vector extension 512 Fused Multiply Accumulation Packed Single precision - HasAVX512VPOPCNTDQ bool // Advanced vector extension 512 Double and quad word population count instructions - HasAVX512VPCLMULQDQ bool // Advanced vector extension 512 Vector carry-less multiply operations - HasAVX512VNNI bool // Advanced vector extension 512 Vector Neural Network Instructions - HasAVX512GFNI bool // Advanced vector extension 512 Galois field New Instructions - HasAVX512VAES bool // Advanced vector extension 512 Vector AES instructions - HasAVX512VBMI2 bool // Advanced vector extension 512 Vector Byte Manipulation Instructions 2 - HasAVX512BITALG bool // Advanced vector extension 512 Bit Algorithms - HasAVX512BF16 bool // Advanced vector extension 512 BFloat16 Instructions - HasAMXTile bool // Advanced Matrix Extension Tile instructions - HasAMXInt8 bool // Advanced Matrix Extension Int8 instructions - HasAMXBF16 bool // Advanced Matrix Extension BFloat16 instructions - HasBMI1 bool // Bit manipulation instruction set 1 - HasBMI2 bool // Bit manipulation instruction set 2 - HasCX16 bool // Compare and exchange 16 Bytes - HasERMS bool // Enhanced REP for MOVSB and STOSB - HasFMA bool // Fused-multiply-add instructions - HasOSXSAVE bool // OS supports XSAVE/XRESTOR for saving/restoring XMM registers. - HasPCLMULQDQ bool // PCLMULQDQ instruction - most often used for AES-GCM - HasPOPCNT bool // Hamming weight instruction POPCNT. - HasRDRAND bool // RDRAND instruction (on-chip random number generator) - HasRDSEED bool // RDSEED instruction (on-chip random number generator) - HasSSE2 bool // Streaming SIMD extension 2 (always available on amd64) - HasSSE3 bool // Streaming SIMD extension 3 - HasSSSE3 bool // Supplemental streaming SIMD extension 3 - HasSSE41 bool // Streaming SIMD extension 4 and 4.1 - HasSSE42 bool // Streaming SIMD extension 4 and 4.2 - _ CacheLinePad -} - -// ARM64 contains the supported CPU features of the -// current ARMv8(aarch64) platform. If the current platform -// is not arm64 then all feature flags are false. -var ARM64 struct { - _ CacheLinePad - HasFP bool // Floating-point instruction set (always available) - HasASIMD bool // Advanced SIMD (always available) - HasEVTSTRM bool // Event stream support - HasAES bool // AES hardware implementation - HasPMULL bool // Polynomial multiplication instruction set - HasSHA1 bool // SHA1 hardware implementation - HasSHA2 bool // SHA2 hardware implementation - HasCRC32 bool // CRC32 hardware implementation - HasATOMICS bool // Atomic memory operation instruction set - HasFPHP bool // Half precision floating-point instruction set - HasASIMDHP bool // Advanced SIMD half precision instruction set - HasCPUID bool // CPUID identification scheme registers - HasASIMDRDM bool // Rounding double multiply add/subtract instruction set - HasJSCVT bool // Javascript conversion from floating-point to integer - HasFCMA bool // Floating-point multiplication and addition of complex numbers - HasLRCPC bool // Release Consistent processor consistent support - HasDCPOP bool // Persistent memory support - HasSHA3 bool // SHA3 hardware implementation - HasSM3 bool // SM3 hardware implementation - HasSM4 bool // SM4 hardware implementation - HasASIMDDP bool // Advanced SIMD double precision instruction set - HasSHA512 bool // SHA512 hardware implementation - HasSVE bool // Scalable Vector Extensions - HasASIMDFHM bool // Advanced SIMD multiplication FP16 to FP32 - _ CacheLinePad -} - -// ARM contains the supported CPU features of the current ARM (32-bit) platform. -// All feature flags are false if: -// 1. the current platform is not arm, or -// 2. the current operating system is not Linux. -var ARM struct { - _ CacheLinePad - HasSWP bool // SWP instruction support - HasHALF bool // Half-word load and store support - HasTHUMB bool // ARM Thumb instruction set - Has26BIT bool // Address space limited to 26-bits - HasFASTMUL bool // 32-bit operand, 64-bit result multiplication support - HasFPA bool // Floating point arithmetic support - HasVFP bool // Vector floating point support - HasEDSP bool // DSP Extensions support - HasJAVA bool // Java instruction set - HasIWMMXT bool // Intel Wireless MMX technology support - HasCRUNCH bool // MaverickCrunch context switching and handling - HasTHUMBEE bool // Thumb EE instruction set - HasNEON bool // NEON instruction set - HasVFPv3 bool // Vector floating point version 3 support - HasVFPv3D16 bool // Vector floating point version 3 D8-D15 - HasTLS bool // Thread local storage support - HasVFPv4 bool // Vector floating point version 4 support - HasIDIVA bool // Integer divide instruction support in ARM mode - HasIDIVT bool // Integer divide instruction support in Thumb mode - HasVFPD32 bool // Vector floating point version 3 D15-D31 - HasLPAE bool // Large Physical Address Extensions - HasEVTSTRM bool // Event stream support - HasAES bool // AES hardware implementation - HasPMULL bool // Polynomial multiplication instruction set - HasSHA1 bool // SHA1 hardware implementation - HasSHA2 bool // SHA2 hardware implementation - HasCRC32 bool // CRC32 hardware implementation - _ CacheLinePad -} - -// MIPS64X contains the supported CPU features of the current mips64/mips64le -// platforms. If the current platform is not mips64/mips64le or the current -// operating system is not Linux then all feature flags are false. -var MIPS64X struct { - _ CacheLinePad - HasMSA bool // MIPS SIMD architecture - _ CacheLinePad -} - -// PPC64 contains the supported CPU features of the current ppc64/ppc64le platforms. -// If the current platform is not ppc64/ppc64le then all feature flags are false. -// -// For ppc64/ppc64le, it is safe to check only for ISA level starting on ISA v3.00, -// since there are no optional categories. There are some exceptions that also -// require kernel support to work (DARN, SCV), so there are feature bits for -// those as well. The struct is padded to avoid false sharing. -var PPC64 struct { - _ CacheLinePad - HasDARN bool // Hardware random number generator (requires kernel enablement) - HasSCV bool // Syscall vectored (requires kernel enablement) - IsPOWER8 bool // ISA v2.07 (POWER8) - IsPOWER9 bool // ISA v3.00 (POWER9), implies IsPOWER8 - _ CacheLinePad -} - -// S390X contains the supported CPU features of the current IBM Z -// (s390x) platform. If the current platform is not IBM Z then all -// feature flags are false. -// -// S390X is padded to avoid false sharing. Further HasVX is only set -// if the OS supports vector registers in addition to the STFLE -// feature bit being set. -var S390X struct { - _ CacheLinePad - HasZARCH bool // z/Architecture mode is active [mandatory] - HasSTFLE bool // store facility list extended - HasLDISP bool // long (20-bit) displacements - HasEIMM bool // 32-bit immediates - HasDFP bool // decimal floating point - HasETF3EH bool // ETF-3 enhanced - HasMSA bool // message security assist (CPACF) - HasAES bool // KM-AES{128,192,256} functions - HasAESCBC bool // KMC-AES{128,192,256} functions - HasAESCTR bool // KMCTR-AES{128,192,256} functions - HasAESGCM bool // KMA-GCM-AES{128,192,256} functions - HasGHASH bool // KIMD-GHASH function - HasSHA1 bool // K{I,L}MD-SHA-1 functions - HasSHA256 bool // K{I,L}MD-SHA-256 functions - HasSHA512 bool // K{I,L}MD-SHA-512 functions - HasSHA3 bool // K{I,L}MD-SHA3-{224,256,384,512} and K{I,L}MD-SHAKE-{128,256} functions - HasVX bool // vector facility - HasVXE bool // vector-enhancements facility 1 - _ CacheLinePad -} - -func init() { - archInit() - initOptions() - processOptions() -} - -// options contains the cpu debug options that can be used in GODEBUG. -// Options are arch dependent and are added by the arch specific initOptions functions. -// Features that are mandatory for the specific GOARCH should have the Required field set -// (e.g. SSE2 on amd64). -var options []option - -// Option names should be lower case. e.g. avx instead of AVX. -type option struct { - Name string - Feature *bool - Specified bool // whether feature value was specified in GODEBUG - Enable bool // whether feature should be enabled - Required bool // whether feature is mandatory and can not be disabled -} - -func processOptions() { - env := os.Getenv("GODEBUG") -field: - for env != "" { - field := "" - i := strings.IndexByte(env, ',') - if i < 0 { - field, env = env, "" - } else { - field, env = env[:i], env[i+1:] - } - if len(field) < 4 || field[:4] != "cpu." { - continue - } - i = strings.IndexByte(field, '=') - if i < 0 { - print("GODEBUG sys/cpu: no value specified for \"", field, "\"\n") - continue - } - key, value := field[4:i], field[i+1:] // e.g. "SSE2", "on" - - var enable bool - switch value { - case "on": - enable = true - case "off": - enable = false - default: - print("GODEBUG sys/cpu: value \"", value, "\" not supported for cpu option \"", key, "\"\n") - continue field - } - - if key == "all" { - for i := range options { - options[i].Specified = true - options[i].Enable = enable || options[i].Required - } - continue field - } - - for i := range options { - if options[i].Name == key { - options[i].Specified = true - options[i].Enable = enable - continue field - } - } - - print("GODEBUG sys/cpu: unknown cpu feature \"", key, "\"\n") - } - - for _, o := range options { - if !o.Specified { - continue - } - - if o.Enable && !*o.Feature { - print("GODEBUG sys/cpu: can not enable \"", o.Name, "\", missing CPU support\n") - continue - } - - if !o.Enable && o.Required { - print("GODEBUG sys/cpu: can not disable \"", o.Name, "\", required CPU feature\n") - continue - } - - *o.Feature = o.Enable - } -} diff --git a/internal/xcpu/cpu_aix.go b/internal/xcpu/cpu_aix.go deleted file mode 100644 index 5e6e2583..00000000 --- a/internal/xcpu/cpu_aix.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build aix - -package xcpu - -const ( - // getsystemcfg constants - _SC_IMPL = 2 - _IMPL_POWER8 = 0x10000 - _IMPL_POWER9 = 0x20000 -) - -func archInit() { - impl := getsystemcfg(_SC_IMPL) - if impl&_IMPL_POWER8 != 0 { - PPC64.IsPOWER8 = true - } - if impl&_IMPL_POWER9 != 0 { - PPC64.IsPOWER8 = true - PPC64.IsPOWER9 = true - } - - Initialized = true -} - -func getsystemcfg(label int) (n uint64) { - r0, _ := callgetsystemcfg(label) - n = uint64(r0) - return -} diff --git a/internal/xcpu/cpu_arm.go b/internal/xcpu/cpu_arm.go deleted file mode 100644 index ff120458..00000000 --- a/internal/xcpu/cpu_arm.go +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package xcpu - -const cacheLineSize = 32 - -// HWCAP/HWCAP2 bits. -// These are specific to Linux. -const ( - hwcap_SWP = 1 << 0 - hwcap_HALF = 1 << 1 - hwcap_THUMB = 1 << 2 - hwcap_26BIT = 1 << 3 - hwcap_FAST_MULT = 1 << 4 - hwcap_FPA = 1 << 5 - hwcap_VFP = 1 << 6 - hwcap_EDSP = 1 << 7 - hwcap_JAVA = 1 << 8 - hwcap_IWMMXT = 1 << 9 - hwcap_CRUNCH = 1 << 10 - hwcap_THUMBEE = 1 << 11 - hwcap_NEON = 1 << 12 - hwcap_VFPv3 = 1 << 13 - hwcap_VFPv3D16 = 1 << 14 - hwcap_TLS = 1 << 15 - hwcap_VFPv4 = 1 << 16 - hwcap_IDIVA = 1 << 17 - hwcap_IDIVT = 1 << 18 - hwcap_VFPD32 = 1 << 19 - hwcap_LPAE = 1 << 20 - hwcap_EVTSTRM = 1 << 21 - - hwcap2_AES = 1 << 0 - hwcap2_PMULL = 1 << 1 - hwcap2_SHA1 = 1 << 2 - hwcap2_SHA2 = 1 << 3 - hwcap2_CRC32 = 1 << 4 -) - -func initOptions() { - options = []option{ - {Name: "pmull", Feature: &ARM.HasPMULL}, - {Name: "sha1", Feature: &ARM.HasSHA1}, - {Name: "sha2", Feature: &ARM.HasSHA2}, - {Name: "swp", Feature: &ARM.HasSWP}, - {Name: "thumb", Feature: &ARM.HasTHUMB}, - {Name: "thumbee", Feature: &ARM.HasTHUMBEE}, - {Name: "tls", Feature: &ARM.HasTLS}, - {Name: "vfp", Feature: &ARM.HasVFP}, - {Name: "vfpd32", Feature: &ARM.HasVFPD32}, - {Name: "vfpv3", Feature: &ARM.HasVFPv3}, - {Name: "vfpv3d16", Feature: &ARM.HasVFPv3D16}, - {Name: "vfpv4", Feature: &ARM.HasVFPv4}, - {Name: "half", Feature: &ARM.HasHALF}, - {Name: "26bit", Feature: &ARM.Has26BIT}, - {Name: "fastmul", Feature: &ARM.HasFASTMUL}, - {Name: "fpa", Feature: &ARM.HasFPA}, - {Name: "edsp", Feature: &ARM.HasEDSP}, - {Name: "java", Feature: &ARM.HasJAVA}, - {Name: "iwmmxt", Feature: &ARM.HasIWMMXT}, - {Name: "crunch", Feature: &ARM.HasCRUNCH}, - {Name: "neon", Feature: &ARM.HasNEON}, - {Name: "idivt", Feature: &ARM.HasIDIVT}, - {Name: "idiva", Feature: &ARM.HasIDIVA}, - {Name: "lpae", Feature: &ARM.HasLPAE}, - {Name: "evtstrm", Feature: &ARM.HasEVTSTRM}, - {Name: "aes", Feature: &ARM.HasAES}, - {Name: "crc32", Feature: &ARM.HasCRC32}, - } - -} diff --git a/internal/xcpu/cpu_arm64.go b/internal/xcpu/cpu_arm64.go deleted file mode 100644 index 3d4113a5..00000000 --- a/internal/xcpu/cpu_arm64.go +++ /dev/null @@ -1,172 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package xcpu - -import "runtime" - -// cacheLineSize is used to prevent false sharing of cache lines. -// We choose 128 because Apple Silicon, a.k.a. M1, has 128-byte cache line size. -// It doesn't cost much and is much more future-proof. -const cacheLineSize = 128 - -func initOptions() { - options = []option{ - {Name: "fp", Feature: &ARM64.HasFP}, - {Name: "asimd", Feature: &ARM64.HasASIMD}, - {Name: "evstrm", Feature: &ARM64.HasEVTSTRM}, - {Name: "aes", Feature: &ARM64.HasAES}, - {Name: "fphp", Feature: &ARM64.HasFPHP}, - {Name: "jscvt", Feature: &ARM64.HasJSCVT}, - {Name: "lrcpc", Feature: &ARM64.HasLRCPC}, - {Name: "pmull", Feature: &ARM64.HasPMULL}, - {Name: "sha1", Feature: &ARM64.HasSHA1}, - {Name: "sha2", Feature: &ARM64.HasSHA2}, - {Name: "sha3", Feature: &ARM64.HasSHA3}, - {Name: "sha512", Feature: &ARM64.HasSHA512}, - {Name: "sm3", Feature: &ARM64.HasSM3}, - {Name: "sm4", Feature: &ARM64.HasSM4}, - {Name: "sve", Feature: &ARM64.HasSVE}, - {Name: "crc32", Feature: &ARM64.HasCRC32}, - {Name: "atomics", Feature: &ARM64.HasATOMICS}, - {Name: "asimdhp", Feature: &ARM64.HasASIMDHP}, - {Name: "cpuid", Feature: &ARM64.HasCPUID}, - {Name: "asimrdm", Feature: &ARM64.HasASIMDRDM}, - {Name: "fcma", Feature: &ARM64.HasFCMA}, - {Name: "dcpop", Feature: &ARM64.HasDCPOP}, - {Name: "asimddp", Feature: &ARM64.HasASIMDDP}, - {Name: "asimdfhm", Feature: &ARM64.HasASIMDFHM}, - } -} - -func archInit() { - switch runtime.GOOS { - case "freebsd": - readARM64Registers() - case "linux", "netbsd", "openbsd": - doinit() - default: - // Many platforms don't seem to allow reading these registers. - setMinimalFeatures() - } -} - -// setMinimalFeatures fakes the minimal ARM64 features expected by -// TestARM64minimalFeatures. -func setMinimalFeatures() { - ARM64.HasASIMD = true - ARM64.HasFP = true -} - -func readARM64Registers() { - Initialized = true - - parseARM64SystemRegisters(getisar0(), getisar1(), getpfr0()) -} - -func parseARM64SystemRegisters(isar0, isar1, pfr0 uint64) { - // ID_AA64ISAR0_EL1 - switch extractBits(isar0, 4, 7) { - case 1: - ARM64.HasAES = true - case 2: - ARM64.HasAES = true - ARM64.HasPMULL = true - } - - switch extractBits(isar0, 8, 11) { - case 1: - ARM64.HasSHA1 = true - } - - switch extractBits(isar0, 12, 15) { - case 1: - ARM64.HasSHA2 = true - case 2: - ARM64.HasSHA2 = true - ARM64.HasSHA512 = true - } - - switch extractBits(isar0, 16, 19) { - case 1: - ARM64.HasCRC32 = true - } - - switch extractBits(isar0, 20, 23) { - case 2: - ARM64.HasATOMICS = true - } - - switch extractBits(isar0, 28, 31) { - case 1: - ARM64.HasASIMDRDM = true - } - - switch extractBits(isar0, 32, 35) { - case 1: - ARM64.HasSHA3 = true - } - - switch extractBits(isar0, 36, 39) { - case 1: - ARM64.HasSM3 = true - } - - switch extractBits(isar0, 40, 43) { - case 1: - ARM64.HasSM4 = true - } - - switch extractBits(isar0, 44, 47) { - case 1: - ARM64.HasASIMDDP = true - } - - // ID_AA64ISAR1_EL1 - switch extractBits(isar1, 0, 3) { - case 1: - ARM64.HasDCPOP = true - } - - switch extractBits(isar1, 12, 15) { - case 1: - ARM64.HasJSCVT = true - } - - switch extractBits(isar1, 16, 19) { - case 1: - ARM64.HasFCMA = true - } - - switch extractBits(isar1, 20, 23) { - case 1: - ARM64.HasLRCPC = true - } - - // ID_AA64PFR0_EL1 - switch extractBits(pfr0, 16, 19) { - case 0: - ARM64.HasFP = true - case 1: - ARM64.HasFP = true - ARM64.HasFPHP = true - } - - switch extractBits(pfr0, 20, 23) { - case 0: - ARM64.HasASIMD = true - case 1: - ARM64.HasASIMD = true - ARM64.HasASIMDHP = true - } - - switch extractBits(pfr0, 32, 35) { - case 1: - ARM64.HasSVE = true - } -} - -func extractBits(data uint64, start, end uint) uint { - return (uint)(data>>start) & ((1 << (end - start + 1)) - 1) -} diff --git a/internal/xcpu/cpu_arm64.s b/internal/xcpu/cpu_arm64.s deleted file mode 100644 index fcb9a388..00000000 --- a/internal/xcpu/cpu_arm64.s +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build gc - -#include "textflag.h" - -// func getisar0() uint64 -TEXT ·getisar0(SB),NOSPLIT,$0-8 - // get Instruction Set Attributes 0 into x0 - // mrs x0, ID_AA64ISAR0_EL1 = d5380600 - WORD $0xd5380600 - MOVD R0, ret+0(FP) - RET - -// func getisar1() uint64 -TEXT ·getisar1(SB),NOSPLIT,$0-8 - // get Instruction Set Attributes 1 into x0 - // mrs x0, ID_AA64ISAR1_EL1 = d5380620 - WORD $0xd5380620 - MOVD R0, ret+0(FP) - RET - -// func getpfr0() uint64 -TEXT ·getpfr0(SB),NOSPLIT,$0-8 - // get Processor Feature Register 0 into x0 - // mrs x0, ID_AA64PFR0_EL1 = d5380400 - WORD $0xd5380400 - MOVD R0, ret+0(FP) - RET diff --git a/internal/xcpu/cpu_gc_arm64.go b/internal/xcpu/cpu_gc_arm64.go deleted file mode 100644 index 26d3050d..00000000 --- a/internal/xcpu/cpu_gc_arm64.go +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build gc - -package xcpu - -func getisar0() uint64 -func getisar1() uint64 -func getpfr0() uint64 diff --git a/internal/xcpu/cpu_gc_s390x.go b/internal/xcpu/cpu_gc_s390x.go deleted file mode 100644 index 34ca88b7..00000000 --- a/internal/xcpu/cpu_gc_s390x.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build gc - -package xcpu - -// haveAsmFunctions reports whether the other functions in this file can -// be safely called. -func haveAsmFunctions() bool { return true } - -// The following feature detection functions are defined in cpu_s390x.s. -// They are likely to be expensive to call so the results should be cached. -func stfle() facilityList -func kmQuery() queryResult -func kmcQuery() queryResult -func kmctrQuery() queryResult -func kmaQuery() queryResult -func kimdQuery() queryResult -func klmdQuery() queryResult diff --git a/internal/xcpu/cpu_gc_x86.go b/internal/xcpu/cpu_gc_x86.go deleted file mode 100644 index 9d6f61c2..00000000 --- a/internal/xcpu/cpu_gc_x86.go +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build (386 || amd64 || amd64p32) && gc - -package xcpu - -// cpuid is implemented in cpu_x86.s for gc compiler -// and in cpu_gccgo.c for gccgo. -func cpuid(eaxArg, ecxArg uint32) (eax, ebx, ecx, edx uint32) - -// xgetbv with ecx = 0 is implemented in cpu_x86.s for gc compiler -// and in cpu_gccgo.c for gccgo. -func xgetbv() (eax, edx uint32) diff --git a/internal/xcpu/cpu_gccgo_arm64.go b/internal/xcpu/cpu_gccgo_arm64.go deleted file mode 100644 index d6c2a3a8..00000000 --- a/internal/xcpu/cpu_gccgo_arm64.go +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build gccgo - -package xcpu - -func getisar0() uint64 { return 0 } -func getisar1() uint64 { return 0 } -func getpfr0() uint64 { return 0 } diff --git a/internal/xcpu/cpu_gccgo_s390x.go b/internal/xcpu/cpu_gccgo_s390x.go deleted file mode 100644 index 4deec625..00000000 --- a/internal/xcpu/cpu_gccgo_s390x.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build gccgo - -package xcpu - -// haveAsmFunctions reports whether the other functions in this file can -// be safely called. -func haveAsmFunctions() bool { return false } - -// TODO(mundaym): the following feature detection functions are currently -// stubs. See https://golang.org/cl/162887 for how to fix this. -// They are likely to be expensive to call so the results should be cached. -func stfle() facilityList { panic("not implemented for gccgo") } -func kmQuery() queryResult { panic("not implemented for gccgo") } -func kmcQuery() queryResult { panic("not implemented for gccgo") } -func kmctrQuery() queryResult { panic("not implemented for gccgo") } -func kmaQuery() queryResult { panic("not implemented for gccgo") } -func kimdQuery() queryResult { panic("not implemented for gccgo") } -func klmdQuery() queryResult { panic("not implemented for gccgo") } diff --git a/internal/xcpu/cpu_gccgo_x86.c b/internal/xcpu/cpu_gccgo_x86.c deleted file mode 100644 index 3f73a05d..00000000 --- a/internal/xcpu/cpu_gccgo_x86.c +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build (386 || amd64 || amd64p32) && gccgo - -#include -#include -#include - -// Need to wrap __get_cpuid_count because it's declared as static. -int -gccgoGetCpuidCount(uint32_t leaf, uint32_t subleaf, - uint32_t *eax, uint32_t *ebx, - uint32_t *ecx, uint32_t *edx) -{ - return __get_cpuid_count(leaf, subleaf, eax, ebx, ecx, edx); -} - -#pragma GCC diagnostic ignored "-Wunknown-pragmas" -#pragma GCC push_options -#pragma GCC target("xsave") -#pragma clang attribute push (__attribute__((target("xsave"))), apply_to=function) - -// xgetbv reads the contents of an XCR (Extended Control Register) -// specified in the ECX register into registers EDX:EAX. -// Currently, the only supported value for XCR is 0. -void -gccgoXgetbv(uint32_t *eax, uint32_t *edx) -{ - uint64_t v = _xgetbv(0); - *eax = v & 0xffffffff; - *edx = v >> 32; -} - -#pragma clang attribute pop -#pragma GCC pop_options diff --git a/internal/xcpu/cpu_gccgo_x86.go b/internal/xcpu/cpu_gccgo_x86.go deleted file mode 100644 index e66c6ee9..00000000 --- a/internal/xcpu/cpu_gccgo_x86.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build (386 || amd64 || amd64p32) && gccgo - -package xcpu - -//extern gccgoGetCpuidCount -func gccgoGetCpuidCount(eaxArg, ecxArg uint32, eax, ebx, ecx, edx *uint32) - -func cpuid(eaxArg, ecxArg uint32) (eax, ebx, ecx, edx uint32) { - var a, b, c, d uint32 - gccgoGetCpuidCount(eaxArg, ecxArg, &a, &b, &c, &d) - return a, b, c, d -} - -//extern gccgoXgetbv -func gccgoXgetbv(eax, edx *uint32) - -func xgetbv() (eax, edx uint32) { - var a, d uint32 - gccgoXgetbv(&a, &d) - return a, d -} - -// gccgo doesn't build on Darwin, per: -// https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/gcc.rb#L76 -func darwinSupportsAVX512() bool { - return false -} diff --git a/internal/xcpu/cpu_linux.go b/internal/xcpu/cpu_linux.go deleted file mode 100644 index 10a48916..00000000 --- a/internal/xcpu/cpu_linux.go +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build !386 && !amd64 && !amd64p32 && !arm64 - -package xcpu - -func archInit() { - if err := readHWCAP(); err != nil { - return - } - doinit() - Initialized = true -} diff --git a/internal/xcpu/cpu_linux_arm.go b/internal/xcpu/cpu_linux_arm.go deleted file mode 100644 index 28e32637..00000000 --- a/internal/xcpu/cpu_linux_arm.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package xcpu - -func doinit() { - ARM.HasSWP = isSet(hwCap, hwcap_SWP) - ARM.HasHALF = isSet(hwCap, hwcap_HALF) - ARM.HasTHUMB = isSet(hwCap, hwcap_THUMB) - ARM.Has26BIT = isSet(hwCap, hwcap_26BIT) - ARM.HasFASTMUL = isSet(hwCap, hwcap_FAST_MULT) - ARM.HasFPA = isSet(hwCap, hwcap_FPA) - ARM.HasVFP = isSet(hwCap, hwcap_VFP) - ARM.HasEDSP = isSet(hwCap, hwcap_EDSP) - ARM.HasJAVA = isSet(hwCap, hwcap_JAVA) - ARM.HasIWMMXT = isSet(hwCap, hwcap_IWMMXT) - ARM.HasCRUNCH = isSet(hwCap, hwcap_CRUNCH) - ARM.HasTHUMBEE = isSet(hwCap, hwcap_THUMBEE) - ARM.HasNEON = isSet(hwCap, hwcap_NEON) - ARM.HasVFPv3 = isSet(hwCap, hwcap_VFPv3) - ARM.HasVFPv3D16 = isSet(hwCap, hwcap_VFPv3D16) - ARM.HasTLS = isSet(hwCap, hwcap_TLS) - ARM.HasVFPv4 = isSet(hwCap, hwcap_VFPv4) - ARM.HasIDIVA = isSet(hwCap, hwcap_IDIVA) - ARM.HasIDIVT = isSet(hwCap, hwcap_IDIVT) - ARM.HasVFPD32 = isSet(hwCap, hwcap_VFPD32) - ARM.HasLPAE = isSet(hwCap, hwcap_LPAE) - ARM.HasEVTSTRM = isSet(hwCap, hwcap_EVTSTRM) - ARM.HasAES = isSet(hwCap2, hwcap2_AES) - ARM.HasPMULL = isSet(hwCap2, hwcap2_PMULL) - ARM.HasSHA1 = isSet(hwCap2, hwcap2_SHA1) - ARM.HasSHA2 = isSet(hwCap2, hwcap2_SHA2) - ARM.HasCRC32 = isSet(hwCap2, hwcap2_CRC32) -} - -func isSet(hwc uint, value uint) bool { - return hwc&value != 0 -} diff --git a/internal/xcpu/cpu_linux_arm64.go b/internal/xcpu/cpu_linux_arm64.go deleted file mode 100644 index 481f450b..00000000 --- a/internal/xcpu/cpu_linux_arm64.go +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package xcpu - -import ( - "strings" - "syscall" -) - -// HWCAP/HWCAP2 bits. These are exposed by Linux. -const ( - hwcap_FP = 1 << 0 - hwcap_ASIMD = 1 << 1 - hwcap_EVTSTRM = 1 << 2 - hwcap_AES = 1 << 3 - hwcap_PMULL = 1 << 4 - hwcap_SHA1 = 1 << 5 - hwcap_SHA2 = 1 << 6 - hwcap_CRC32 = 1 << 7 - hwcap_ATOMICS = 1 << 8 - hwcap_FPHP = 1 << 9 - hwcap_ASIMDHP = 1 << 10 - hwcap_CPUID = 1 << 11 - hwcap_ASIMDRDM = 1 << 12 - hwcap_JSCVT = 1 << 13 - hwcap_FCMA = 1 << 14 - hwcap_LRCPC = 1 << 15 - hwcap_DCPOP = 1 << 16 - hwcap_SHA3 = 1 << 17 - hwcap_SM3 = 1 << 18 - hwcap_SM4 = 1 << 19 - hwcap_ASIMDDP = 1 << 20 - hwcap_SHA512 = 1 << 21 - hwcap_SVE = 1 << 22 - hwcap_ASIMDFHM = 1 << 23 -) - -// linuxKernelCanEmulateCPUID reports whether we're running -// on Linux 4.11+. Ideally we'd like to ask the question about -// whether the current kernel contains -// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=77c97b4ee21290f5f083173d957843b615abbff2 -// but the version number will have to do. -func linuxKernelCanEmulateCPUID() bool { - var un syscall.Utsname - syscall.Uname(&un) - var sb strings.Builder - for _, b := range un.Release[:] { - if b == 0 { - break - } - sb.WriteByte(byte(b)) - } - major, minor, _, ok := parseRelease(sb.String()) - return ok && (major > 4 || major == 4 && minor >= 11) -} - -func doinit() { - if err := readHWCAP(); err != nil { - // We failed to read /proc/self/auxv. This can happen if the binary has - // been given extra capabilities(7) with /bin/setcap. - // - // When this happens, we have two options. If the Linux kernel is new - // enough (4.11+), we can read the arm64 registers directly which'll - // trap into the kernel and then return back to userspace. - // - // But on older kernels, such as Linux 4.4.180 as used on many Synology - // devices, calling readARM64Registers (specifically getisar0) will - // cause a SIGILL and we'll die. So for older kernels, parse /proc/cpuinfo - // instead. - // - // See golang/go#57336. - if linuxKernelCanEmulateCPUID() { - readARM64Registers() - } else { - readLinuxProcCPUInfo() - } - return - } - - // HWCAP feature bits - ARM64.HasFP = isSet(hwCap, hwcap_FP) - ARM64.HasASIMD = isSet(hwCap, hwcap_ASIMD) - ARM64.HasEVTSTRM = isSet(hwCap, hwcap_EVTSTRM) - ARM64.HasAES = isSet(hwCap, hwcap_AES) - ARM64.HasPMULL = isSet(hwCap, hwcap_PMULL) - ARM64.HasSHA1 = isSet(hwCap, hwcap_SHA1) - ARM64.HasSHA2 = isSet(hwCap, hwcap_SHA2) - ARM64.HasCRC32 = isSet(hwCap, hwcap_CRC32) - ARM64.HasATOMICS = isSet(hwCap, hwcap_ATOMICS) - ARM64.HasFPHP = isSet(hwCap, hwcap_FPHP) - ARM64.HasASIMDHP = isSet(hwCap, hwcap_ASIMDHP) - ARM64.HasCPUID = isSet(hwCap, hwcap_CPUID) - ARM64.HasASIMDRDM = isSet(hwCap, hwcap_ASIMDRDM) - ARM64.HasJSCVT = isSet(hwCap, hwcap_JSCVT) - ARM64.HasFCMA = isSet(hwCap, hwcap_FCMA) - ARM64.HasLRCPC = isSet(hwCap, hwcap_LRCPC) - ARM64.HasDCPOP = isSet(hwCap, hwcap_DCPOP) - ARM64.HasSHA3 = isSet(hwCap, hwcap_SHA3) - ARM64.HasSM3 = isSet(hwCap, hwcap_SM3) - ARM64.HasSM4 = isSet(hwCap, hwcap_SM4) - ARM64.HasASIMDDP = isSet(hwCap, hwcap_ASIMDDP) - ARM64.HasSHA512 = isSet(hwCap, hwcap_SHA512) - ARM64.HasSVE = isSet(hwCap, hwcap_SVE) - ARM64.HasASIMDFHM = isSet(hwCap, hwcap_ASIMDFHM) -} - -func isSet(hwc uint, value uint) bool { - return hwc&value != 0 -} diff --git a/internal/xcpu/cpu_linux_mips64x.go b/internal/xcpu/cpu_linux_mips64x.go deleted file mode 100644 index 15fdee9c..00000000 --- a/internal/xcpu/cpu_linux_mips64x.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build linux && (mips64 || mips64le) - -package xcpu - -// HWCAP bits. These are exposed by the Linux kernel 5.4. -const ( - // CPU features - hwcap_MIPS_MSA = 1 << 1 -) - -func doinit() { - // HWCAP feature bits - MIPS64X.HasMSA = isSet(hwCap, hwcap_MIPS_MSA) -} - -func isSet(hwc uint, value uint) bool { - return hwc&value != 0 -} diff --git a/internal/xcpu/cpu_linux_noinit.go b/internal/xcpu/cpu_linux_noinit.go deleted file mode 100644 index 878e56fb..00000000 --- a/internal/xcpu/cpu_linux_noinit.go +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build linux && !arm && !arm64 && !mips64 && !mips64le && !ppc64 && !ppc64le && !s390x - -package xcpu - -func doinit() {} diff --git a/internal/xcpu/cpu_linux_ppc64x.go b/internal/xcpu/cpu_linux_ppc64x.go deleted file mode 100644 index 6a8ea12a..00000000 --- a/internal/xcpu/cpu_linux_ppc64x.go +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build linux && (ppc64 || ppc64le) - -package xcpu - -// HWCAP/HWCAP2 bits. These are exposed by the kernel. -const ( - // ISA Level - _PPC_FEATURE2_ARCH_2_07 = 0x80000000 - _PPC_FEATURE2_ARCH_3_00 = 0x00800000 - - // CPU features - _PPC_FEATURE2_DARN = 0x00200000 - _PPC_FEATURE2_SCV = 0x00100000 -) - -func doinit() { - // HWCAP2 feature bits - PPC64.IsPOWER8 = isSet(hwCap2, _PPC_FEATURE2_ARCH_2_07) - PPC64.IsPOWER9 = isSet(hwCap2, _PPC_FEATURE2_ARCH_3_00) - PPC64.HasDARN = isSet(hwCap2, _PPC_FEATURE2_DARN) - PPC64.HasSCV = isSet(hwCap2, _PPC_FEATURE2_SCV) -} - -func isSet(hwc uint, value uint) bool { - return hwc&value != 0 -} diff --git a/internal/xcpu/cpu_linux_s390x.go b/internal/xcpu/cpu_linux_s390x.go deleted file mode 100644 index ff0ca7f4..00000000 --- a/internal/xcpu/cpu_linux_s390x.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package xcpu - -const ( - // bit mask values from /usr/include/bits/hwcap.h - hwcap_ZARCH = 2 - hwcap_STFLE = 4 - hwcap_MSA = 8 - hwcap_LDISP = 16 - hwcap_EIMM = 32 - hwcap_DFP = 64 - hwcap_ETF3EH = 256 - hwcap_VX = 2048 - hwcap_VXE = 8192 -) - -func initS390Xbase() { - // test HWCAP bit vector - has := func(featureMask uint) bool { - return hwCap&featureMask == featureMask - } - - // mandatory - S390X.HasZARCH = has(hwcap_ZARCH) - - // optional - S390X.HasSTFLE = has(hwcap_STFLE) - S390X.HasLDISP = has(hwcap_LDISP) - S390X.HasEIMM = has(hwcap_EIMM) - S390X.HasETF3EH = has(hwcap_ETF3EH) - S390X.HasDFP = has(hwcap_DFP) - S390X.HasMSA = has(hwcap_MSA) - S390X.HasVX = has(hwcap_VX) - if S390X.HasVX { - S390X.HasVXE = has(hwcap_VXE) - } -} diff --git a/internal/xcpu/cpu_loong64.go b/internal/xcpu/cpu_loong64.go deleted file mode 100644 index fdb21c60..00000000 --- a/internal/xcpu/cpu_loong64.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build loong64 - -package xcpu - -const cacheLineSize = 64 - -func initOptions() { -} diff --git a/internal/xcpu/cpu_mips64x.go b/internal/xcpu/cpu_mips64x.go deleted file mode 100644 index 447fee98..00000000 --- a/internal/xcpu/cpu_mips64x.go +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build mips64 || mips64le - -package xcpu - -const cacheLineSize = 32 - -func initOptions() { - options = []option{ - {Name: "msa", Feature: &MIPS64X.HasMSA}, - } -} diff --git a/internal/xcpu/cpu_mipsx.go b/internal/xcpu/cpu_mipsx.go deleted file mode 100644 index 6efa1917..00000000 --- a/internal/xcpu/cpu_mipsx.go +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build mips || mipsle - -package xcpu - -const cacheLineSize = 32 - -func initOptions() {} diff --git a/internal/xcpu/cpu_netbsd_arm64.go b/internal/xcpu/cpu_netbsd_arm64.go deleted file mode 100644 index b84b4408..00000000 --- a/internal/xcpu/cpu_netbsd_arm64.go +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package xcpu - -import ( - "syscall" - "unsafe" -) - -// Minimal copy of functionality from x/sys/unix so the cpu package can call -// sysctl without depending on x/sys/unix. - -const ( - _CTL_QUERY = -2 - - _SYSCTL_VERS_1 = 0x1000000 -) - -var _zero uintptr - -func sysctl(mib []int32, old *byte, oldlen *uintptr, new *byte, newlen uintptr) (err error) { - var _p0 unsafe.Pointer - if len(mib) > 0 { - _p0 = unsafe.Pointer(&mib[0]) - } else { - _p0 = unsafe.Pointer(&_zero) - } - _, _, errno := syscall.Syscall6( - syscall.SYS___SYSCTL, - uintptr(_p0), - uintptr(len(mib)), - uintptr(unsafe.Pointer(old)), - uintptr(unsafe.Pointer(oldlen)), - uintptr(unsafe.Pointer(new)), - uintptr(newlen)) - if errno != 0 { - return errno - } - return nil -} - -type sysctlNode struct { - Flags uint32 - Num int32 - Name [32]int8 - Ver uint32 - __rsvd uint32 - Un [16]byte - _sysctl_size [8]byte - _sysctl_func [8]byte - _sysctl_parent [8]byte - _sysctl_desc [8]byte -} - -func sysctlNodes(mib []int32) ([]sysctlNode, error) { - var olen uintptr - - // Get a list of all sysctl nodes below the given MIB by performing - // a sysctl for the given MIB with CTL_QUERY appended. - mib = append(mib, _CTL_QUERY) - qnode := sysctlNode{Flags: _SYSCTL_VERS_1} - qp := (*byte)(unsafe.Pointer(&qnode)) - sz := unsafe.Sizeof(qnode) - if err := sysctl(mib, nil, &olen, qp, sz); err != nil { - return nil, err - } - - // Now that we know the size, get the actual nodes. - nodes := make([]sysctlNode, olen/sz) - np := (*byte)(unsafe.Pointer(&nodes[0])) - if err := sysctl(mib, np, &olen, qp, sz); err != nil { - return nil, err - } - - return nodes, nil -} - -func nametomib(name string) ([]int32, error) { - // Split name into components. - var parts []string - last := 0 - for i := 0; i < len(name); i++ { - if name[i] == '.' { - parts = append(parts, name[last:i]) - last = i + 1 - } - } - parts = append(parts, name[last:]) - - mib := []int32{} - // Discover the nodes and construct the MIB OID. - for partno, part := range parts { - nodes, err := sysctlNodes(mib) - if err != nil { - return nil, err - } - for _, node := range nodes { - n := make([]byte, 0) - for i := range node.Name { - if node.Name[i] != 0 { - n = append(n, byte(node.Name[i])) - } - } - if string(n) == part { - mib = append(mib, int32(node.Num)) - break - } - } - if len(mib) != partno+1 { - return nil, err - } - } - - return mib, nil -} - -// aarch64SysctlCPUID is struct aarch64_sysctl_cpu_id from NetBSD's -type aarch64SysctlCPUID struct { - midr uint64 /* Main ID Register */ - revidr uint64 /* Revision ID Register */ - mpidr uint64 /* Multiprocessor Affinity Register */ - aa64dfr0 uint64 /* A64 Debug Feature Register 0 */ - aa64dfr1 uint64 /* A64 Debug Feature Register 1 */ - aa64isar0 uint64 /* A64 Instruction Set Attribute Register 0 */ - aa64isar1 uint64 /* A64 Instruction Set Attribute Register 1 */ - aa64mmfr0 uint64 /* A64 Memory Model Feature Register 0 */ - aa64mmfr1 uint64 /* A64 Memory Model Feature Register 1 */ - aa64mmfr2 uint64 /* A64 Memory Model Feature Register 2 */ - aa64pfr0 uint64 /* A64 Processor Feature Register 0 */ - aa64pfr1 uint64 /* A64 Processor Feature Register 1 */ - aa64zfr0 uint64 /* A64 SVE Feature ID Register 0 */ - mvfr0 uint32 /* Media and VFP Feature Register 0 */ - mvfr1 uint32 /* Media and VFP Feature Register 1 */ - mvfr2 uint32 /* Media and VFP Feature Register 2 */ - pad uint32 - clidr uint64 /* Cache Level ID Register */ - ctr uint64 /* Cache Type Register */ -} - -func sysctlCPUID(name string) (*aarch64SysctlCPUID, error) { - mib, err := nametomib(name) - if err != nil { - return nil, err - } - - out := aarch64SysctlCPUID{} - n := unsafe.Sizeof(out) - _, _, errno := syscall.Syscall6( - syscall.SYS___SYSCTL, - uintptr(unsafe.Pointer(&mib[0])), - uintptr(len(mib)), - uintptr(unsafe.Pointer(&out)), - uintptr(unsafe.Pointer(&n)), - uintptr(0), - uintptr(0)) - if errno != 0 { - return nil, errno - } - return &out, nil -} - -func doinit() { - cpuid, err := sysctlCPUID("machdep.cpu0.cpu_id") - if err != nil { - setMinimalFeatures() - return - } - parseARM64SystemRegisters(cpuid.aa64isar0, cpuid.aa64isar1, cpuid.aa64pfr0) - - Initialized = true -} diff --git a/internal/xcpu/cpu_openbsd_arm64.go b/internal/xcpu/cpu_openbsd_arm64.go deleted file mode 100644 index 2459a486..00000000 --- a/internal/xcpu/cpu_openbsd_arm64.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package xcpu - -import ( - "syscall" - "unsafe" -) - -// Minimal copy of functionality from x/sys/unix so the cpu package can call -// sysctl without depending on x/sys/unix. - -const ( - // From OpenBSD's sys/sysctl.h. - _CTL_MACHDEP = 7 - - // From OpenBSD's machine/cpu.h. - _CPU_ID_AA64ISAR0 = 2 - _CPU_ID_AA64ISAR1 = 3 -) - -// Implemented in the runtime package (runtime/sys_openbsd3.go) -func syscall_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno) - -//go:linkname syscall_syscall6 syscall.syscall6 - -func sysctl(mib []uint32, old *byte, oldlen *uintptr, new *byte, newlen uintptr) (err error) { - _, _, errno := syscall_syscall6(libc_sysctl_trampoline_addr, uintptr(unsafe.Pointer(&mib[0])), uintptr(len(mib)), uintptr(unsafe.Pointer(old)), uintptr(unsafe.Pointer(oldlen)), uintptr(unsafe.Pointer(new)), uintptr(newlen)) - if errno != 0 { - return errno - } - return nil -} - -var libc_sysctl_trampoline_addr uintptr - -//go:cgo_import_dynamic libc_sysctl sysctl "libc.so" - -func sysctlUint64(mib []uint32) (uint64, bool) { - var out uint64 - nout := unsafe.Sizeof(out) - if err := sysctl(mib, (*byte)(unsafe.Pointer(&out)), &nout, nil, 0); err != nil { - return 0, false - } - return out, true -} - -func doinit() { - setMinimalFeatures() - - // Get ID_AA64ISAR0 and ID_AA64ISAR1 from sysctl. - isar0, ok := sysctlUint64([]uint32{_CTL_MACHDEP, _CPU_ID_AA64ISAR0}) - if !ok { - return - } - isar1, ok := sysctlUint64([]uint32{_CTL_MACHDEP, _CPU_ID_AA64ISAR1}) - if !ok { - return - } - parseARM64SystemRegisters(isar0, isar1, 0) - - Initialized = true -} diff --git a/internal/xcpu/cpu_openbsd_arm64.s b/internal/xcpu/cpu_openbsd_arm64.s deleted file mode 100644 index 054ba05d..00000000 --- a/internal/xcpu/cpu_openbsd_arm64.s +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -#include "textflag.h" - -TEXT libc_sysctl_trampoline<>(SB),NOSPLIT,$0-0 - JMP libc_sysctl(SB) - -GLOBL ·libc_sysctl_trampoline_addr(SB), RODATA, $8 -DATA ·libc_sysctl_trampoline_addr(SB)/8, $libc_sysctl_trampoline<>(SB) diff --git a/internal/xcpu/cpu_other_arm.go b/internal/xcpu/cpu_other_arm.go deleted file mode 100644 index e3247948..00000000 --- a/internal/xcpu/cpu_other_arm.go +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build !linux && arm - -package xcpu - -func archInit() {} diff --git a/internal/xcpu/cpu_other_arm64.go b/internal/xcpu/cpu_other_arm64.go deleted file mode 100644 index 5257a0b6..00000000 --- a/internal/xcpu/cpu_other_arm64.go +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build !linux && !netbsd && !openbsd && arm64 - -package xcpu - -func doinit() {} diff --git a/internal/xcpu/cpu_other_mips64x.go b/internal/xcpu/cpu_other_mips64x.go deleted file mode 100644 index b1ddc9d5..00000000 --- a/internal/xcpu/cpu_other_mips64x.go +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build !linux && (mips64 || mips64le) - -package xcpu - -func archInit() { - Initialized = true -} diff --git a/internal/xcpu/cpu_other_ppc64x.go b/internal/xcpu/cpu_other_ppc64x.go deleted file mode 100644 index 00a08baa..00000000 --- a/internal/xcpu/cpu_other_ppc64x.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build !aix && !linux && (ppc64 || ppc64le) - -package xcpu - -func archInit() { - PPC64.IsPOWER8 = true - Initialized = true -} diff --git a/internal/xcpu/cpu_other_riscv64.go b/internal/xcpu/cpu_other_riscv64.go deleted file mode 100644 index 7f8fd1fc..00000000 --- a/internal/xcpu/cpu_other_riscv64.go +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build !linux && riscv64 - -package xcpu - -func archInit() { - Initialized = true -} diff --git a/internal/xcpu/cpu_ppc64x.go b/internal/xcpu/cpu_ppc64x.go deleted file mode 100644 index 22afeec2..00000000 --- a/internal/xcpu/cpu_ppc64x.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build ppc64 || ppc64le - -package xcpu - -const cacheLineSize = 128 - -func initOptions() { - options = []option{ - {Name: "darn", Feature: &PPC64.HasDARN}, - {Name: "scv", Feature: &PPC64.HasSCV}, - } -} diff --git a/internal/xcpu/cpu_riscv64.go b/internal/xcpu/cpu_riscv64.go deleted file mode 100644 index 28e57b68..00000000 --- a/internal/xcpu/cpu_riscv64.go +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build riscv64 - -package xcpu - -const cacheLineSize = 64 - -func initOptions() {} diff --git a/internal/xcpu/cpu_s390x.go b/internal/xcpu/cpu_s390x.go deleted file mode 100644 index e85a8c5d..00000000 --- a/internal/xcpu/cpu_s390x.go +++ /dev/null @@ -1,172 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package xcpu - -const cacheLineSize = 256 - -func initOptions() { - options = []option{ - {Name: "zarch", Feature: &S390X.HasZARCH, Required: true}, - {Name: "stfle", Feature: &S390X.HasSTFLE, Required: true}, - {Name: "ldisp", Feature: &S390X.HasLDISP, Required: true}, - {Name: "eimm", Feature: &S390X.HasEIMM, Required: true}, - {Name: "dfp", Feature: &S390X.HasDFP}, - {Name: "etf3eh", Feature: &S390X.HasETF3EH}, - {Name: "msa", Feature: &S390X.HasMSA}, - {Name: "aes", Feature: &S390X.HasAES}, - {Name: "aescbc", Feature: &S390X.HasAESCBC}, - {Name: "aesctr", Feature: &S390X.HasAESCTR}, - {Name: "aesgcm", Feature: &S390X.HasAESGCM}, - {Name: "ghash", Feature: &S390X.HasGHASH}, - {Name: "sha1", Feature: &S390X.HasSHA1}, - {Name: "sha256", Feature: &S390X.HasSHA256}, - {Name: "sha3", Feature: &S390X.HasSHA3}, - {Name: "sha512", Feature: &S390X.HasSHA512}, - {Name: "vx", Feature: &S390X.HasVX}, - {Name: "vxe", Feature: &S390X.HasVXE}, - } -} - -// bitIsSet reports whether the bit at index is set. The bit index -// is in big endian order, so bit index 0 is the leftmost bit. -func bitIsSet(bits []uint64, index uint) bool { - return bits[index/64]&((1<<63)>>(index%64)) != 0 -} - -// facility is a bit index for the named facility. -type facility uint8 - -const ( - // mandatory facilities - zarch facility = 1 // z architecture mode is active - stflef facility = 7 // store-facility-list-extended - ldisp facility = 18 // long-displacement - eimm facility = 21 // extended-immediate - - // miscellaneous facilities - dfp facility = 42 // decimal-floating-point - etf3eh facility = 30 // extended-translation 3 enhancement - - // cryptography facilities - msa facility = 17 // message-security-assist - msa3 facility = 76 // message-security-assist extension 3 - msa4 facility = 77 // message-security-assist extension 4 - msa5 facility = 57 // message-security-assist extension 5 - msa8 facility = 146 // message-security-assist extension 8 - msa9 facility = 155 // message-security-assist extension 9 - - // vector facilities - vx facility = 129 // vector facility - vxe facility = 135 // vector-enhancements 1 - vxe2 facility = 148 // vector-enhancements 2 -) - -// facilityList contains the result of an STFLE call. -// Bits are numbered in big endian order so the -// leftmost bit (the MSB) is at index 0. -type facilityList struct { - bits [4]uint64 -} - -// Has reports whether the given facilities are present. -func (s *facilityList) Has(fs ...facility) bool { - if len(fs) == 0 { - panic("no facility bits provided") - } - for _, f := range fs { - if !bitIsSet(s.bits[:], uint(f)) { - return false - } - } - return true -} - -// function is the code for the named cryptographic function. -type function uint8 - -const ( - // KM{,A,C,CTR} function codes - aes128 function = 18 // AES-128 - aes192 function = 19 // AES-192 - aes256 function = 20 // AES-256 - - // K{I,L}MD function codes - sha1 function = 1 // SHA-1 - sha256 function = 2 // SHA-256 - sha512 function = 3 // SHA-512 - sha3_224 function = 32 // SHA3-224 - sha3_256 function = 33 // SHA3-256 - sha3_384 function = 34 // SHA3-384 - sha3_512 function = 35 // SHA3-512 - shake128 function = 36 // SHAKE-128 - shake256 function = 37 // SHAKE-256 - - // KLMD function codes - ghash function = 65 // GHASH -) - -// queryResult contains the result of a Query function -// call. Bits are numbered in big endian order so the -// leftmost bit (the MSB) is at index 0. -type queryResult struct { - bits [2]uint64 -} - -// Has reports whether the given functions are present. -func (q *queryResult) Has(fns ...function) bool { - if len(fns) == 0 { - panic("no function codes provided") - } - for _, f := range fns { - if !bitIsSet(q.bits[:], uint(f)) { - return false - } - } - return true -} - -func doinit() { - initS390Xbase() - - // We need implementations of stfle, km and so on - // to detect cryptographic features. - if !haveAsmFunctions() { - return - } - - // optional cryptographic functions - if S390X.HasMSA { - aes := []function{aes128, aes192, aes256} - - // cipher message - km, kmc := kmQuery(), kmcQuery() - S390X.HasAES = km.Has(aes...) - S390X.HasAESCBC = kmc.Has(aes...) - if S390X.HasSTFLE { - facilities := stfle() - if facilities.Has(msa4) { - kmctr := kmctrQuery() - S390X.HasAESCTR = kmctr.Has(aes...) - } - if facilities.Has(msa8) { - kma := kmaQuery() - S390X.HasAESGCM = kma.Has(aes...) - } - } - - // compute message digest - kimd := kimdQuery() // intermediate (no padding) - klmd := klmdQuery() // last (padding) - S390X.HasSHA1 = kimd.Has(sha1) && klmd.Has(sha1) - S390X.HasSHA256 = kimd.Has(sha256) && klmd.Has(sha256) - S390X.HasSHA512 = kimd.Has(sha512) && klmd.Has(sha512) - S390X.HasGHASH = kimd.Has(ghash) // KLMD-GHASH does not exist - sha3 := []function{ - sha3_224, sha3_256, sha3_384, sha3_512, - shake128, shake256, - } - S390X.HasSHA3 = kimd.Has(sha3...) && klmd.Has(sha3...) - } -} diff --git a/internal/xcpu/cpu_s390x.s b/internal/xcpu/cpu_s390x.s deleted file mode 100644 index 1fb4b701..00000000 --- a/internal/xcpu/cpu_s390x.s +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build gc - -#include "textflag.h" - -// func stfle() facilityList -TEXT ·stfle(SB), NOSPLIT|NOFRAME, $0-32 - MOVD $ret+0(FP), R1 - MOVD $3, R0 // last doubleword index to store - XC $32, (R1), (R1) // clear 4 doublewords (32 bytes) - WORD $0xb2b01000 // store facility list extended (STFLE) - RET - -// func kmQuery() queryResult -TEXT ·kmQuery(SB), NOSPLIT|NOFRAME, $0-16 - MOVD $0, R0 // set function code to 0 (KM-Query) - MOVD $ret+0(FP), R1 // address of 16-byte return value - WORD $0xB92E0024 // cipher message (KM) - RET - -// func kmcQuery() queryResult -TEXT ·kmcQuery(SB), NOSPLIT|NOFRAME, $0-16 - MOVD $0, R0 // set function code to 0 (KMC-Query) - MOVD $ret+0(FP), R1 // address of 16-byte return value - WORD $0xB92F0024 // cipher message with chaining (KMC) - RET - -// func kmctrQuery() queryResult -TEXT ·kmctrQuery(SB), NOSPLIT|NOFRAME, $0-16 - MOVD $0, R0 // set function code to 0 (KMCTR-Query) - MOVD $ret+0(FP), R1 // address of 16-byte return value - WORD $0xB92D4024 // cipher message with counter (KMCTR) - RET - -// func kmaQuery() queryResult -TEXT ·kmaQuery(SB), NOSPLIT|NOFRAME, $0-16 - MOVD $0, R0 // set function code to 0 (KMA-Query) - MOVD $ret+0(FP), R1 // address of 16-byte return value - WORD $0xb9296024 // cipher message with authentication (KMA) - RET - -// func kimdQuery() queryResult -TEXT ·kimdQuery(SB), NOSPLIT|NOFRAME, $0-16 - MOVD $0, R0 // set function code to 0 (KIMD-Query) - MOVD $ret+0(FP), R1 // address of 16-byte return value - WORD $0xB93E0024 // compute intermediate message digest (KIMD) - RET - -// func klmdQuery() queryResult -TEXT ·klmdQuery(SB), NOSPLIT|NOFRAME, $0-16 - MOVD $0, R0 // set function code to 0 (KLMD-Query) - MOVD $ret+0(FP), R1 // address of 16-byte return value - WORD $0xB93F0024 // compute last message digest (KLMD) - RET diff --git a/internal/xcpu/cpu_wasm.go b/internal/xcpu/cpu_wasm.go deleted file mode 100644 index 230aaab4..00000000 --- a/internal/xcpu/cpu_wasm.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build wasm - -package xcpu - -// We're compiling the cpu package for an unknown (software-abstracted) CPU. -// Make CacheLinePad an empty struct and hope that the usual struct alignment -// rules are good enough. - -const cacheLineSize = 0 - -func initOptions() {} - -func archInit() {} diff --git a/internal/xcpu/cpu_x86.go b/internal/xcpu/cpu_x86.go deleted file mode 100644 index d2f83468..00000000 --- a/internal/xcpu/cpu_x86.go +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright 2018 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build 386 || amd64 || amd64p32 - -package xcpu - -import "runtime" - -const cacheLineSize = 64 - -func initOptions() { - options = []option{ - {Name: "adx", Feature: &X86.HasADX}, - {Name: "aes", Feature: &X86.HasAES}, - {Name: "avx", Feature: &X86.HasAVX}, - {Name: "avx2", Feature: &X86.HasAVX2}, - {Name: "avx512", Feature: &X86.HasAVX512}, - {Name: "avx512f", Feature: &X86.HasAVX512F}, - {Name: "avx512cd", Feature: &X86.HasAVX512CD}, - {Name: "avx512er", Feature: &X86.HasAVX512ER}, - {Name: "avx512pf", Feature: &X86.HasAVX512PF}, - {Name: "avx512vl", Feature: &X86.HasAVX512VL}, - {Name: "avx512bw", Feature: &X86.HasAVX512BW}, - {Name: "avx512dq", Feature: &X86.HasAVX512DQ}, - {Name: "avx512ifma", Feature: &X86.HasAVX512IFMA}, - {Name: "avx512vbmi", Feature: &X86.HasAVX512VBMI}, - {Name: "avx512vnniw", Feature: &X86.HasAVX5124VNNIW}, - {Name: "avx5124fmaps", Feature: &X86.HasAVX5124FMAPS}, - {Name: "avx512vpopcntdq", Feature: &X86.HasAVX512VPOPCNTDQ}, - {Name: "avx512vpclmulqdq", Feature: &X86.HasAVX512VPCLMULQDQ}, - {Name: "avx512vnni", Feature: &X86.HasAVX512VNNI}, - {Name: "avx512gfni", Feature: &X86.HasAVX512GFNI}, - {Name: "avx512vaes", Feature: &X86.HasAVX512VAES}, - {Name: "avx512vbmi2", Feature: &X86.HasAVX512VBMI2}, - {Name: "avx512bitalg", Feature: &X86.HasAVX512BITALG}, - {Name: "avx512bf16", Feature: &X86.HasAVX512BF16}, - {Name: "amxtile", Feature: &X86.HasAMXTile}, - {Name: "amxint8", Feature: &X86.HasAMXInt8}, - {Name: "amxbf16", Feature: &X86.HasAMXBF16}, - {Name: "bmi1", Feature: &X86.HasBMI1}, - {Name: "bmi2", Feature: &X86.HasBMI2}, - {Name: "cx16", Feature: &X86.HasCX16}, - {Name: "erms", Feature: &X86.HasERMS}, - {Name: "fma", Feature: &X86.HasFMA}, - {Name: "osxsave", Feature: &X86.HasOSXSAVE}, - {Name: "pclmulqdq", Feature: &X86.HasPCLMULQDQ}, - {Name: "popcnt", Feature: &X86.HasPOPCNT}, - {Name: "rdrand", Feature: &X86.HasRDRAND}, - {Name: "rdseed", Feature: &X86.HasRDSEED}, - {Name: "sse3", Feature: &X86.HasSSE3}, - {Name: "sse41", Feature: &X86.HasSSE41}, - {Name: "sse42", Feature: &X86.HasSSE42}, - {Name: "ssse3", Feature: &X86.HasSSSE3}, - - // These capabilities should always be enabled on amd64: - {Name: "sse2", Feature: &X86.HasSSE2, Required: runtime.GOARCH == "amd64"}, - } -} - -func archInit() { - - Initialized = true - - maxID, _, _, _ := cpuid(0, 0) - - if maxID < 1 { - return - } - - _, _, ecx1, edx1 := cpuid(1, 0) - X86.HasSSE2 = isSet(26, edx1) - - X86.HasSSE3 = isSet(0, ecx1) - X86.HasPCLMULQDQ = isSet(1, ecx1) - X86.HasSSSE3 = isSet(9, ecx1) - X86.HasFMA = isSet(12, ecx1) - X86.HasCX16 = isSet(13, ecx1) - X86.HasSSE41 = isSet(19, ecx1) - X86.HasSSE42 = isSet(20, ecx1) - X86.HasPOPCNT = isSet(23, ecx1) - X86.HasAES = isSet(25, ecx1) - X86.HasOSXSAVE = isSet(27, ecx1) - X86.HasRDRAND = isSet(30, ecx1) - - var osSupportsAVX, osSupportsAVX512 bool - // For XGETBV, OSXSAVE bit is required and sufficient. - if X86.HasOSXSAVE { - eax, _ := xgetbv() - // Check if XMM and YMM registers have OS support. - osSupportsAVX = isSet(1, eax) && isSet(2, eax) - - if runtime.GOOS == "darwin" { - // Darwin doesn't save/restore AVX-512 mask registers correctly across signal handlers. - // Since users can't rely on mask register contents, let's not advertise AVX-512 support. - // See issue 49233. - osSupportsAVX512 = false - } else { - // Check if OPMASK and ZMM registers have OS support. - osSupportsAVX512 = osSupportsAVX && isSet(5, eax) && isSet(6, eax) && isSet(7, eax) - } - } - - X86.HasAVX = isSet(28, ecx1) && osSupportsAVX - - if maxID < 7 { - return - } - - _, ebx7, ecx7, edx7 := cpuid(7, 0) - X86.HasBMI1 = isSet(3, ebx7) - X86.HasAVX2 = isSet(5, ebx7) && osSupportsAVX - X86.HasBMI2 = isSet(8, ebx7) - X86.HasERMS = isSet(9, ebx7) - X86.HasRDSEED = isSet(18, ebx7) - X86.HasADX = isSet(19, ebx7) - - X86.HasAVX512 = isSet(16, ebx7) && osSupportsAVX512 // Because avx-512 foundation is the core required extension - if X86.HasAVX512 { - X86.HasAVX512F = true - X86.HasAVX512CD = isSet(28, ebx7) - X86.HasAVX512ER = isSet(27, ebx7) - X86.HasAVX512PF = isSet(26, ebx7) - X86.HasAVX512VL = isSet(31, ebx7) - X86.HasAVX512BW = isSet(30, ebx7) - X86.HasAVX512DQ = isSet(17, ebx7) - X86.HasAVX512IFMA = isSet(21, ebx7) - X86.HasAVX512VBMI = isSet(1, ecx7) - X86.HasAVX5124VNNIW = isSet(2, edx7) - X86.HasAVX5124FMAPS = isSet(3, edx7) - X86.HasAVX512VPOPCNTDQ = isSet(14, ecx7) - X86.HasAVX512VPCLMULQDQ = isSet(10, ecx7) - X86.HasAVX512VNNI = isSet(11, ecx7) - X86.HasAVX512GFNI = isSet(8, ecx7) - X86.HasAVX512VAES = isSet(9, ecx7) - X86.HasAVX512VBMI2 = isSet(6, ecx7) - X86.HasAVX512BITALG = isSet(12, ecx7) - - eax71, _, _, _ := cpuid(7, 1) - X86.HasAVX512BF16 = isSet(5, eax71) - } - - X86.HasAMXTile = isSet(24, edx7) - X86.HasAMXInt8 = isSet(25, edx7) - X86.HasAMXBF16 = isSet(22, edx7) -} - -func isSet(bitpos uint, value uint32) bool { - return value&(1<> 63)) -) - -// For those platforms don't have a 'cpuid' equivalent we use HWCAP/HWCAP2 -// These are initialized in cpu_$GOARCH.go -// and should not be changed after they are initialized. -var hwCap uint -var hwCap2 uint - -func readHWCAP() error { - // For Go 1.21+, get auxv from the Go runtime. - if a := getAuxv(); len(a) > 0 { - for len(a) >= 2 { - tag, val := a[0], uint(a[1]) - a = a[2:] - switch tag { - case _AT_HWCAP: - hwCap = val - case _AT_HWCAP2: - hwCap2 = val - } - } - return nil - } - - buf, err := os.ReadFile(procAuxv) - if err != nil { - // e.g. on android /proc/self/auxv is not accessible, so silently - // ignore the error and leave Initialized = false. On some - // architectures (e.g. arm64) doinit() implements a fallback - // readout and will set Initialized = true again. - return err - } - bo := hostByteOrder() - for len(buf) >= 2*(uintSize/8) { - var tag, val uint - switch uintSize { - case 32: - tag = uint(bo.Uint32(buf[0:])) - val = uint(bo.Uint32(buf[4:])) - buf = buf[8:] - case 64: - tag = uint(bo.Uint64(buf[0:])) - val = uint(bo.Uint64(buf[8:])) - buf = buf[16:] - } - switch tag { - case _AT_HWCAP: - hwCap = val - case _AT_HWCAP2: - hwCap2 = val - } - } - return nil -} diff --git a/internal/xcpu/parse.go b/internal/xcpu/parse.go deleted file mode 100644 index be30b60f..00000000 --- a/internal/xcpu/parse.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package xcpu - -import "strconv" - -// parseRelease parses a dot-separated version number. It follows the semver -// syntax, but allows the minor and patch versions to be elided. -// -// This is a copy of the Go runtime's parseRelease from -// https://golang.org/cl/209597. -func parseRelease(rel string) (major, minor, patch int, ok bool) { - // Strip anything after a dash or plus. - for i := 0; i < len(rel); i++ { - if rel[i] == '-' || rel[i] == '+' { - rel = rel[:i] - break - } - } - - next := func() (int, bool) { - for i := 0; i < len(rel); i++ { - if rel[i] == '.' { - ver, err := strconv.Atoi(rel[:i]) - rel = rel[i+1:] - return ver, err == nil - } - } - ver, err := strconv.Atoi(rel) - rel = "" - return ver, err == nil - } - if major, ok = next(); !ok || rel == "" { - return - } - if minor, ok = next(); !ok || rel == "" { - return - } - patch, ok = next() - return -} diff --git a/internal/xcpu/proc_cpuinfo_linux.go b/internal/xcpu/proc_cpuinfo_linux.go deleted file mode 100644 index 9c88d24e..00000000 --- a/internal/xcpu/proc_cpuinfo_linux.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2022 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build linux && arm64 - -package xcpu - -import ( - "errors" - "io" - "os" - "strings" -) - -func readLinuxProcCPUInfo() error { - f, err := os.Open("/proc/cpuinfo") - if err != nil { - return err - } - defer f.Close() - - var buf [1 << 10]byte // enough for first CPU - n, err := io.ReadFull(f, buf[:]) - if err != nil && err != io.ErrUnexpectedEOF { - return err - } - in := string(buf[:n]) - const features = "\nFeatures : " - i := strings.Index(in, features) - if i == -1 { - return errors.New("no CPU features found") - } - in = in[i+len(features):] - if i := strings.Index(in, "\n"); i != -1 { - in = in[:i] - } - m := map[string]*bool{} - - initOptions() // need it early here; it's harmless to call twice - for _, o := range options { - m[o.Name] = o.Feature - } - // The EVTSTRM field has alias "evstrm" in Go, but Linux calls it "evtstrm". - m["evtstrm"] = &ARM64.HasEVTSTRM - - for _, f := range strings.Fields(in) { - if p, ok := m[f]; ok { - *p = true - } - } - return nil -} diff --git a/internal/xcpu/runtime_auxv.go b/internal/xcpu/runtime_auxv.go deleted file mode 100644 index b842842e..00000000 --- a/internal/xcpu/runtime_auxv.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package xcpu - -// getAuxvFn is non-nil on Go 1.21+ (via runtime_auxv_go121.go init) -// on platforms that use auxv. -var getAuxvFn func() []uintptr - -func getAuxv() []uintptr { - if getAuxvFn == nil { - return nil - } - return getAuxvFn() -} diff --git a/internal/xcpu/runtime_auxv_go121.go b/internal/xcpu/runtime_auxv_go121.go deleted file mode 100644 index b4dba06a..00000000 --- a/internal/xcpu/runtime_auxv_go121.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2023 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -//go:build go1.21 - -package xcpu - -import ( - _ "unsafe" // for linkname -) - -//go:linkname runtime_getAuxv runtime.getAuxv -func runtime_getAuxv() []uintptr - -func init() { - getAuxvFn = runtime_getAuxv -} diff --git a/internal/xcpu/syscall_aix_gccgo.go b/internal/xcpu/syscall_aix_gccgo.go deleted file mode 100644 index 905566fe..00000000 --- a/internal/xcpu/syscall_aix_gccgo.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2020 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Recreate a getsystemcfg syscall handler instead of -// using the one provided by x/sys/unix to avoid having -// the dependency between them. (See golang.org/issue/32102) -// Moreover, this file will be used during the building of -// gccgo's libgo and thus must not used a CGo method. - -//go:build aix && gccgo - -package xcpu - -import ( - "syscall" -) - -//extern getsystemcfg -func gccgoGetsystemcfg(label uint32) (r uint64) - -func callgetsystemcfg(label int) (r1 uintptr, e1 syscall.Errno) { - r1 = uintptr(gccgoGetsystemcfg(uint32(label))) - e1 = syscall.GetErrno() - return -} diff --git a/internal/xcpu/syscall_aix_ppc64_gc.go b/internal/xcpu/syscall_aix_ppc64_gc.go deleted file mode 100644 index 18837396..00000000 --- a/internal/xcpu/syscall_aix_ppc64_gc.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2019 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Minimal copy of x/sys/unix so the cpu package can make a -// system call on AIX without depending on x/sys/unix. -// (See golang.org/issue/32102) - -//go:build aix && ppc64 && gc - -package xcpu - -import ( - "syscall" - "unsafe" -) - -//go:cgo_import_dynamic libc_getsystemcfg getsystemcfg "libc.a/shr_64.o" - -//go:linkname libc_getsystemcfg libc_getsystemcfg - -type syscallFunc uintptr - -var libc_getsystemcfg syscallFunc - -type errno = syscall.Errno - -// Implemented in runtime/syscall_aix.go. -func rawSyscall6(trap, nargs, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err errno) -func syscall6(trap, nargs, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err errno) - -func callgetsystemcfg(label int) (r1 uintptr, e1 errno) { - r1, _, e1 = syscall6(uintptr(unsafe.Pointer(&libc_getsystemcfg)), 1, uintptr(label), 0, 0, 0, 0, 0) - return -} diff --git a/mask_amd64.s b/mask_amd64.s index caca53ec..bd42be31 100644 --- a/mask_amd64.s +++ b/mask_amd64.s @@ -26,11 +26,6 @@ TEXT ·maskAsm(SB), NOSPLIT, $0-28 TESTQ $31, AX JNZ unaligned -aligned: - CMPB ·useAVX2(SB), $1 - JE avx2 - JMP sse - unaligned_loop_1byte: XORB SI, (AX) INCQ AX @@ -47,7 +42,7 @@ unaligned_loop_1byte: ORQ DX, DI TESTQ $31, AX - JZ aligned + JZ sse unaligned: TESTQ $7, AX // AND $7 & len, if not zero jump to loop_1b. @@ -60,27 +55,7 @@ unaligned_loop: SUBQ $8, CX TESTQ $31, AX JNZ unaligned_loop - JMP aligned - -avx2: - CMPQ CX, $0x80 - JL sse - VMOVQ DI, X0 - VPBROADCASTQ X0, Y0 - -avx2_loop: - VPXOR (AX), Y0, Y1 - VPXOR 32(AX), Y0, Y2 - VPXOR 64(AX), Y0, Y3 - VPXOR 96(AX), Y0, Y4 - VMOVDQU Y1, (AX) - VMOVDQU Y2, 32(AX) - VMOVDQU Y3, 64(AX) - VMOVDQU Y4, 96(AX) - ADDQ $0x80, AX - SUBQ $0x80, CX - CMPQ CX, $0x80 - JAE avx2_loop // loop if CX >= 0x80 + JMP sse sse: CMPQ CX, $0x40 diff --git a/mask_asm.go b/mask_asm.go index 3b1ee517..259eec03 100644 --- a/mask_asm.go +++ b/mask_asm.go @@ -2,8 +2,6 @@ package websocket -import "nhooyr.io/websocket/internal/xcpu" - func mask(b []byte, key uint32) uint32 { if len(b) > 0 { return maskAsm(&b[0], len(b), key) @@ -11,14 +9,13 @@ func mask(b []byte, key uint32) uint32 { return key } -//lint:ignore U1000 mask_*.s -var useAVX2 = xcpu.X86.HasAVX2 - // @nhooyr: I am not confident that the amd64 or the arm64 implementations of this // function are perfect. There are almost certainly missing optimizations or -// opportunities for // simplification. I'm confident there are no bugs though. +// opportunities for simplification. I'm confident there are no bugs though. // For example, the arm64 implementation doesn't align memory like the amd64. // Or the amd64 implementation could use AVX512 instead of just AVX2. +// The AVX2 code I had to disable anyway as it wasn't performing as expected. +// See https://github.com/nhooyr/websocket/pull/326#issuecomment-1771138049 // //go:noescape func maskAsm(b *byte, len int, key uint32) uint32 From 2cd18b3742d1b29df86bd8daa81fc55fe26f9f8c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 22 Feb 2024 05:39:37 -0800 Subject: [PATCH 469/519] README.md: Link to assembly benchmark results --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3dead855..cde3ec6d 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ Advantages of nhooyr.io/websocket: - Gorilla requires registering a pong callback before sending a Ping - Can target Wasm ([gorilla/websocket#432](https://github.com/gorilla/websocket/issues/432)) - Transparent message buffer reuse with [wsjson](https://pkg.go.dev/nhooyr.io/websocket/wsjson) subpackage -- [3-4x](https://github.com/nhooyr/websocket/pull/326) faster WebSocket masking implementation in assembly for amd64 and arm64 and [2x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster implementation in pure Go +- [3.5x](https://github.com/nhooyr/websocket/pull/326#issuecomment-1959470758) faster WebSocket masking implementation in assembly for amd64 and arm64 and [2x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster implementation in pure Go - Gorilla's implementation is slower and uses [unsafe](https://golang.org/pkg/unsafe/). - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support - Gorilla only supports no context takeover mode From 4c273310109b8d134d96b35d66d7231e8c54a05b Mon Sep 17 00:00:00 2001 From: Grigorii Khvatskii Date: Mon, 26 Feb 2024 16:12:18 -0500 Subject: [PATCH 470/519] Fix unaligned load error on 32-bit architectures On some 32-bit architectures, 64-bit atomic operations panic when the value is not aligned properly. In this package, this causes netConn operations to panic when compiling with GOARCH=386, since netConn does atomic operations with int64 values in the netConn struct (namely, with readExpired and writeExpired). This commit fixes this by moving readExpired and writeExpired to the beginning of the struct, which makes them properly aligned. --- netconn.go | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/netconn.go b/netconn.go index 1667f45c..133ba55f 100644 --- a/netconn.go +++ b/netconn.go @@ -94,22 +94,23 @@ func NetConn(ctx context.Context, c *Conn, msgType MessageType) net.Conn { } type netConn struct { + readExpired int64 + writeExpired int64 + c *Conn msgType MessageType - writeTimer *time.Timer - writeMu *mu - writeExpired int64 - writeCtx context.Context - writeCancel context.CancelFunc - - readTimer *time.Timer - readMu *mu - readExpired int64 - readCtx context.Context - readCancel context.CancelFunc - readEOFed bool - reader io.Reader + writeTimer *time.Timer + writeMu *mu + writeCtx context.Context + writeCancel context.CancelFunc + + readTimer *time.Timer + readMu *mu + readCtx context.Context + readCancel context.CancelFunc + readEOFed bool + reader io.Reader } var _ net.Conn = &netConn{} From 819f9d18929d8db111767c09d0c35c5c6cceb9c8 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 5 Mar 2024 14:11:05 -0800 Subject: [PATCH 471/519] close.go: Fix comment fmt --- close.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/close.go b/close.go index c3dee7e0..e925d043 100644 --- a/close.go +++ b/close.go @@ -93,8 +93,7 @@ func CloseStatus(err error) StatusCode { // The connection can only be closed once. Additional calls to Close // are no-ops. // -// The maximum length of reason must be 125 bytes. Avoid -// sending a dynamic reason. +// The maximum length of reason must be 125 bytes. Avoid sending a dynamic reason. // // Close will unblock all goroutines interacting with the connection once // complete. From 1cc90bb49096127bb51ee1bfe860bdee099e94d8 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 7 Mar 2024 11:39:07 -0800 Subject: [PATCH 472/519] netconn: Explain why we start with the two int64 atomics --- netconn.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netconn.go b/netconn.go index 133ba55f..3324014d 100644 --- a/netconn.go +++ b/netconn.go @@ -94,6 +94,8 @@ func NetConn(ctx context.Context, c *Conn, msgType MessageType) net.Conn { } type netConn struct { + // These must be first to be aligned on 32 bit platforms. + // https://github.com/nhooyr/websocket/pull/438 readExpired int64 writeExpired int64 From 46f41124ad13951bf574ad485b2db964d0172434 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Mon, 18 Dec 2023 15:32:54 -0800 Subject: [PATCH 473/519] fix closenow race --- accept_test.go | 39 +++++++++++++++++++++++++++++++++++++++ close.go | 2 ++ 2 files changed, 41 insertions(+) diff --git a/accept_test.go b/accept_test.go index 7cb85d0f..9ab0ddf5 100644 --- a/accept_test.go +++ b/accept_test.go @@ -6,10 +6,12 @@ package websocket import ( "bufio" "errors" + "io" "net" "net/http" "net/http/httptest" "strings" + "sync" "testing" "nhooyr.io/websocket/internal/test/assert" @@ -142,6 +144,43 @@ func TestAccept(t *testing.T) { _, err := Accept(w, r, nil) assert.Contains(t, err, `failed to hijack connection`) }) + t.Run("closeRace", func(t *testing.T) { + t.Parallel() + + server, _ := net.Pipe() + + pr, pw := io.Pipe() + rw := bufio.NewReadWriter(bufio.NewReader(pr), bufio.NewWriter(pw)) + newResponseWriter := func() http.ResponseWriter { + return mockHijacker{ + ResponseWriter: httptest.NewRecorder(), + hijack: func() (net.Conn, *bufio.ReadWriter, error) { + return server, rw, nil + }, + } + } + w := newResponseWriter() + + r := httptest.NewRequest("GET", "/", nil) + r.Header.Set("Connection", "Upgrade") + r.Header.Set("Upgrade", "websocket") + r.Header.Set("Sec-WebSocket-Version", "13") + r.Header.Set("Sec-WebSocket-Key", xrand.Base64(16)) + + c, err := Accept(w, r, nil) + wg := &sync.WaitGroup{} + wg.Add(2) + go func() { + c.Close(StatusInternalError, "the sky is falling") + wg.Done() + }() + go func() { + c.CloseNow() + wg.Done() + }() + wg.Wait() + assert.Success(t, err) + }) } func Test_verifyClientHandshake(t *testing.T) { diff --git a/close.go b/close.go index e925d043..21edcf11 100644 --- a/close.go +++ b/close.go @@ -113,6 +113,8 @@ func (c *Conn) CloseNow() (err error) { } c.close(nil) + c.closeMu.Lock() + defer c.closeMu.Unlock() return c.closeErr } From 0b3912f68dc3389749e822b03bbbb8e7a138fd11 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 5 Apr 2024 16:23:59 -0700 Subject: [PATCH 474/519] accept_test: Fix @alixander's test Not ideal but whatever, I'm going to rewrite all of this anyway. --- accept_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/accept_test.go b/accept_test.go index 9ab0ddf5..18233b1e 100644 --- a/accept_test.go +++ b/accept_test.go @@ -6,7 +6,6 @@ package websocket import ( "bufio" "errors" - "io" "net" "net/http" "net/http/httptest" @@ -149,8 +148,7 @@ func TestAccept(t *testing.T) { server, _ := net.Pipe() - pr, pw := io.Pipe() - rw := bufio.NewReadWriter(bufio.NewReader(pr), bufio.NewWriter(pw)) + rw := bufio.NewReadWriter(bufio.NewReader(server), bufio.NewWriter(server)) newResponseWriter := func() http.ResponseWriter { return mockHijacker{ ResponseWriter: httptest.NewRecorder(), From db18a31624813282beba3c7e0219dd2f2f1c522d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 5 Apr 2024 16:24:20 -0700 Subject: [PATCH 475/519] close.go: Rewrite how the library handles closing Far simpler now. Sorry this took a while. Closes #427 Closes #429 Closes #434 Closes #436 Closes #437 --- close.go | 154 ++++++++++++++++++++++++++++++++++----------------- conn.go | 74 ++++++++++--------------- conn_test.go | 3 + read.go | 29 +++++----- write.go | 24 +------- ws_js.go | 2 - 6 files changed, 150 insertions(+), 136 deletions(-) diff --git a/close.go b/close.go index 21edcf11..820625ac 100644 --- a/close.go +++ b/close.go @@ -97,82 +97,106 @@ func CloseStatus(err error) StatusCode { // // Close will unblock all goroutines interacting with the connection once // complete. -func (c *Conn) Close(code StatusCode, reason string) error { - defer c.wg.Wait() - return c.closeHandshake(code, reason) +func (c *Conn) Close(code StatusCode, reason string) (err error) { + defer errd.Wrap(&err, "failed to close WebSocket") + + if !c.casClosing() { + err = c.waitGoroutines() + if err != nil { + return err + } + return net.ErrClosed + } + defer func() { + if errors.Is(err, net.ErrClosed) { + err = nil + } + }() + + err = c.closeHandshake(code, reason) + + err2 := c.close() + if err == nil && err2 != nil { + err = err2 + } + + err2 = c.waitGoroutines() + if err == nil && err2 != nil { + err = err2 + } + + return err } // CloseNow closes the WebSocket connection without attempting a close handshake. // Use when you do not want the overhead of the close handshake. func (c *Conn) CloseNow() (err error) { - defer c.wg.Wait() defer errd.Wrap(&err, "failed to close WebSocket") - if c.isClosed() { + if !c.casClosing() { + err = c.waitGoroutines() + if err != nil { + return err + } return net.ErrClosed } + defer func() { + if errors.Is(err, net.ErrClosed) { + err = nil + } + }() - c.close(nil) - c.closeMu.Lock() - defer c.closeMu.Unlock() - return c.closeErr -} - -func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { - defer errd.Wrap(&err, "failed to close WebSocket") - - writeErr := c.writeClose(code, reason) - closeHandshakeErr := c.waitCloseHandshake() + err = c.close() - if writeErr != nil { - return writeErr + err2 := c.waitGoroutines() + if err == nil && err2 != nil { + err = err2 } + return err +} - if CloseStatus(closeHandshakeErr) == -1 && !errors.Is(net.ErrClosed, closeHandshakeErr) { - return closeHandshakeErr +func (c *Conn) closeHandshake(code StatusCode, reason string) error { + err := c.writeClose(code, reason) + if err != nil { + return err } + err = c.waitCloseHandshake() + if CloseStatus(err) != code { + return err + } return nil } func (c *Conn) writeClose(code StatusCode, reason string) error { - c.closeMu.Lock() - wroteClose := c.wroteClose - c.wroteClose = true - c.closeMu.Unlock() - if wroteClose { - return net.ErrClosed - } - ce := CloseError{ Code: code, Reason: reason, } var p []byte - var marshalErr error + var err error if ce.Code != StatusNoStatusRcvd { - p, marshalErr = ce.bytes() - } - - writeErr := c.writeControl(context.Background(), opClose, p) - if CloseStatus(writeErr) != -1 { - // Not a real error if it's due to a close frame being received. - writeErr = nil + p, err = ce.bytes() + if err != nil { + return err + } } - // We do this after in case there was an error writing the close frame. - c.setCloseErr(fmt.Errorf("sent close frame: %w", ce)) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() - if marshalErr != nil { - return marshalErr + err = c.writeControl(ctx, opClose, p) + // If the connection closed as we're writing we ignore the error as we might + // have written the close frame, the peer responded and then someone else read it + // and closed the connection. + if err != nil && !errors.Is(err, net.ErrClosed) { + return err } - return writeErr + return nil } func (c *Conn) waitCloseHandshake() error { - defer c.close(nil) - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() @@ -208,6 +232,36 @@ func (c *Conn) waitCloseHandshake() error { } } +func (c *Conn) waitGoroutines() error { + t := time.NewTimer(time.Second * 15) + defer t.Stop() + + select { + case <-c.timeoutLoopDone: + case <-t.C: + return errors.New("failed to wait for timeoutLoop goroutine to exit") + } + + c.closeReadMu.Lock() + ctx := c.closeReadCtx + c.closeReadMu.Unlock() + if ctx != nil { + select { + case <-ctx.Done(): + case <-t.C: + return errors.New("failed to wait for close read goroutine to exit") + } + } + + select { + case <-c.closed: + case <-t.C: + return errors.New("failed to wait for connection to be closed") + } + + return nil +} + func parseClosePayload(p []byte) (CloseError, error) { if len(p) == 0 { return CloseError{ @@ -278,16 +332,14 @@ func (ce CloseError) bytesErr() ([]byte, error) { return buf, nil } -func (c *Conn) setCloseErr(err error) { +func (c *Conn) casClosing() bool { c.closeMu.Lock() - c.setCloseErrLocked(err) - c.closeMu.Unlock() -} - -func (c *Conn) setCloseErrLocked(err error) { - if c.closeErr == nil && err != nil { - c.closeErr = fmt.Errorf("WebSocket closed: %w", err) + defer c.closeMu.Unlock() + if !c.closing { + c.closing = true + return true } + return false } func (c *Conn) isClosed() bool { diff --git a/conn.go b/conn.go index ef4d62ad..8ba82962 100644 --- a/conn.go +++ b/conn.go @@ -6,7 +6,6 @@ package websocket import ( "bufio" "context" - "errors" "fmt" "io" "net" @@ -53,8 +52,9 @@ type Conn struct { br *bufio.Reader bw *bufio.Writer - readTimeout chan context.Context - writeTimeout chan context.Context + readTimeout chan context.Context + writeTimeout chan context.Context + timeoutLoopDone chan struct{} // Read state. readMu *mu @@ -70,11 +70,12 @@ type Conn struct { writeHeaderBuf [8]byte writeHeader header - wg sync.WaitGroup - closed chan struct{} - closeMu sync.Mutex - closeErr error - wroteClose bool + closeReadMu sync.Mutex + closeReadCtx context.Context + + closed chan struct{} + closeMu sync.Mutex + closing bool pingCounter int32 activePingsMu sync.Mutex @@ -103,8 +104,9 @@ func newConn(cfg connConfig) *Conn { br: cfg.br, bw: cfg.bw, - readTimeout: make(chan context.Context), - writeTimeout: make(chan context.Context), + readTimeout: make(chan context.Context), + writeTimeout: make(chan context.Context), + timeoutLoopDone: make(chan struct{}), closed: make(chan struct{}), activePings: make(map[string]chan<- struct{}), @@ -128,14 +130,10 @@ func newConn(cfg connConfig) *Conn { } runtime.SetFinalizer(c, func(c *Conn) { - c.close(errors.New("connection garbage collected")) + c.close() }) - c.wg.Add(1) - go func() { - defer c.wg.Done() - c.timeoutLoop() - }() + go c.timeoutLoop() return c } @@ -146,35 +144,29 @@ func (c *Conn) Subprotocol() string { return c.subprotocol } -func (c *Conn) close(err error) { +func (c *Conn) close() error { c.closeMu.Lock() defer c.closeMu.Unlock() if c.isClosed() { - return - } - if err == nil { - err = c.rwc.Close() + return net.ErrClosed } - c.setCloseErrLocked(err) - - close(c.closed) runtime.SetFinalizer(c, nil) + close(c.closed) // Have to close after c.closed is closed to ensure any goroutine that wakes up // from the connection being closed also sees that c.closed is closed and returns // closeErr. - c.rwc.Close() - - c.wg.Add(1) - go func() { - defer c.wg.Done() - c.msgWriter.close() - c.msgReader.close() - }() + err := c.rwc.Close() + // With the close of rwc, these become safe to close. + c.msgWriter.close() + c.msgReader.close() + return err } func (c *Conn) timeoutLoop() { + defer close(c.timeoutLoopDone) + readCtx := context.Background() writeCtx := context.Background() @@ -187,14 +179,10 @@ func (c *Conn) timeoutLoop() { case readCtx = <-c.readTimeout: case <-readCtx.Done(): - c.setCloseErr(fmt.Errorf("read timed out: %w", readCtx.Err())) - c.wg.Add(1) - go func() { - defer c.wg.Done() - c.writeError(StatusPolicyViolation, errors.New("read timed out")) - }() + c.close() + return case <-writeCtx.Done(): - c.close(fmt.Errorf("write timed out: %w", writeCtx.Err())) + c.close() return } } @@ -243,9 +231,7 @@ func (c *Conn) ping(ctx context.Context, p string) error { case <-c.closed: return net.ErrClosed case <-ctx.Done(): - err := fmt.Errorf("failed to wait for pong: %w", ctx.Err()) - c.close(err) - return err + return fmt.Errorf("failed to wait for pong: %w", ctx.Err()) case <-pong: return nil } @@ -281,9 +267,7 @@ func (m *mu) lock(ctx context.Context) error { case <-m.c.closed: return net.ErrClosed case <-ctx.Done(): - err := fmt.Errorf("failed to acquire lock: %w", ctx.Err()) - m.c.close(err) - return err + return fmt.Errorf("failed to acquire lock: %w", ctx.Err()) case m.ch <- struct{}{}: // To make sure the connection is certainly alive. // As it's possible the send on m.ch was selected diff --git a/conn_test.go b/conn_test.go index 97b172dc..ff7279f5 100644 --- a/conn_test.go +++ b/conn_test.go @@ -345,6 +345,9 @@ func TestConn(t *testing.T) { func TestWasm(t *testing.T) { t.Parallel() + if os.Getenv("CI") == "" { + t.Skip() + } s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := echoServer(w, r, &websocket.AcceptOptions{ diff --git a/read.go b/read.go index 81b89831..bd0ddf95 100644 --- a/read.go +++ b/read.go @@ -60,14 +60,21 @@ func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { // Call CloseRead when you do not expect to read any more messages. // Since it actively reads from the connection, it will ensure that ping, pong and close // frames are responded to. This means c.Ping and c.Close will still work as expected. +// +// This function is idempotent. func (c *Conn) CloseRead(ctx context.Context) context.Context { + c.closeReadMu.Lock() + if c.closeReadCtx != nil { + c.closeReadMu.Unlock() + return c.closeReadCtx + } ctx, cancel := context.WithCancel(ctx) + c.closeReadCtx = ctx + c.closeReadMu.Unlock() - c.wg.Add(1) go func() { - defer c.CloseNow() - defer c.wg.Done() defer cancel() + defer c.close() _, _, err := c.Reader(ctx) if err == nil { c.Close(StatusPolicyViolation, "unexpected data message") @@ -222,7 +229,6 @@ func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { case <-ctx.Done(): return header{}, ctx.Err() default: - c.close(err) return header{}, err } } @@ -251,9 +257,7 @@ func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { case <-ctx.Done(): return n, ctx.Err() default: - err = fmt.Errorf("failed to read frame payload: %w", err) - c.close(err) - return n, err + return n, fmt.Errorf("failed to read frame payload: %w", err) } } @@ -320,9 +324,7 @@ func (c *Conn) handleControl(ctx context.Context, h header) (err error) { } err = fmt.Errorf("received close frame: %w", ce) - c.setCloseErr(err) c.writeClose(ce.Code, ce.Reason) - c.close(err) return err } @@ -336,9 +338,7 @@ func (c *Conn) reader(ctx context.Context) (_ MessageType, _ io.Reader, err erro defer c.readMu.unlock() if !c.msgReader.fin { - err = errors.New("previous message not read to completion") - c.close(fmt.Errorf("failed to get reader: %w", err)) - return 0, nil, err + return 0, nil, errors.New("previous message not read to completion") } h, err := c.readLoop(ctx) @@ -411,10 +411,9 @@ func (mr *msgReader) Read(p []byte) (n int, err error) { return n, io.EOF } if err != nil { - err = fmt.Errorf("failed to read: %w", err) - mr.c.close(err) + return n, fmt.Errorf("failed to read: %w", err) } - return n, err + return n, nil } func (mr *msgReader) read(p []byte) (int, error) { diff --git a/write.go b/write.go index 7ac7ce63..d7222f2d 100644 --- a/write.go +++ b/write.go @@ -159,7 +159,6 @@ func (mw *msgWriter) Write(p []byte) (_ int, err error) { defer func() { if err != nil { err = fmt.Errorf("failed to write: %w", err) - mw.c.close(err) } }() @@ -242,30 +241,12 @@ func (c *Conn) writeControl(ctx context.Context, opcode opcode, p []byte) error return nil } -// frame handles all writes to the connection. +// writeFrame handles all writes to the connection. func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opcode, p []byte) (_ int, err error) { err = c.writeFrameMu.lock(ctx) if err != nil { return 0, err } - - // If the state says a close has already been written, we wait until - // the connection is closed and return that error. - // - // However, if the frame being written is a close, that means its the close from - // the state being set so we let it go through. - c.closeMu.Lock() - wroteClose := c.wroteClose - c.closeMu.Unlock() - if wroteClose && opcode != opClose { - c.writeFrameMu.unlock() - select { - case <-ctx.Done(): - return 0, ctx.Err() - case <-c.closed: - return 0, net.ErrClosed - } - } defer c.writeFrameMu.unlock() select { @@ -283,7 +264,6 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco err = ctx.Err() default: } - c.close(err) err = fmt.Errorf("failed to write frame: %w", err) } }() @@ -392,7 +372,5 @@ func extractBufioWriterBuf(bw *bufio.Writer, w io.Writer) []byte { } func (c *Conn) writeError(code StatusCode, err error) { - c.setCloseErr(err) c.writeClose(code, err.Error()) - c.close(nil) } diff --git a/ws_js.go b/ws_js.go index 77d0d80f..2b8e3b3d 100644 --- a/ws_js.go +++ b/ws_js.go @@ -225,7 +225,6 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) error { // or the connection is closed. // It thus performs the full WebSocket close handshake. func (c *Conn) Close(code StatusCode, reason string) error { - defer c.wg.Wait() err := c.exportedClose(code, reason) if err != nil { return fmt.Errorf("failed to close WebSocket: %w", err) @@ -239,7 +238,6 @@ func (c *Conn) Close(code StatusCode, reason string) error { // note: No different from Close(StatusGoingAway, "") in WASM as there is no way to close // a WebSocket without the close handshake. func (c *Conn) CloseNow() error { - defer c.wg.Wait() return c.Close(StatusGoingAway, "") } From 856e371494bab94984371763f53f114b4cef3547 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 5 Apr 2024 16:39:03 -0700 Subject: [PATCH 476/519] ws_js: Update to match new close code --- read.go | 5 +++-- ws_js.go | 25 +++++++++++++++++-------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/read.go b/read.go index bd0ddf95..8bd73695 100644 --- a/read.go +++ b/read.go @@ -64,9 +64,10 @@ func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { // This function is idempotent. func (c *Conn) CloseRead(ctx context.Context) context.Context { c.closeReadMu.Lock() - if c.closeReadCtx != nil { + ctx2 := c.closeReadCtx + if ctx2 != nil { c.closeReadMu.Unlock() - return c.closeReadCtx + return ctx2 } ctx, cancel := context.WithCancel(ctx) c.closeReadCtx = ctx diff --git a/ws_js.go b/ws_js.go index 2b8e3b3d..02d61f28 100644 --- a/ws_js.go +++ b/ws_js.go @@ -47,9 +47,10 @@ type Conn struct { // read limit for a message in bytes. msgReadLimit xsync.Int64 - wg sync.WaitGroup + closeReadMu sync.Mutex + closeReadCtx context.Context + closingMu sync.Mutex - isReadClosed xsync.Int64 closeOnce sync.Once closed chan struct{} closeErrOnce sync.Once @@ -130,7 +131,10 @@ func (c *Conn) closeWithInternal() { // Read attempts to read a message from the connection. // The maximum time spent waiting is bounded by the context. func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { - if c.isReadClosed.Load() == 1 { + c.closeReadMu.Lock() + closedRead := c.closeReadCtx != nil + c.closeReadMu.Unlock() + if closedRead { return 0, nil, errors.New("WebSocket connection read closed") } @@ -387,14 +391,19 @@ func (w *writer) Close() error { // CloseRead implements *Conn.CloseRead for wasm. func (c *Conn) CloseRead(ctx context.Context) context.Context { - c.isReadClosed.Store(1) - + c.closeReadMu.Lock() + ctx2 := c.closeReadCtx + if ctx2 != nil { + c.closeReadMu.Unlock() + return ctx2 + } ctx, cancel := context.WithCancel(ctx) - c.wg.Add(1) + c.closeReadCtx = ctx + c.closeReadMu.Unlock() + go func() { - defer c.CloseNow() - defer c.wg.Done() defer cancel() + defer c.CloseNow() _, _, err := c.read(ctx) if err != nil { c.Close(StatusPolicyViolation, "unexpected data message") From 0edbb2805cd3da973ff35ab5b54969a38f6eaecf Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 5 Apr 2024 16:44:17 -0700 Subject: [PATCH 477/519] netconn: fmt --- netconn.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netconn.go b/netconn.go index 3324014d..86f7dadb 100644 --- a/netconn.go +++ b/netconn.go @@ -94,7 +94,7 @@ func NetConn(ctx context.Context, c *Conn, msgType MessageType) net.Conn { } type netConn struct { - // These must be first to be aligned on 32 bit platforms. + // These must be first to be aligned on 32 bit platforms. // https://github.com/nhooyr/websocket/pull/438 readExpired int64 writeExpired int64 From c97fb09c4f6ddbecf13cbfe6564367d05006d007 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 5 Apr 2024 17:45:54 -0700 Subject: [PATCH 478/519] mask_asm: Disable for now until v1.9.0 See #326 --- mask.go | 2 -- mask_asm.go | 13 +++++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/mask.go b/mask.go index 5f0746dc..7bc0c8d5 100644 --- a/mask.go +++ b/mask.go @@ -16,8 +16,6 @@ import ( // to be in little endian. // // See https://github.com/golang/go/issues/31586 -// -//lint:ignore U1000 mask.go func maskGo(b []byte, key uint32) uint32 { if len(b) >= 8 { key64 := uint64(key)<<32 | uint64(key) diff --git a/mask_asm.go b/mask_asm.go index 259eec03..60c0290f 100644 --- a/mask_asm.go +++ b/mask_asm.go @@ -3,10 +3,14 @@ package websocket func mask(b []byte, key uint32) uint32 { - if len(b) > 0 { - return maskAsm(&b[0], len(b), key) - } - return key + // TODO: Will enable in v1.9.0. + return maskGo(b, key) + /* + if len(b) > 0 { + return maskAsm(&b[0], len(b), key) + } + return key + */ } // @nhooyr: I am not confident that the amd64 or the arm64 implementations of this @@ -18,4 +22,5 @@ func mask(b []byte, key uint32) uint32 { // See https://github.com/nhooyr/websocket/pull/326#issuecomment-1771138049 // //go:noescape +//lint:ignore U1000 disabled till v1.9.0 func maskAsm(b *byte, len int, key uint32) uint32 From 211ef4bcce3f6cf639c870a00d464a2676416066 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 7 Apr 2024 07:39:48 -0700 Subject: [PATCH 479/519] ws_js_test: Fix --- close.go | 4 ---- conn.go | 1 - conn_test.go | 2 +- read.go | 6 +++--- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/close.go b/close.go index 820625ac..d1512597 100644 --- a/close.go +++ b/close.go @@ -206,10 +206,6 @@ func (c *Conn) waitCloseHandshake() error { } defer c.readMu.unlock() - if c.readCloseFrameErr != nil { - return c.readCloseFrameErr - } - for i := int64(0); i < c.msgReader.payloadLength; i++ { _, err := c.br.ReadByte() if err != nil { diff --git a/conn.go b/conn.go index 8ba82962..f5da573a 100644 --- a/conn.go +++ b/conn.go @@ -61,7 +61,6 @@ type Conn struct { readHeaderBuf [8]byte readControlBuf [maxControlPayload]byte msgReader *msgReader - readCloseFrameErr error // Write state. msgWriter *msgWriter diff --git a/conn_test.go b/conn_test.go index ff7279f5..9fbe961d 100644 --- a/conn_test.go +++ b/conn_test.go @@ -363,7 +363,7 @@ func TestWasm(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - cmd := exec.CommandContext(ctx, "go", "test", "-exec=wasmbrowsertest", ".") + cmd := exec.CommandContext(ctx, "go", "test", "-exec=wasmbrowsertest", ".", "-v") cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm", fmt.Sprintf("WS_ECHO_SERVER_URL=%v", s.URL)) b, err := cmd.CombinedOutput() diff --git a/read.go b/read.go index 8bd73695..5df031ca 100644 --- a/read.go +++ b/read.go @@ -313,9 +313,7 @@ func (c *Conn) handleControl(ctx context.Context, h header) (err error) { return nil } - defer func() { - c.readCloseFrameErr = err - }() + // opClose ce, err := parseClosePayload(b) if err != nil { @@ -326,6 +324,8 @@ func (c *Conn) handleControl(ctx context.Context, h header) (err error) { err = fmt.Errorf("received close frame: %w", ce) c.writeClose(ce.Code, ce.Reason) + c.readMu.unlock() + c.close() return err } From 250db1efbe15806649120e4f6748de43859b5d12 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 7 Apr 2024 07:47:26 -0700 Subject: [PATCH 480/519] read: Fix CloseRead to have its own done channel Context can be cancelled by parent. Doesn't indicate the CloseRead goroutine has exited. --- close.go | 6 +++--- conn.go | 13 +++++++------ mask_asm.go | 2 +- read.go | 2 ++ 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/close.go b/close.go index d1512597..625ed121 100644 --- a/close.go +++ b/close.go @@ -239,11 +239,11 @@ func (c *Conn) waitGoroutines() error { } c.closeReadMu.Lock() - ctx := c.closeReadCtx + closeRead := c.closeReadCtx != nil c.closeReadMu.Unlock() - if ctx != nil { + if closeRead { select { - case <-ctx.Done(): + case <-c.closeReadDone: case <-t.C: return errors.New("failed to wait for close read goroutine to exit") } diff --git a/conn.go b/conn.go index f5da573a..8690fb3b 100644 --- a/conn.go +++ b/conn.go @@ -57,10 +57,10 @@ type Conn struct { timeoutLoopDone chan struct{} // Read state. - readMu *mu - readHeaderBuf [8]byte - readControlBuf [maxControlPayload]byte - msgReader *msgReader + readMu *mu + readHeaderBuf [8]byte + readControlBuf [maxControlPayload]byte + msgReader *msgReader // Write state. msgWriter *msgWriter @@ -69,8 +69,9 @@ type Conn struct { writeHeaderBuf [8]byte writeHeader header - closeReadMu sync.Mutex - closeReadCtx context.Context + closeReadMu sync.Mutex + closeReadCtx context.Context + closeReadDone chan struct{} closed chan struct{} closeMu sync.Mutex diff --git a/mask_asm.go b/mask_asm.go index 60c0290f..f9484b5b 100644 --- a/mask_asm.go +++ b/mask_asm.go @@ -3,7 +3,7 @@ package websocket func mask(b []byte, key uint32) uint32 { - // TODO: Will enable in v1.9.0. + // TODO: Will enable in v1.9.0. return maskGo(b, key) /* if len(b) > 0 { diff --git a/read.go b/read.go index 5df031ca..a59e71d9 100644 --- a/read.go +++ b/read.go @@ -71,9 +71,11 @@ func (c *Conn) CloseRead(ctx context.Context) context.Context { } ctx, cancel := context.WithCancel(ctx) c.closeReadCtx = ctx + c.closeReadDone = make(chan struct{}) c.closeReadMu.Unlock() go func() { + defer close(c.closeReadDone) defer cancel() defer c.close() _, _, err := c.Reader(ctx) From 43abf8ed82390611189393dd2547ed9bd675a956 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 7 Apr 2024 08:18:44 -0700 Subject: [PATCH 481/519] README.md: Revert assembly change for now --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cde3ec6d..d093746d 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,6 @@ go get nhooyr.io/websocket - [RFC 7692](https://tools.ietf.org/html/rfc7692) permessage-deflate compression - [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper for write only connections - Compile to [Wasm](https://pkg.go.dev/nhooyr.io/websocket#hdr-Wasm) -- WebSocket masking implemented in assembly for amd64 and arm64 [#326](https://github.com/nhooyr/websocket/issues/326) ## Roadmap @@ -37,6 +36,8 @@ See GitHub issues for minor issues but the major future enhancements are: - [ ] Ping pong heartbeat helper [#267](https://github.com/nhooyr/websocket/issues/267) - [ ] Ping pong instrumentation callbacks [#246](https://github.com/nhooyr/websocket/issues/246) - [ ] Graceful shutdown helpers [#209](https://github.com/nhooyr/websocket/issues/209) +- [ ] Assembly for WebSocket masking [#16](https://github.com/nhooyr/websocket/issues/16) + - WIP at [#326](https://github.com/nhooyr/websocket/pull/326), about 3x faster - [ ] HTTP/2 [#4](https://github.com/nhooyr/websocket/issues/4) - [ ] The holy grail [#402](https://github.com/nhooyr/websocket/issues/402) @@ -120,8 +121,9 @@ Advantages of nhooyr.io/websocket: - Gorilla requires registering a pong callback before sending a Ping - Can target Wasm ([gorilla/websocket#432](https://github.com/gorilla/websocket/issues/432)) - Transparent message buffer reuse with [wsjson](https://pkg.go.dev/nhooyr.io/websocket/wsjson) subpackage -- [3.5x](https://github.com/nhooyr/websocket/pull/326#issuecomment-1959470758) faster WebSocket masking implementation in assembly for amd64 and arm64 and [2x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster implementation in pure Go +- [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster WebSocket masking implementation in pure Go - Gorilla's implementation is slower and uses [unsafe](https://golang.org/pkg/unsafe/). + Soon we'll have assembly and be 3x faster [#326](https://github.com/nhooyr/websocket/pull/326) - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support - Gorilla only supports no context takeover mode - [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper for write only connections ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492)) From e87d61ad4a680ccb5d0fabcdf5dceb2e154ece64 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 7 Apr 2024 09:12:54 -0700 Subject: [PATCH 482/519] Misc fixes for release --- .github/workflows/ci.yml | 8 ++++---- .github/workflows/daily.yml | 8 ++++---- close.go | 2 +- conn_test.go | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c650580..e9b4b5f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - run: ./ci/fmt.sh @@ -19,7 +19,7 @@ jobs: steps: - uses: actions/checkout@v4 - run: go version - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - run: ./ci/lint.sh @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - run: ./ci/test.sh @@ -41,7 +41,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - run: ./ci/bench.sh diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index 340de501..2ba9ce34 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - run: AUTOBAHN=1 ./ci/bench.sh @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - run: AUTOBAHN=1 ./ci/test.sh @@ -34,7 +34,7 @@ jobs: - uses: actions/checkout@v4 with: ref: dev - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - run: AUTOBAHN=1 ./ci/bench.sh @@ -44,7 +44,7 @@ jobs: - uses: actions/checkout@v4 with: ref: dev - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - run: AUTOBAHN=1 ./ci/test.sh diff --git a/close.go b/close.go index 625ed121..31504b0e 100644 --- a/close.go +++ b/close.go @@ -131,7 +131,7 @@ func (c *Conn) Close(code StatusCode, reason string) (err error) { // CloseNow closes the WebSocket connection without attempting a close handshake. // Use when you do not want the overhead of the close handshake. func (c *Conn) CloseNow() (err error) { - defer errd.Wrap(&err, "failed to close WebSocket") + defer errd.Wrap(&err, "failed to immediately close WebSocket") if !c.casClosing() { err = c.waitGoroutines() diff --git a/conn_test.go b/conn_test.go index 9fbe961d..2b44ad22 100644 --- a/conn_test.go +++ b/conn_test.go @@ -346,7 +346,7 @@ func TestConn(t *testing.T) { func TestWasm(t *testing.T) { t.Parallel() if os.Getenv("CI") == "" { - t.Skip() + t.SkipNow() } s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { From afe94af9aa98d974da157da151c15817bc9d85d0 Mon Sep 17 00:00:00 2001 From: Jacob Date: Tue, 9 Apr 2024 20:29:44 +0200 Subject: [PATCH 483/519] Use new atomic types from Go 1.19 This is a cleaner solution for the fix in #438 thanks to the fact that Go 1.19 now is the default and the atomic.Int64 types are automatically aligned correctly on 32 bit systems. Using this also means that xsync.Int64 can be removed. The new atomic.Int64 type solves the issue and should be quite a lot faster as it avoids the interface conversion. --- conn.go | 4 ++-- internal/xsync/int64.go | 23 ----------------------- netconn.go | 41 +++++++++++++++++++---------------------- read.go | 4 ++-- ws_js.go | 4 ++-- 5 files changed, 25 insertions(+), 51 deletions(-) delete mode 100644 internal/xsync/int64.go diff --git a/conn.go b/conn.go index 8690fb3b..48bc510a 100644 --- a/conn.go +++ b/conn.go @@ -77,7 +77,7 @@ type Conn struct { closeMu sync.Mutex closing bool - pingCounter int32 + pingCounter atomic.Int32 activePingsMu sync.Mutex activePings map[string]chan<- struct{} } @@ -200,7 +200,7 @@ func (c *Conn) flate() bool { // // TCP Keepalives should suffice for most use cases. func (c *Conn) Ping(ctx context.Context) error { - p := atomic.AddInt32(&c.pingCounter, 1) + p := c.pingCounter.Add(1) err := c.ping(ctx, strconv.Itoa(int(p))) if err != nil { diff --git a/internal/xsync/int64.go b/internal/xsync/int64.go deleted file mode 100644 index a0c40204..00000000 --- a/internal/xsync/int64.go +++ /dev/null @@ -1,23 +0,0 @@ -package xsync - -import ( - "sync/atomic" -) - -// Int64 represents an atomic int64. -type Int64 struct { - // We do not use atomic.Load/StoreInt64 since it does not - // work on 32 bit computers but we need 64 bit integers. - i atomic.Value -} - -// Load loads the int64. -func (v *Int64) Load() int64 { - i, _ := v.i.Load().(int64) - return i -} - -// Store stores the int64. -func (v *Int64) Store(i int64) { - v.i.Store(i) -} diff --git a/netconn.go b/netconn.go index 86f7dadb..b118e4d3 100644 --- a/netconn.go +++ b/netconn.go @@ -68,7 +68,7 @@ func NetConn(ctx context.Context, c *Conn, msgType MessageType) net.Conn { defer nc.writeMu.unlock() // Prevents future writes from writing until the deadline is reset. - atomic.StoreInt64(&nc.writeExpired, 1) + nc.writeExpired.Store(1) }) if !nc.writeTimer.Stop() { <-nc.writeTimer.C @@ -84,7 +84,7 @@ func NetConn(ctx context.Context, c *Conn, msgType MessageType) net.Conn { defer nc.readMu.unlock() // Prevents future reads from reading until the deadline is reset. - atomic.StoreInt64(&nc.readExpired, 1) + nc.readExpired.Store(1) }) if !nc.readTimer.Stop() { <-nc.readTimer.C @@ -94,25 +94,22 @@ func NetConn(ctx context.Context, c *Conn, msgType MessageType) net.Conn { } type netConn struct { - // These must be first to be aligned on 32 bit platforms. - // https://github.com/nhooyr/websocket/pull/438 - readExpired int64 - writeExpired int64 - c *Conn msgType MessageType - writeTimer *time.Timer - writeMu *mu - writeCtx context.Context - writeCancel context.CancelFunc - - readTimer *time.Timer - readMu *mu - readCtx context.Context - readCancel context.CancelFunc - readEOFed bool - reader io.Reader + writeTimer *time.Timer + writeMu *mu + writeExpired atomic.Int64 + writeCtx context.Context + writeCancel context.CancelFunc + + readTimer *time.Timer + readMu *mu + readExpired atomic.Int64 + readCtx context.Context + readCancel context.CancelFunc + readEOFed bool + reader io.Reader } var _ net.Conn = &netConn{} @@ -129,7 +126,7 @@ func (nc *netConn) Write(p []byte) (int, error) { nc.writeMu.forceLock() defer nc.writeMu.unlock() - if atomic.LoadInt64(&nc.writeExpired) == 1 { + if nc.writeExpired.Load() == 1 { return 0, fmt.Errorf("failed to write: %w", context.DeadlineExceeded) } @@ -157,7 +154,7 @@ func (nc *netConn) Read(p []byte) (int, error) { } func (nc *netConn) read(p []byte) (int, error) { - if atomic.LoadInt64(&nc.readExpired) == 1 { + if nc.readExpired.Load() == 1 { return 0, fmt.Errorf("failed to read: %w", context.DeadlineExceeded) } @@ -209,7 +206,7 @@ func (nc *netConn) SetDeadline(t time.Time) error { } func (nc *netConn) SetWriteDeadline(t time.Time) error { - atomic.StoreInt64(&nc.writeExpired, 0) + nc.writeExpired.Store(0) if t.IsZero() { nc.writeTimer.Stop() } else { @@ -223,7 +220,7 @@ func (nc *netConn) SetWriteDeadline(t time.Time) error { } func (nc *netConn) SetReadDeadline(t time.Time) error { - atomic.StoreInt64(&nc.readExpired, 0) + nc.readExpired.Store(0) if t.IsZero() { nc.readTimer.Stop() } else { diff --git a/read.go b/read.go index a59e71d9..20ed9408 100644 --- a/read.go +++ b/read.go @@ -11,11 +11,11 @@ import ( "io" "net" "strings" + "sync/atomic" "time" "nhooyr.io/websocket/internal/errd" "nhooyr.io/websocket/internal/util" - "nhooyr.io/websocket/internal/xsync" ) // Reader reads from the connection until there is a WebSocket @@ -465,7 +465,7 @@ func (mr *msgReader) read(p []byte) (int, error) { type limitReader struct { c *Conn r io.Reader - limit xsync.Int64 + limit atomic.Int64 n int64 } diff --git a/ws_js.go b/ws_js.go index 02d61f28..6e58329e 100644 --- a/ws_js.go +++ b/ws_js.go @@ -12,11 +12,11 @@ import ( "runtime" "strings" "sync" + "sync/atomic" "syscall/js" "nhooyr.io/websocket/internal/bpool" "nhooyr.io/websocket/internal/wsjs" - "nhooyr.io/websocket/internal/xsync" ) // opcode represents a WebSocket opcode. @@ -45,7 +45,7 @@ type Conn struct { ws wsjs.WebSocket // read limit for a message in bytes. - msgReadLimit xsync.Int64 + msgReadLimit atomic.Int64 closeReadMu sync.Mutex closeReadCtx context.Context From cfde4a5ebfd40869983e926c9098e12f82761740 Mon Sep 17 00:00:00 2001 From: Jacob Date: Thu, 11 Apr 2024 10:35:07 +0200 Subject: [PATCH 484/519] Use Int64 instead of Int32 for counting pings --- conn.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/conn.go b/conn.go index 48bc510a..d7434a9d 100644 --- a/conn.go +++ b/conn.go @@ -77,7 +77,7 @@ type Conn struct { closeMu sync.Mutex closing bool - pingCounter atomic.Int32 + pingCounter atomic.Int64 activePingsMu sync.Mutex activePings map[string]chan<- struct{} } @@ -202,7 +202,7 @@ func (c *Conn) flate() bool { func (c *Conn) Ping(ctx context.Context) error { p := c.pingCounter.Add(1) - err := c.ping(ctx, strconv.Itoa(int(p))) + err := c.ping(ctx, strconv.FormatInt(p, 10)) if err != nil { return fmt.Errorf("failed to ping: %w", err) } From c2e0c41f803b936da8450869dcc3419f2b96342c Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Fri, 9 Aug 2024 13:43:37 -0500 Subject: [PATCH 485/519] Update import path to github.com/coder/websocket This will create easily solved breakage on updates but I figure it's best to pull the band aid off as early as possible --- README.md | 42 +++++++++++++-------------- accept.go | 2 +- accept_test.go | 4 +-- autobahn_test.go | 10 +++---- close.go | 2 +- close_test.go | 2 +- compress_test.go | 4 +-- conn_test.go | 14 ++++----- dial.go | 2 +- dial_test.go | 8 ++--- doc.go | 4 +-- example_test.go | 4 +-- export_test.go | 2 +- frame.go | 2 +- frame_test.go | 2 +- go.mod | 2 +- internal/examples/chat/README.md | 2 +- internal/examples/chat/chat.go | 2 +- internal/examples/chat/chat_test.go | 2 +- internal/examples/chat/index.html | 2 +- internal/examples/echo/README.md | 2 +- internal/examples/echo/server.go | 2 +- internal/examples/echo/server_test.go | 4 +-- internal/examples/go.mod | 6 ++-- internal/test/wstest/echo.go | 6 ++-- internal/test/wstest/pipe.go | 2 +- internal/thirdparty/frame_test.go | 6 ++-- internal/thirdparty/gin_test.go | 10 +++---- internal/thirdparty/go.mod | 6 ++-- internal/xsync/go_test.go | 2 +- mask_test.go | 2 +- read.go | 6 ++-- write.go | 4 +-- ws_js.go | 8 ++--- ws_js_test.go | 6 ++-- wsjson/wsjson.go | 10 +++---- wsjson/wsjson_test.go | 2 +- 37 files changed, 99 insertions(+), 99 deletions(-) diff --git a/README.md b/README.md index d093746d..c3502fa2 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,14 @@ # websocket -[![Go Reference](https://pkg.go.dev/badge/nhooyr.io/websocket.svg)](https://pkg.go.dev/nhooyr.io/websocket) -[![Go Coverage](https://img.shields.io/badge/coverage-91%25-success)](https://nhooyr.io/websocket/coverage.html) +[![Go Reference](https://pkg.go.dev/badge/github.com/coder/websocket.svg)](https://pkg.go.dev/github.com/coder/websocket) +[![Go Coverage](https://img.shields.io/badge/coverage-91%25-success)](https://github.com/coder/websocket/coverage.html) websocket is a minimal and idiomatic WebSocket library for Go. ## Install ```sh -go get nhooyr.io/websocket +go get github.com/coder/websocket ``` ## Highlights @@ -16,16 +16,16 @@ go get nhooyr.io/websocket - Minimal and idiomatic API - First class [context.Context](https://blog.golang.org/context) support - Fully passes the WebSocket [autobahn-testsuite](https://github.com/crossbario/autobahn-testsuite) -- [Zero dependencies](https://pkg.go.dev/nhooyr.io/websocket?tab=imports) -- JSON helpers in the [wsjson](https://pkg.go.dev/nhooyr.io/websocket/wsjson) subpackage +- [Zero dependencies](https://pkg.go.dev/github.com/coder/websocket?tab=imports) +- JSON helpers in the [wsjson](https://pkg.go.dev/github.com/coder/websocket/wsjson) subpackage - Zero alloc reads and writes - Concurrent writes -- [Close handshake](https://pkg.go.dev/nhooyr.io/websocket#Conn.Close) -- [net.Conn](https://pkg.go.dev/nhooyr.io/websocket#NetConn) wrapper -- [Ping pong](https://pkg.go.dev/nhooyr.io/websocket#Conn.Ping) API +- [Close handshake](https://pkg.go.dev/github.com/coder/websocket#Conn.Close) +- [net.Conn](https://pkg.go.dev/github.com/coder/websocket#NetConn) wrapper +- [Ping pong](https://pkg.go.dev/github.com/coder/websocket#Conn.Ping) API - [RFC 7692](https://tools.ietf.org/html/rfc7692) permessage-deflate compression -- [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper for write only connections -- Compile to [Wasm](https://pkg.go.dev/nhooyr.io/websocket#hdr-Wasm) +- [CloseRead](https://pkg.go.dev/github.com/coder/websocket#Conn.CloseRead) helper for write only connections +- Compile to [Wasm](https://pkg.go.dev/github.com/coder/websocket#hdr-Wasm) ## Roadmap @@ -102,14 +102,14 @@ Advantages of [gorilla/websocket](https://github.com/gorilla/websocket): - Mature and widely used - [Prepared writes](https://pkg.go.dev/github.com/gorilla/websocket#PreparedMessage) - Configurable [buffer sizes](https://pkg.go.dev/github.com/gorilla/websocket#hdr-Buffers) -- No extra goroutine per connection to support cancellation with context.Context. This costs nhooyr.io/websocket 2 KB of memory per connection. +- No extra goroutine per connection to support cancellation with context.Context. This costs github.com/coder/websocket 2 KB of memory per connection. - Will be removed soon with [context.AfterFunc](https://github.com/golang/go/issues/57928). See [#411](https://github.com/nhooyr/websocket/issues/411) -Advantages of nhooyr.io/websocket: +Advantages of github.com/coder/websocket: - Minimal and idiomatic API - - 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. -- [net.Conn](https://pkg.go.dev/nhooyr.io/websocket#NetConn) wrapper + - Compare godoc of [github.com/coder/websocket](https://pkg.go.dev/github.com/coder/websocket) with [gorilla/websocket](https://pkg.go.dev/github.com/gorilla/websocket) side by side. +- [net.Conn](https://pkg.go.dev/github.com/coder/websocket#NetConn) wrapper - Zero alloc reads and writes ([gorilla/websocket#535](https://github.com/gorilla/websocket/issues/535)) - Full [context.Context](https://blog.golang.org/context) support - Dial uses [net/http.Client](https://golang.org/pkg/net/http/#Client) @@ -117,24 +117,24 @@ Advantages of nhooyr.io/websocket: - Gorilla writes directly to a net.Conn and so duplicates features of net/http.Client. - Concurrent writes - Close handshake ([gorilla/websocket#448](https://github.com/gorilla/websocket/issues/448)) -- Idiomatic [ping pong](https://pkg.go.dev/nhooyr.io/websocket#Conn.Ping) API +- Idiomatic [ping pong](https://pkg.go.dev/github.com/coder/websocket#Conn.Ping) API - Gorilla requires registering a pong callback before sending a Ping - Can target Wasm ([gorilla/websocket#432](https://github.com/gorilla/websocket/issues/432)) -- Transparent message buffer reuse with [wsjson](https://pkg.go.dev/nhooyr.io/websocket/wsjson) subpackage +- Transparent message buffer reuse with [wsjson](https://pkg.go.dev/github.com/coder/websocket/wsjson) subpackage - [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster WebSocket masking implementation in pure Go - Gorilla's implementation is slower and uses [unsafe](https://golang.org/pkg/unsafe/). Soon we'll have assembly and be 3x faster [#326](https://github.com/nhooyr/websocket/pull/326) - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support - Gorilla only supports no context takeover mode -- [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper for write only connections ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492)) +- [CloseRead](https://pkg.go.dev/github.com/coder/websocket#Conn.CloseRead) helper for write only connections ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492)) #### golang.org/x/net/websocket [golang.org/x/net/websocket](https://pkg.go.dev/golang.org/x/net/websocket) is deprecated. See [golang/go/issues/18152](https://github.com/golang/go/issues/18152). -The [net.Conn](https://pkg.go.dev/nhooyr.io/websocket#NetConn) can help in transitioning -to nhooyr.io/websocket. +The [net.Conn](https://pkg.go.dev/github.com/coder/websocket#NetConn) can help in transitioning +to github.com/coder/websocket. #### gobwas/ws @@ -143,7 +143,7 @@ in an event driven style for performance. See the author's [blog post](https://m However it is quite bloated. See https://pkg.go.dev/github.com/gobwas/ws -When writing idiomatic Go, nhooyr.io/websocket will be faster and easier to use. +When writing idiomatic Go, github.com/coder/websocket will be faster and easier to use. #### lesismal/nbio @@ -152,4 +152,4 @@ event driven for performance reasons. However it is quite bloated. See https://pkg.go.dev/github.com/lesismal/nbio -When writing idiomatic Go, nhooyr.io/websocket will be faster and easier to use. +When writing idiomatic Go, github.com/coder/websocket will be faster and easier to use. diff --git a/accept.go b/accept.go index 285b3103..f672a730 100644 --- a/accept.go +++ b/accept.go @@ -17,7 +17,7 @@ import ( "path/filepath" "strings" - "nhooyr.io/websocket/internal/errd" + "github.com/coder/websocket/internal/errd" ) // AcceptOptions represents Accept's options. diff --git a/accept_test.go b/accept_test.go index 18233b1e..4f799126 100644 --- a/accept_test.go +++ b/accept_test.go @@ -13,8 +13,8 @@ import ( "sync" "testing" - "nhooyr.io/websocket/internal/test/assert" - "nhooyr.io/websocket/internal/test/xrand" + "github.com/coder/websocket/internal/test/assert" + "github.com/coder/websocket/internal/test/xrand" ) func TestAccept(t *testing.T) { diff --git a/autobahn_test.go b/autobahn_test.go index 57ceebd5..b1b3a7e9 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -17,11 +17,11 @@ import ( "testing" "time" - "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/errd" - "nhooyr.io/websocket/internal/test/assert" - "nhooyr.io/websocket/internal/test/wstest" - "nhooyr.io/websocket/internal/util" + "github.com/coder/websocket" + "github.com/coder/websocket/internal/errd" + "github.com/coder/websocket/internal/test/assert" + "github.com/coder/websocket/internal/test/wstest" + "github.com/coder/websocket/internal/util" ) var excludedAutobahnCases = []string{ diff --git a/close.go b/close.go index 31504b0e..ff2e878a 100644 --- a/close.go +++ b/close.go @@ -11,7 +11,7 @@ import ( "net" "time" - "nhooyr.io/websocket/internal/errd" + "github.com/coder/websocket/internal/errd" ) // StatusCode represents a WebSocket status code. diff --git a/close_test.go b/close_test.go index 6bf3c256..aec582c1 100644 --- a/close_test.go +++ b/close_test.go @@ -9,7 +9,7 @@ import ( "strings" "testing" - "nhooyr.io/websocket/internal/test/assert" + "github.com/coder/websocket/internal/test/assert" ) func TestCloseError(t *testing.T) { diff --git a/compress_test.go b/compress_test.go index 667e1408..d97492cf 100644 --- a/compress_test.go +++ b/compress_test.go @@ -10,8 +10,8 @@ import ( "strings" "testing" - "nhooyr.io/websocket/internal/test/assert" - "nhooyr.io/websocket/internal/test/xrand" + "github.com/coder/websocket/internal/test/assert" + "github.com/coder/websocket/internal/test/xrand" ) func Test_slidingWindow(t *testing.T) { diff --git a/conn_test.go b/conn_test.go index 2b44ad22..be7c9983 100644 --- a/conn_test.go +++ b/conn_test.go @@ -16,13 +16,13 @@ import ( "testing" "time" - "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/errd" - "nhooyr.io/websocket/internal/test/assert" - "nhooyr.io/websocket/internal/test/wstest" - "nhooyr.io/websocket/internal/test/xrand" - "nhooyr.io/websocket/internal/xsync" - "nhooyr.io/websocket/wsjson" + "github.com/coder/websocket" + "github.com/coder/websocket/internal/errd" + "github.com/coder/websocket/internal/test/assert" + "github.com/coder/websocket/internal/test/wstest" + "github.com/coder/websocket/internal/test/xrand" + "github.com/coder/websocket/internal/xsync" + "github.com/coder/websocket/wsjson" ) func TestConn(t *testing.T) { diff --git a/dial.go b/dial.go index e4c4daa1..ad61a35d 100644 --- a/dial.go +++ b/dial.go @@ -17,7 +17,7 @@ import ( "sync" "time" - "nhooyr.io/websocket/internal/errd" + "github.com/coder/websocket/internal/errd" ) // DialOptions represents Dial's options. diff --git a/dial_test.go b/dial_test.go index 237a2874..f94cd73b 100644 --- a/dial_test.go +++ b/dial_test.go @@ -15,10 +15,10 @@ import ( "testing" "time" - "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/test/assert" - "nhooyr.io/websocket/internal/util" - "nhooyr.io/websocket/internal/xsync" + "github.com/coder/websocket" + "github.com/coder/websocket/internal/test/assert" + "github.com/coder/websocket/internal/util" + "github.com/coder/websocket/internal/xsync" ) func TestBadDials(t *testing.T) { diff --git a/doc.go b/doc.go index 2ab648a6..03edf129 100644 --- a/doc.go +++ b/doc.go @@ -15,7 +15,7 @@ // // The wsjson subpackage contain helpers for JSON and protobuf messages. // -// More documentation at https://nhooyr.io/websocket. +// More documentation at https://github.com/coder/websocket. // // # Wasm // @@ -31,4 +31,4 @@ // - Conn.CloseNow is Close(StatusGoingAway, "") // - HTTPClient, HTTPHeader and CompressionMode in DialOptions are no-op // - *http.Response from Dial is &http.Response{} with a 101 status code on success -package websocket // import "nhooyr.io/websocket" +package websocket // import "github.com/coder/websocket" diff --git a/example_test.go b/example_test.go index 590c0411..4cc0cf11 100644 --- a/example_test.go +++ b/example_test.go @@ -6,8 +6,8 @@ import ( "net/http" "time" - "nhooyr.io/websocket" - "nhooyr.io/websocket/wsjson" + "github.com/coder/websocket" + "github.com/coder/websocket/wsjson" ) func ExampleAccept() { diff --git a/export_test.go b/export_test.go index a644d8f0..d3443991 100644 --- a/export_test.go +++ b/export_test.go @@ -6,7 +6,7 @@ package websocket import ( "net" - "nhooyr.io/websocket/internal/util" + "github.com/coder/websocket/internal/util" ) func (c *Conn) RecordBytesWritten() *int { diff --git a/frame.go b/frame.go index d5631863..e7ab76be 100644 --- a/frame.go +++ b/frame.go @@ -9,7 +9,7 @@ import ( "io" "math" - "nhooyr.io/websocket/internal/errd" + "github.com/coder/websocket/internal/errd" ) // opcode represents a WebSocket opcode. diff --git a/frame_test.go b/frame_test.go index bd626358..08874cb5 100644 --- a/frame_test.go +++ b/frame_test.go @@ -13,7 +13,7 @@ import ( "testing" "time" - "nhooyr.io/websocket/internal/test/assert" + "github.com/coder/websocket/internal/test/assert" ) func TestHeader(t *testing.T) { diff --git a/go.mod b/go.mod index 715a9f7a..336411a5 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module nhooyr.io/websocket +module github.com/coder/websocket go 1.19 diff --git a/internal/examples/chat/README.md b/internal/examples/chat/README.md index 574c6994..4d354586 100644 --- a/internal/examples/chat/README.md +++ b/internal/examples/chat/README.md @@ -1,6 +1,6 @@ # Chat Example -This directory contains a full stack example of a simple chat webapp using nhooyr.io/websocket. +This directory contains a full stack example of a simple chat webapp using github.com/coder/websocket. ```bash $ cd examples/chat diff --git a/internal/examples/chat/chat.go b/internal/examples/chat/chat.go index 8b1e30c1..3cb1e021 100644 --- a/internal/examples/chat/chat.go +++ b/internal/examples/chat/chat.go @@ -12,7 +12,7 @@ import ( "golang.org/x/time/rate" - "nhooyr.io/websocket" + "github.com/coder/websocket" ) // chatServer enables broadcasting to a set of subscribers. diff --git a/internal/examples/chat/chat_test.go b/internal/examples/chat/chat_test.go index f80f1de1..8eb72051 100644 --- a/internal/examples/chat/chat_test.go +++ b/internal/examples/chat/chat_test.go @@ -14,7 +14,7 @@ import ( "golang.org/x/time/rate" - "nhooyr.io/websocket" + "github.com/coder/websocket" ) func Test_chatServer(t *testing.T) { diff --git a/internal/examples/chat/index.html b/internal/examples/chat/index.html index 64edd286..7038342d 100644 --- a/internal/examples/chat/index.html +++ b/internal/examples/chat/index.html @@ -2,7 +2,7 @@ - nhooyr.io/websocket - Chat Example + github.com/coder/websocket - Chat Example diff --git a/internal/examples/echo/README.md b/internal/examples/echo/README.md index ac03f640..3abbbb57 100644 --- a/internal/examples/echo/README.md +++ b/internal/examples/echo/README.md @@ -1,6 +1,6 @@ # Echo Example -This directory contains a echo server example using nhooyr.io/websocket. +This directory contains a echo server example using github.com/coder/websocket. ```bash $ cd examples/echo diff --git a/internal/examples/echo/server.go b/internal/examples/echo/server.go index 246ad582..a44d20b5 100644 --- a/internal/examples/echo/server.go +++ b/internal/examples/echo/server.go @@ -9,7 +9,7 @@ import ( "golang.org/x/time/rate" - "nhooyr.io/websocket" + "github.com/coder/websocket" ) // echoServer is the WebSocket echo server implementation. diff --git a/internal/examples/echo/server_test.go b/internal/examples/echo/server_test.go index 9b608301..81e8cfc2 100644 --- a/internal/examples/echo/server_test.go +++ b/internal/examples/echo/server_test.go @@ -6,8 +6,8 @@ import ( "testing" "time" - "nhooyr.io/websocket" - "nhooyr.io/websocket/wsjson" + "github.com/coder/websocket" + "github.com/coder/websocket/wsjson" ) // Test_echoServer tests the echoServer by sending it 5 different messages diff --git a/internal/examples/go.mod b/internal/examples/go.mod index c98b81ce..4f7a8a70 100644 --- a/internal/examples/go.mod +++ b/internal/examples/go.mod @@ -1,10 +1,10 @@ -module nhooyr.io/websocket/examples +module github.com/coder/websocket/examples go 1.19 -replace nhooyr.io/websocket => ../.. +replace github.com/coder/websocket => ../.. require ( + github.com/coder/websocket v0.0.0-00010101000000-000000000000 golang.org/x/time v0.3.0 - nhooyr.io/websocket v0.0.0-00010101000000-000000000000 ) diff --git a/internal/test/wstest/echo.go b/internal/test/wstest/echo.go index dc21a8f0..c0c8dcd7 100644 --- a/internal/test/wstest/echo.go +++ b/internal/test/wstest/echo.go @@ -7,9 +7,9 @@ import ( "io" "time" - "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/test/xrand" - "nhooyr.io/websocket/internal/xsync" + "github.com/coder/websocket" + "github.com/coder/websocket/internal/test/xrand" + "github.com/coder/websocket/internal/xsync" ) // EchoLoop echos every msg received from c until an error diff --git a/internal/test/wstest/pipe.go b/internal/test/wstest/pipe.go index 8e1deb47..b8cf094d 100644 --- a/internal/test/wstest/pipe.go +++ b/internal/test/wstest/pipe.go @@ -10,7 +10,7 @@ import ( "net/http" "net/http/httptest" - "nhooyr.io/websocket" + "github.com/coder/websocket" ) // Pipe is used to create an in memory connection diff --git a/internal/thirdparty/frame_test.go b/internal/thirdparty/frame_test.go index 89042e53..75b05291 100644 --- a/internal/thirdparty/frame_test.go +++ b/internal/thirdparty/frame_test.go @@ -11,7 +11,7 @@ import ( _ "github.com/gorilla/websocket" _ "github.com/lesismal/nbio/nbhttp/websocket" - _ "nhooyr.io/websocket" + _ "github.com/coder/websocket" ) func basicMask(b []byte, maskKey [4]byte, pos int) int { @@ -22,10 +22,10 @@ func basicMask(b []byte, maskKey [4]byte, pos int) int { return pos & 3 } -//go:linkname maskGo nhooyr.io/websocket.maskGo +//go:linkname maskGo github.com/coder/websocket.maskGo func maskGo(b []byte, key32 uint32) int -//go:linkname maskAsm nhooyr.io/websocket.maskAsm +//go:linkname maskAsm github.com/coder/websocket.maskAsm func maskAsm(b *byte, len int, key32 uint32) uint32 //go:linkname nbioMaskBytes github.com/lesismal/nbio/nbhttp/websocket.maskXOR diff --git a/internal/thirdparty/gin_test.go b/internal/thirdparty/gin_test.go index 6d59578d..bd30ebdd 100644 --- a/internal/thirdparty/gin_test.go +++ b/internal/thirdparty/gin_test.go @@ -10,11 +10,11 @@ import ( "github.com/gin-gonic/gin" - "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/errd" - "nhooyr.io/websocket/internal/test/assert" - "nhooyr.io/websocket/internal/test/wstest" - "nhooyr.io/websocket/wsjson" + "github.com/coder/websocket" + "github.com/coder/websocket/internal/errd" + "github.com/coder/websocket/internal/test/assert" + "github.com/coder/websocket/internal/test/wstest" + "github.com/coder/websocket/wsjson" ) func TestGin(t *testing.T) { diff --git a/internal/thirdparty/go.mod b/internal/thirdparty/go.mod index d991dd64..d946ffae 100644 --- a/internal/thirdparty/go.mod +++ b/internal/thirdparty/go.mod @@ -1,15 +1,15 @@ -module nhooyr.io/websocket/internal/thirdparty +module github.com/coder/websocket/internal/thirdparty go 1.19 -replace nhooyr.io/websocket => ../.. +replace github.com/coder/websocket => ../.. require ( + github.com/coder/websocket v0.0.0-00010101000000-000000000000 github.com/gin-gonic/gin v1.9.1 github.com/gobwas/ws v1.3.0 github.com/gorilla/websocket v1.5.0 github.com/lesismal/nbio v1.3.18 - nhooyr.io/websocket v0.0.0-00010101000000-000000000000 ) require ( diff --git a/internal/xsync/go_test.go b/internal/xsync/go_test.go index dabea8a5..a3f7053b 100644 --- a/internal/xsync/go_test.go +++ b/internal/xsync/go_test.go @@ -3,7 +3,7 @@ package xsync import ( "testing" - "nhooyr.io/websocket/internal/test/assert" + "github.com/coder/websocket/internal/test/assert" ) func TestGoRecover(t *testing.T) { diff --git a/mask_test.go b/mask_test.go index 54f55e43..00a9f0a2 100644 --- a/mask_test.go +++ b/mask_test.go @@ -8,7 +8,7 @@ import ( "math/bits" "testing" - "nhooyr.io/websocket/internal/test/assert" + "github.com/coder/websocket/internal/test/assert" ) func basicMask(b []byte, key uint32) uint32 { diff --git a/read.go b/read.go index a59e71d9..1b9404b8 100644 --- a/read.go +++ b/read.go @@ -13,9 +13,9 @@ import ( "strings" "time" - "nhooyr.io/websocket/internal/errd" - "nhooyr.io/websocket/internal/util" - "nhooyr.io/websocket/internal/xsync" + "github.com/coder/websocket/internal/errd" + "github.com/coder/websocket/internal/util" + "github.com/coder/websocket/internal/xsync" ) // Reader reads from the connection until there is a WebSocket diff --git a/write.go b/write.go index d7222f2d..e294a680 100644 --- a/write.go +++ b/write.go @@ -16,8 +16,8 @@ import ( "compress/flate" - "nhooyr.io/websocket/internal/errd" - "nhooyr.io/websocket/internal/util" + "github.com/coder/websocket/internal/errd" + "github.com/coder/websocket/internal/util" ) // Writer returns a writer bounded by the context that will write diff --git a/ws_js.go b/ws_js.go index 02d61f28..a8de0c63 100644 --- a/ws_js.go +++ b/ws_js.go @@ -1,4 +1,4 @@ -package websocket // import "nhooyr.io/websocket" +package websocket // import "github.com/coder/websocket" import ( "bytes" @@ -14,9 +14,9 @@ import ( "sync" "syscall/js" - "nhooyr.io/websocket/internal/bpool" - "nhooyr.io/websocket/internal/wsjs" - "nhooyr.io/websocket/internal/xsync" + "github.com/coder/websocket/internal/bpool" + "github.com/coder/websocket/internal/wsjs" + "github.com/coder/websocket/internal/xsync" ) // opcode represents a WebSocket opcode. diff --git a/ws_js_test.go b/ws_js_test.go index ba98b9a0..b56ad16b 100644 --- a/ws_js_test.go +++ b/ws_js_test.go @@ -7,9 +7,9 @@ import ( "testing" "time" - "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/test/assert" - "nhooyr.io/websocket/internal/test/wstest" + "github.com/coder/websocket" + "github.com/coder/websocket/internal/test/assert" + "github.com/coder/websocket/internal/test/wstest" ) func TestWasm(t *testing.T) { diff --git a/wsjson/wsjson.go b/wsjson/wsjson.go index 7c986a0d..05e7cfa1 100644 --- a/wsjson/wsjson.go +++ b/wsjson/wsjson.go @@ -1,15 +1,15 @@ // Package wsjson provides helpers for reading and writing JSON messages. -package wsjson // import "nhooyr.io/websocket/wsjson" +package wsjson // import "github.com/coder/websocket/wsjson" import ( "context" "encoding/json" "fmt" - "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/bpool" - "nhooyr.io/websocket/internal/errd" - "nhooyr.io/websocket/internal/util" + "github.com/coder/websocket" + "github.com/coder/websocket/internal/bpool" + "github.com/coder/websocket/internal/errd" + "github.com/coder/websocket/internal/util" ) // Read reads a JSON message from c into v. diff --git a/wsjson/wsjson_test.go b/wsjson/wsjson_test.go index 080ab38d..87a72854 100644 --- a/wsjson/wsjson_test.go +++ b/wsjson/wsjson_test.go @@ -6,7 +6,7 @@ import ( "strconv" "testing" - "nhooyr.io/websocket/internal/test/xrand" + "github.com/coder/websocket/internal/test/xrand" ) func BenchmarkJSON(b *testing.B) { From 782e5d250b0b078dd77f5500d9d9ed61665a692f Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Fri, 9 Aug 2024 13:50:13 -0500 Subject: [PATCH 486/519] Remove version on lint rule vulncheck was using slices which is only in newer Go versions. --- .github/workflows/ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9b4b5f6..13ddbf3e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,8 +20,6 @@ jobs: - uses: actions/checkout@v4 - run: go version - uses: actions/setup-go@v5 - with: - go-version-file: ./go.mod - run: ./ci/lint.sh test: From 77b2e157ec71d3316af3ac6d4b9a370111e3383a Mon Sep 17 00:00:00 2001 From: Ammar Bandukwala Date: Fri, 9 Aug 2024 14:03:00 -0500 Subject: [PATCH 487/519] Add transfer notice to README --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index c3502fa2..c74b79dd 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,11 @@ websocket is a minimal and idiomatic WebSocket library for Go. go get github.com/coder/websocket ``` +> [!NOTE] +> Coder now maintains this project as explained in [this blog post](https://coder.com/blog/websocket). +> We're grateful to [nhooyr](https://github.com/nhooyr) for authoring and maintaining this project from +> 2019 to 2024. + ## Highlights - Minimal and idiomatic API From 4ae7594800e654c62353c3ed53db5b24a0ef039b Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Thu, 15 Aug 2024 17:01:11 +0000 Subject: [PATCH 488/519] Fix coverage --- .github/workflows/static.yml | 42 ++++++++++++++++++++++++++++++++++++ README.md | 2 +- 2 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/static.yml diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml new file mode 100644 index 00000000..dcde3c4b --- /dev/null +++ b/.github/workflows/static.yml @@ -0,0 +1,42 @@ +name: static + +on: + push: + branches: ['master'] + workflow_dispatch: + +# Set permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages. +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Generate coverage and badge + run: | + ./ci/test.sh + mkdir -p ./ci/out/static + cp ./ci/out/coverage.html ./ci/out/static/coverage.html + percent=$(go tool cover -func ./ci/out/coverage.prof | tail -n1 | awk '{print $3}' | tr -d '%') + wget -O ./ci/out/static/coverage.svg "https://img.shields.io/badge/coverage-${percent}%25-success" + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./ci/out/static/ + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/README.md b/README.md index c74b79dd..44a6ddf0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # websocket [![Go Reference](https://pkg.go.dev/badge/github.com/coder/websocket.svg)](https://pkg.go.dev/github.com/coder/websocket) -[![Go Coverage](https://img.shields.io/badge/coverage-91%25-success)](https://github.com/coder/websocket/coverage.html) +[![Go Coverage](https://coder.github.io/websocket/coverage.svg)](https://coder.github.io/websocket/coverage.html) websocket is a minimal and idiomatic WebSocket library for Go. From aeff634d43162cdc5fe5ffb2ad54003b892f104c Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Thu, 15 Aug 2024 18:57:47 +0000 Subject: [PATCH 489/519] Add setup-go to static.yml --- .github/workflows/static.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index dcde3c4b..bbc03b39 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -26,6 +26,10 @@ jobs: uses: actions/checkout@v4 - name: Setup Pages uses: actions/configure-pages@v5 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: ./go.mod - name: Generate coverage and badge run: | ./ci/test.sh From 0f14077dcfa84bb22fa4f83f84d9f0444c5accfe Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Thu, 15 Aug 2024 19:09:36 +0000 Subject: [PATCH 490/519] Split coverage and deploy jobs in static.yml to avoid env issue --- .github/workflows/static.yml | 34 +++++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index bbc03b39..9b89a520 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -16,27 +16,39 @@ concurrency: cancel-in-progress: true jobs: + coverage: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version-file: ./go.mod + - run: | + ./ci/test.sh + echo "PERCENT=$(go tool cover -func ./ci/out/coverage.prof | tail -n1 | awk '{print $3}' | tr -d '%')" >> "$GITHUB_OUTPUT" + { + echo "HTML<> "$GITHUB_OUTPUT" + deploy: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest + needs: coverage steps: - - name: Checkout - uses: actions/checkout@v4 - name: Setup Pages uses: actions/configure-pages@v5 - - name: Setup Go - uses: actions/setup-go@v5 - with: - go-version-file: ./go.mod - - name: Generate coverage and badge + - name: Write coverage.html and coverage.svg + env: + PERCENT: ${{ steps.coverage.outputs.PERCENT }} + HTML: ${{ steps.coverage.outputs.HTML }} run: | - ./ci/test.sh mkdir -p ./ci/out/static - cp ./ci/out/coverage.html ./ci/out/static/coverage.html - percent=$(go tool cover -func ./ci/out/coverage.prof | tail -n1 | awk '{print $3}' | tr -d '%') - wget -O ./ci/out/static/coverage.svg "https://img.shields.io/badge/coverage-${percent}%25-success" + wget -O ./ci/out/static/coverage.svg "https://img.shields.io/badge/coverage-${PERCENT}%25-success" + echo "$HTML" > ./ci/out/static/coverage.html - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: From dee24acb12f2222f55af7df9503e93b2462a13fa Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Thu, 15 Aug 2024 19:33:19 +0000 Subject: [PATCH 491/519] Clean out env passed to wasmbrowsertest in TestWasm --- conn_test.go | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/conn_test.go b/conn_test.go index be7c9983..b4d57f21 100644 --- a/conn_test.go +++ b/conn_test.go @@ -364,7 +364,7 @@ func TestWasm(t *testing.T) { defer cancel() cmd := exec.CommandContext(ctx, "go", "test", "-exec=wasmbrowsertest", ".", "-v") - cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm", fmt.Sprintf("WS_ECHO_SERVER_URL=%v", s.URL)) + cmd.Env = append(cleanEnv(os.Environ()), "GOOS=js", "GOARCH=wasm", fmt.Sprintf("WS_ECHO_SERVER_URL=%v", s.URL)) b, err := cmd.CombinedOutput() if err != nil { @@ -372,6 +372,18 @@ func TestWasm(t *testing.T) { } } +func cleanEnv(env []string) (out []string) { + for _, e := range env { + // Filter out GITHUB envs and anything with token in it, + // especially GITHUB_TOKEN in CI as it breaks TestWasm. + if strings.HasPrefix(e, "GITHUB") || strings.Contains(e, "TOKEN") { + continue + } + out = append(out, e) + } + return out +} + func assertCloseStatus(exp websocket.StatusCode, err error) error { if websocket.CloseStatus(err) == -1 { return fmt.Errorf("expected websocket.CloseError: %T %v", err, err) From 473cd1a22a3f4540176be9cbe8ac3706dc20fdad Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Thu, 15 Aug 2024 19:57:52 +0000 Subject: [PATCH 492/519] Revert "Split coverage and deploy jobs in static.yml to avoid env issue" This reverts commit 0f14077dcfa84bb22fa4f83f84d9f0444c5accfe. --- .github/workflows/static.yml | 34 +++++++++++----------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 9b89a520..bbc03b39 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -16,39 +16,27 @@ concurrency: cancel-in-progress: true jobs: - coverage: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-go@v5 - with: - go-version-file: ./go.mod - - run: | - ./ci/test.sh - echo "PERCENT=$(go tool cover -func ./ci/out/coverage.prof | tail -n1 | awk '{print $3}' | tr -d '%')" >> "$GITHUB_OUTPUT" - { - echo "HTML<> "$GITHUB_OUTPUT" - deploy: environment: name: github-pages url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest - needs: coverage steps: + - name: Checkout + uses: actions/checkout@v4 - name: Setup Pages uses: actions/configure-pages@v5 - - name: Write coverage.html and coverage.svg - env: - PERCENT: ${{ steps.coverage.outputs.PERCENT }} - HTML: ${{ steps.coverage.outputs.HTML }} + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: ./go.mod + - name: Generate coverage and badge run: | + ./ci/test.sh mkdir -p ./ci/out/static - wget -O ./ci/out/static/coverage.svg "https://img.shields.io/badge/coverage-${PERCENT}%25-success" - echo "$HTML" > ./ci/out/static/coverage.html + cp ./ci/out/coverage.html ./ci/out/static/coverage.html + percent=$(go tool cover -func ./ci/out/coverage.prof | tail -n1 | awk '{print $3}' | tr -d '%') + wget -O ./ci/out/static/coverage.svg "https://img.shields.io/badge/coverage-${percent}%25-success" - name: Upload artifact uses: actions/upload-pages-artifact@v3 with: From 4ce1d9047a58a93af14604803f688715c9e1f29a Mon Sep 17 00:00:00 2001 From: Kian Yang Lee Date: Wed, 21 Aug 2024 17:02:48 +0800 Subject: [PATCH 493/519] Replace filepath.Match with path.Match (#452) OS-specific behaviour is not necessary. This PR replaces filepath.Match with path.Match and also updated the documentation to reflect that. Closes #451 --- accept.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/accept.go b/accept.go index f672a730..5b997be6 100644 --- a/accept.go +++ b/accept.go @@ -14,7 +14,7 @@ import ( "net/http" "net/textproto" "net/url" - "path/filepath" + "path" "strings" "github.com/coder/websocket/internal/errd" @@ -41,8 +41,8 @@ type AcceptOptions struct { // One would set this field to []string{"example.com"} to authorize example.com to connect. // // Each pattern is matched case insensitively against the request origin host - // with filepath.Match. - // See https://golang.org/pkg/path/filepath/#Match + // with path.Match. + // See https://golang.org/pkg/path/#Match // // Please ensure you understand the ramifications of enabling this. // If used incorrectly your WebSocket server will be open to CSRF attacks. @@ -96,7 +96,7 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con if !opts.InsecureSkipVerify { err = authenticateOrigin(r, opts.OriginPatterns) if err != nil { - if errors.Is(err, filepath.ErrBadPattern) { + if errors.Is(err, path.ErrBadPattern) { log.Printf("websocket: %v", err) err = errors.New(http.StatusText(http.StatusForbidden)) } @@ -221,7 +221,7 @@ func authenticateOrigin(r *http.Request, originHosts []string) error { for _, hostPattern := range originHosts { matched, err := match(hostPattern, u.Host) if err != nil { - return fmt.Errorf("failed to parse filepath pattern %q: %w", hostPattern, err) + return fmt.Errorf("failed to parse path pattern %q: %w", hostPattern, err) } if matched { return nil @@ -234,7 +234,7 @@ func authenticateOrigin(r *http.Request, originHosts []string) error { } func match(pattern, s string) (bool, error) { - return filepath.Match(strings.ToLower(pattern), strings.ToLower(s)) + return path.Match(strings.ToLower(pattern), strings.ToLower(s)) } func selectSubprotocol(r *http.Request, subprotocols []string) string { From 641f4f5c15bcd4f7c5b71aa270ee9335d473a5ea Mon Sep 17 00:00:00 2001 From: bestgopher <84328409@qq.com> Date: Wed, 21 Aug 2024 18:39:28 +0800 Subject: [PATCH 494/519] internal/bpool: add New function (#465) Signed-off-by: bestgopher <84328409@qq.com> --- internal/bpool/bpool.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/bpool/bpool.go b/internal/bpool/bpool.go index aa826fba..12cf577a 100644 --- a/internal/bpool/bpool.go +++ b/internal/bpool/bpool.go @@ -5,15 +5,16 @@ import ( "sync" ) -var bpool sync.Pool +var bpool = sync.Pool{ + New: func() any { + return &bytes.Buffer{} + }, +} // Get returns a buffer from the pool or creates a new one if // the pool is empty. func Get() *bytes.Buffer { b := bpool.Get() - if b == nil { - return &bytes.Buffer{} - } return b.(*bytes.Buffer) } From 3dd723ae69f00f042c4fd6961ed6cee3e9e11659 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Thu, 22 Aug 2024 11:10:19 +0300 Subject: [PATCH 495/519] accept: Add unwrapping for hijack like http.ResponseController (#472) Since we rely on the connection not being hijacked too early (i.e. detecting the presence of http.Hijacker) to set headers, we must manually implement the unwrapping of the http.ResponseController. By doing so, we also retain Go 1.19 compatibility without build tags. Closes #455 --- accept.go | 2 +- accept_test.go | 38 ++++++++++++++++++++++++++++++++++++++ hijack.go | 33 +++++++++++++++++++++++++++++++++ hijack_go120_test.go | 38 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 hijack.go create mode 100644 hijack_go120_test.go diff --git a/accept.go b/accept.go index 5b997be6..68c00ed3 100644 --- a/accept.go +++ b/accept.go @@ -105,7 +105,7 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con } } - hj, ok := w.(http.Hijacker) + hj, ok := hijacker(w) if !ok { err = errors.New("http.ResponseWriter does not implement http.Hijacker") http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented) diff --git a/accept_test.go b/accept_test.go index 4f799126..3b45ac5c 100644 --- a/accept_test.go +++ b/accept_test.go @@ -143,6 +143,33 @@ func TestAccept(t *testing.T) { _, err := Accept(w, r, nil) assert.Contains(t, err, `failed to hijack connection`) }) + + t.Run("wrapperHijackerIsUnwrapped", func(t *testing.T) { + t.Parallel() + + rr := httptest.NewRecorder() + w := mockUnwrapper{ + ResponseWriter: rr, + unwrap: func() http.ResponseWriter { + return mockHijacker{ + ResponseWriter: rr, + hijack: func() (conn net.Conn, writer *bufio.ReadWriter, err error) { + return nil, nil, errors.New("haha") + }, + } + }, + } + + r := httptest.NewRequest("GET", "/", nil) + r.Header.Set("Connection", "Upgrade") + r.Header.Set("Upgrade", "websocket") + r.Header.Set("Sec-WebSocket-Version", "13") + r.Header.Set("Sec-WebSocket-Key", xrand.Base64(16)) + + _, err := Accept(w, r, nil) + assert.Contains(t, err, "failed to hijack connection") + }) + t.Run("closeRace", func(t *testing.T) { t.Parallel() @@ -534,3 +561,14 @@ var _ http.Hijacker = mockHijacker{} func (mj mockHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) { return mj.hijack() } + +type mockUnwrapper struct { + http.ResponseWriter + unwrap func() http.ResponseWriter +} + +var _ rwUnwrapper = mockUnwrapper{} + +func (mu mockUnwrapper) Unwrap() http.ResponseWriter { + return mu.unwrap() +} diff --git a/hijack.go b/hijack.go new file mode 100644 index 00000000..9cce45ca --- /dev/null +++ b/hijack.go @@ -0,0 +1,33 @@ +//go:build !js + +package websocket + +import ( + "net/http" +) + +type rwUnwrapper interface { + Unwrap() http.ResponseWriter +} + +// hijacker returns the Hijacker interface of the http.ResponseWriter. +// It follows the Unwrap method of the http.ResponseWriter if available, +// matching the behavior of http.ResponseController. If the Hijacker +// interface is not found, it returns false. +// +// Since the http.ResponseController is not available in Go 1.19, and +// does not support checking the presence of the Hijacker interface, +// this function is used to provide a consistent way to check for the +// Hijacker interface across Go versions. +func hijacker(rw http.ResponseWriter) (http.Hijacker, bool) { + for { + switch t := rw.(type) { + case http.Hijacker: + return t, true + case rwUnwrapper: + rw = t.Unwrap() + default: + return nil, false + } + } +} diff --git a/hijack_go120_test.go b/hijack_go120_test.go new file mode 100644 index 00000000..0f0673a9 --- /dev/null +++ b/hijack_go120_test.go @@ -0,0 +1,38 @@ +//go:build !js && go1.20 + +package websocket + +import ( + "bufio" + "errors" + "net" + "net/http" + "net/http/httptest" + "testing" + + "github.com/coder/websocket/internal/test/assert" +) + +func Test_hijackerHTTPResponseControllerCompatibility(t *testing.T) { + t.Parallel() + + rr := httptest.NewRecorder() + w := mockUnwrapper{ + ResponseWriter: rr, + unwrap: func() http.ResponseWriter { + return mockHijacker{ + ResponseWriter: rr, + hijack: func() (conn net.Conn, writer *bufio.ReadWriter, err error) { + return nil, nil, errors.New("haha") + }, + } + }, + } + + _, _, err := http.NewResponseController(w).Hijack() + assert.Contains(t, err, "haha") + hj, ok := hijacker(w) + assert.Equal(t, "hijacker found", ok, true) + _, _, err = hj.Hijack() + assert.Contains(t, err, "haha") +} From 3cc37b4b7352a772636b7134834caf127797e965 Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Mon, 9 Sep 2024 16:31:25 +0500 Subject: [PATCH 496/519] chore: optimize ci (#481) * chore: prevent ci from running twice on PRs Now CI will only run on push to `master` and PRs to `master`. Previously, it was running twice on each push to a PR branch, once for the PR to `master` and once for pushing to the PR branch. --- .github/workflows/ci.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 13ddbf3e..8450f14d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,11 @@ name: ci -on: [push, pull_request] +on: + push: + branches: + - master + pull_request: + branches: + - master concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true From 807f8e8fcb7bad04425166c19b703e0378ff2c7c Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Mon, 9 Sep 2024 18:17:06 +0500 Subject: [PATCH 497/519] chore: create dependabot.yml (#480) --- .github/dependabot.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..c032ff24 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,9 @@ +version: 2 +updates: + - package-ecosystem: 'gomod' + directories: + - '/' + - '/internal/examples' + - '/internal/thirdparty' + schedule: + interval: 'weekly' From faf23b7ad88d609cdc20a1048409ab3fad77c00a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Sep 2024 16:48:22 +0300 Subject: [PATCH 498/519] build(deps): bump golang.org/x/time from 0.3.0 to 0.6.0 in /internal/examples (#482) build(deps): bump golang.org/x/time in /internal/examples Bumps [golang.org/x/time](https://github.com/golang/time) from 0.3.0 to 0.6.0. - [Commits](https://github.com/golang/time/compare/v0.3.0...v0.6.0) --- updated-dependencies: - dependency-name: golang.org/x/time dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- internal/examples/go.mod | 2 +- internal/examples/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/examples/go.mod b/internal/examples/go.mod index 4f7a8a70..ed01ec87 100644 --- a/internal/examples/go.mod +++ b/internal/examples/go.mod @@ -6,5 +6,5 @@ replace github.com/coder/websocket => ../.. require ( github.com/coder/websocket v0.0.0-00010101000000-000000000000 - golang.org/x/time v0.3.0 + golang.org/x/time v0.6.0 ) diff --git a/internal/examples/go.sum b/internal/examples/go.sum index f8a07e82..3558a566 100644 --- a/internal/examples/go.sum +++ b/internal/examples/go.sum @@ -1,2 +1,2 @@ -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= +golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= From 6c8e3ab3985f154e911339dba08291ff0343b492 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Thu, 12 Sep 2024 19:01:50 +0300 Subject: [PATCH 499/519] docs: Fix docs and examples related to r.Context() usage (#477) Contributes to #474 --- README.md | 4 +++- accept.go | 3 +++ internal/examples/chat/chat.go | 6 +++--- internal/examples/echo/server.go | 6 +++--- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 44a6ddf0..80d2b3cc 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,9 @@ http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) { } defer c.CloseNow() - ctx, cancel := context.WithTimeout(r.Context(), time.Second*10) + // Set the context as needed. Use of r.Context() is not recommended + // to avoid surprising behavior (see http.Hijacker). + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() var v interface{} diff --git a/accept.go b/accept.go index 68c00ed3..774ea285 100644 --- a/accept.go +++ b/accept.go @@ -79,6 +79,9 @@ func (opts *AcceptOptions) cloneWithDefaults() *AcceptOptions { // See the InsecureSkipVerify and OriginPatterns options to allow cross origin requests. // // Accept will write a response to w on all errors. +// +// Note that using the http.Request Context after Accept returns may lead to +// unexpected behavior (see http.Hijacker). func Accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, error) { return accept(w, r, opts) } diff --git a/internal/examples/chat/chat.go b/internal/examples/chat/chat.go index 3cb1e021..29f304b7 100644 --- a/internal/examples/chat/chat.go +++ b/internal/examples/chat/chat.go @@ -70,7 +70,7 @@ func (cs *chatServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { // subscribeHandler accepts the WebSocket connection and then subscribes // it to all future messages. func (cs *chatServer) subscribeHandler(w http.ResponseWriter, r *http.Request) { - err := cs.subscribe(r.Context(), w, r) + err := cs.subscribe(w, r) if errors.Is(err, context.Canceled) { return } @@ -111,7 +111,7 @@ func (cs *chatServer) publishHandler(w http.ResponseWriter, r *http.Request) { // // It uses CloseRead to keep reading from the connection to process control // messages and cancel the context if the connection drops. -func (cs *chatServer) subscribe(ctx context.Context, w http.ResponseWriter, r *http.Request) error { +func (cs *chatServer) subscribe(w http.ResponseWriter, r *http.Request) error { var mu sync.Mutex var c *websocket.Conn var closed bool @@ -142,7 +142,7 @@ func (cs *chatServer) subscribe(ctx context.Context, w http.ResponseWriter, r *h mu.Unlock() defer c.CloseNow() - ctx = c.CloseRead(ctx) + ctx := c.CloseRead(context.Background()) for { select { diff --git a/internal/examples/echo/server.go b/internal/examples/echo/server.go index a44d20b5..37e2f2c4 100644 --- a/internal/examples/echo/server.go +++ b/internal/examples/echo/server.go @@ -37,7 +37,7 @@ func (s echoServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { l := rate.NewLimiter(rate.Every(time.Millisecond*100), 10) for { - err = echo(r.Context(), c, l) + err = echo(c, l) if websocket.CloseStatus(err) == websocket.StatusNormalClosure { return } @@ -51,8 +51,8 @@ func (s echoServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { // echo reads from the WebSocket connection and then writes // the received message back to it. // The entire function has 10s to complete. -func echo(ctx context.Context, c *websocket.Conn, l *rate.Limiter) error { - ctx, cancel := context.WithTimeout(ctx, time.Second*10) +func echo(c *websocket.Conn, l *rate.Limiter) error { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() err := l.Wait(ctx) From 75addd95c08eefbfbd10a469a9d7d3f30253e444 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Thu, 19 Sep 2024 12:43:26 +0300 Subject: [PATCH 500/519] ci: fix ci/fmt.sh by pinning versions (#492) --- ci/fmt.sh | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/ci/fmt.sh b/ci/fmt.sh index 31d0c15d..e319a1e4 100755 --- a/ci/fmt.sh +++ b/ci/fmt.sh @@ -2,22 +2,24 @@ set -eu cd -- "$(dirname "$0")/.." +# Pin golang.org/x/tools, the go.mod of v0.25.0 is incompatible with Go 1.19. +X_TOOLS_VERSION=v0.24.0 + go mod tidy (cd ./internal/thirdparty && go mod tidy) (cd ./internal/examples && go mod tidy) gofmt -w -s . -go run golang.org/x/tools/cmd/goimports@latest -w "-local=$(go list -m)" . +go run golang.org/x/tools/cmd/goimports@${X_TOOLS_VERSION} -w "-local=$(go list -m)" . -npx prettier@3.0.3 \ - --write \ +git ls-files "*.yml" "*.md" "*.js" "*.css" "*.html" | xargs npx prettier@3.3.3 \ + --check \ --log-level=warn \ --print-width=90 \ --no-semi \ --single-quote \ - --arrow-parens=avoid \ - $(git ls-files "*.yml" "*.md" "*.js" "*.css" "*.html") + --arrow-parens=avoid -go run golang.org/x/tools/cmd/stringer@latest -type=opcode,MessageType,StatusCode -output=stringer.go +go run golang.org/x/tools/cmd/stringer@${X_TOOLS_VERSION} -type=opcode,MessageType,StatusCode -output=stringer.go if [ "${CI-}" ]; then git diff --exit-code From cef8e11d00b0742ad38986fd607da419a507e1cd Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Thu, 19 Sep 2024 12:44:01 +0300 Subject: [PATCH 501/519] chore: remove funding (#493) --- .github/FUNDING.yml | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index fb83c3a9..00000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -github: nhooyr From 02080e979f93f767ea2af94dcd7ca1b4630d749d Mon Sep 17 00:00:00 2001 From: Chun-Hung Tseng Date: Thu, 7 Nov 2024 12:32:53 +0100 Subject: [PATCH 502/519] Fix a typo in chat_test.go (#491) Fix typo in chat_test.go Co-authored-by: Mathias Fredriksson --- internal/examples/chat/chat_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/examples/chat/chat_test.go b/internal/examples/chat/chat_test.go index 8eb72051..dcada0b2 100644 --- a/internal/examples/chat/chat_test.go +++ b/internal/examples/chat/chat_test.go @@ -52,7 +52,7 @@ func Test_chatServer(t *testing.T) { // 10 clients are started that send 128 different // messages of max 128 bytes concurrently. // - // The test verifies that every message is seen by ever client + // The test verifies that every message is seen by every client // and no errors occur anywhere. t.Run("concurrency", func(t *testing.T) { t.Parallel() From d67767c5d20fa855f3a4aa94710b260ee13af122 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Thu, 7 Nov 2024 14:59:29 +0200 Subject: [PATCH 503/519] chore(.github): group dependabot PRs and reduce frequency (#499) --- .github/dependabot.yml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c032ff24..fb0a4558 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,9 +1,24 @@ version: 2 updates: + # Track in case we ever add dependencies. + - package-ecosystem: 'gomod' + directory: '/' + schedule: + interval: 'weekly' + commit-message: + prefix: 'chore' + + # Keep example and test/benchmark deps up-to-date. - package-ecosystem: 'gomod' directories: - - '/' - '/internal/examples' - '/internal/thirdparty' schedule: - interval: 'weekly' + interval: 'monthly' + commit-message: + prefix: 'chore' + labels: [] + groups: + internal-deps: + patterns: + - '*' From 1253b774ead7f405475f98e331a6b389c604020c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 7 Nov 2024 15:07:31 +0200 Subject: [PATCH 504/519] chore: bump the internal-deps group across 2 directories with 5 updates (#500) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- internal/examples/go.mod | 2 +- internal/examples/go.sum | 4 +- internal/thirdparty/go.mod | 42 +++++++------ internal/thirdparty/go.sum | 122 +++++++++++++++---------------------- 4 files changed, 75 insertions(+), 95 deletions(-) diff --git a/internal/examples/go.mod b/internal/examples/go.mod index ed01ec87..2aa1ee02 100644 --- a/internal/examples/go.mod +++ b/internal/examples/go.mod @@ -6,5 +6,5 @@ replace github.com/coder/websocket => ../.. require ( github.com/coder/websocket v0.0.0-00010101000000-000000000000 - golang.org/x/time v0.6.0 + golang.org/x/time v0.7.0 ) diff --git a/internal/examples/go.sum b/internal/examples/go.sum index 3558a566..60aa8f9a 100644 --- a/internal/examples/go.sum +++ b/internal/examples/go.sum @@ -1,2 +1,2 @@ -golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= -golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= diff --git a/internal/thirdparty/go.mod b/internal/thirdparty/go.mod index d946ffae..e060ce67 100644 --- a/internal/thirdparty/go.mod +++ b/internal/thirdparty/go.mod @@ -6,38 +6,40 @@ replace github.com/coder/websocket => ../.. require ( github.com/coder/websocket v0.0.0-00010101000000-000000000000 - github.com/gin-gonic/gin v1.9.1 - github.com/gobwas/ws v1.3.0 - github.com/gorilla/websocket v1.5.0 - github.com/lesismal/nbio v1.3.18 + github.com/gin-gonic/gin v1.10.0 + github.com/gobwas/ws v1.4.0 + github.com/gorilla/websocket v1.5.3 + github.com/lesismal/nbio v1.5.12 ) require ( - github.com/bytedance/sonic v1.9.1 // indirect - github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect - github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.4 // indirect - github.com/leodido/go-urn v1.2.4 // indirect - github.com/lesismal/llib v1.1.12 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lesismal/llib v1.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.11 // indirect - golang.org/x/arch v0.3.0 // indirect - golang.org/x/crypto v0.9.0 // indirect - golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.17.0 // indirect - golang.org/x/text v0.9.0 // indirect - google.golang.org/protobuf v1.30.0 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/internal/thirdparty/go.sum b/internal/thirdparty/go.sum index 1f542103..2352ac75 100644 --- a/internal/thirdparty/go.sum +++ b/internal/thirdparty/go.sum @@ -1,129 +1,107 @@ -github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= -github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= -github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= -github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= -github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= -github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= -github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.3.0 h1:sbeU3Y4Qzlb+MOzIe6mQGf7QR4Hkv6ZD0qhGkBFL2O0= -github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= +github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= -github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= -github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= -github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= -github.com/lesismal/llib v1.1.12 h1:KJFB8bL02V+QGIvILEw/w7s6bKj9Ps9Px97MZP2EOk0= -github.com/lesismal/llib v1.1.12/go.mod h1:70tFXXe7P1FZ02AU9l8LgSOK7d7sRrpnkUr3rd3gKSg= -github.com/lesismal/nbio v1.3.18 h1:kmJZlxjQpVfuCPYcXdv0Biv9LHVViJZet5K99Xs3RAs= -github.com/lesismal/nbio v1.3.18/go.mod h1:KWlouFT5cgDdW5sMX8RsHASUMGniea9X0XIellZ0B38= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lesismal/llib v1.1.13 h1:+w1+t0PykXpj2dXQck0+p6vdC9/mnbEXHgUy/HXDGfE= +github.com/lesismal/llib v1.1.13/go.mod h1:70tFXXe7P1FZ02AU9l8LgSOK7d7sRrpnkUr3rd3gKSg= +github.com/lesismal/nbio v1.5.12 h1:YcUjjmOvmKEANs6Oo175JogXvHy8CuE7i6ccjM2/tv4= +github.com/lesismal/nbio v1.5.12/go.mod h1:QsxE0fKFe1PioyjuHVDn2y8ktYK7xv9MFbpkoRFj8vI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= -github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= -golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20210513122933-cd7d49e622d5/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= From 11bda985bf5f0a7169f973913a52ca7f39ab9d99 Mon Sep 17 00:00:00 2001 From: Moritz Date: Wed, 4 Dec 2024 20:11:43 +0100 Subject: [PATCH 505/519] fix: avoid writing messages after close and improve handshake (#476) Co-authored-by: Mathias Fredriksson --- close.go | 12 ++--- conn.go | 10 +++- conn_test.go | 149 ++++++++++++++++++++++++++++++++++++++++++++++++++- read.go | 94 ++++++++++++++++++++------------ write.go | 52 +++++++++++------- 5 files changed, 252 insertions(+), 65 deletions(-) diff --git a/close.go b/close.go index ff2e878a..f94951dc 100644 --- a/close.go +++ b/close.go @@ -100,7 +100,7 @@ func CloseStatus(err error) StatusCode { func (c *Conn) Close(code StatusCode, reason string) (err error) { defer errd.Wrap(&err, "failed to close WebSocket") - if !c.casClosing() { + if c.casClosing() { err = c.waitGoroutines() if err != nil { return err @@ -133,7 +133,7 @@ func (c *Conn) Close(code StatusCode, reason string) (err error) { func (c *Conn) CloseNow() (err error) { defer errd.Wrap(&err, "failed to immediately close WebSocket") - if !c.casClosing() { + if c.casClosing() { err = c.waitGoroutines() if err != nil { return err @@ -329,13 +329,7 @@ func (ce CloseError) bytesErr() ([]byte, error) { } func (c *Conn) casClosing() bool { - c.closeMu.Lock() - defer c.closeMu.Unlock() - if !c.closing { - c.closing = true - return true - } - return false + return c.closing.Swap(true) } func (c *Conn) isClosed() bool { diff --git a/conn.go b/conn.go index d7434a9d..76b057dd 100644 --- a/conn.go +++ b/conn.go @@ -69,13 +69,19 @@ type Conn struct { writeHeaderBuf [8]byte writeHeader header + // Close handshake state. + closeStateMu sync.RWMutex + closeReceivedErr error + closeSentErr error + + // CloseRead state. closeReadMu sync.Mutex closeReadCtx context.Context closeReadDone chan struct{} + closing atomic.Bool + closeMu sync.Mutex // Protects following. closed chan struct{} - closeMu sync.Mutex - closing bool pingCounter atomic.Int64 activePingsMu sync.Mutex diff --git a/conn_test.go b/conn_test.go index b4d57f21..9ed8c7ea 100644 --- a/conn_test.go +++ b/conn_test.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "net" "net/http" "net/http/httptest" "os" @@ -460,7 +461,7 @@ func (tt *connTest) goDiscardLoop(c *websocket.Conn) { } func BenchmarkConn(b *testing.B) { - var benchCases = []struct { + benchCases := []struct { name string mode websocket.CompressionMode }{ @@ -625,3 +626,149 @@ func TestConcurrentClosePing(t *testing.T) { }() } } + +func TestConnClosePropagation(t *testing.T) { + t.Parallel() + + want := []byte("hello") + keepWriting := func(c *websocket.Conn) <-chan error { + return xsync.Go(func() error { + for { + err := c.Write(context.Background(), websocket.MessageText, want) + if err != nil { + return err + } + } + }) + } + keepReading := func(c *websocket.Conn) <-chan error { + return xsync.Go(func() error { + for { + _, got, err := c.Read(context.Background()) + if err != nil { + return err + } + if !bytes.Equal(want, got) { + return fmt.Errorf("unexpected message: want %q, got %q", want, got) + } + } + }) + } + checkReadErr := func(t *testing.T, err error) { + // Check read error (output depends on when read is called in relation to connection closure). + var ce websocket.CloseError + if errors.As(err, &ce) { + assert.Equal(t, "", websocket.StatusNormalClosure, ce.Code) + } else { + assert.ErrorIs(t, net.ErrClosed, err) + } + } + checkConnErrs := func(t *testing.T, conn ...*websocket.Conn) { + for _, c := range conn { + // Check write error. + err := c.Write(context.Background(), websocket.MessageText, want) + assert.ErrorIs(t, net.ErrClosed, err) + + _, _, err = c.Read(context.Background()) + checkReadErr(t, err) + } + } + + t.Run("CloseOtherSideDuringWrite", func(t *testing.T) { + tt, this, other := newConnTest(t, nil, nil) + + _ = this.CloseRead(tt.ctx) + thisWriteErr := keepWriting(this) + + _, got, err := other.Read(tt.ctx) + assert.Success(t, err) + assert.Equal(t, "msg", want, got) + + err = other.Close(websocket.StatusNormalClosure, "") + assert.Success(t, err) + + select { + case err := <-thisWriteErr: + assert.ErrorIs(t, net.ErrClosed, err) + case <-tt.ctx.Done(): + t.Fatal(tt.ctx.Err()) + } + + checkConnErrs(t, this, other) + }) + t.Run("CloseThisSideDuringWrite", func(t *testing.T) { + tt, this, other := newConnTest(t, nil, nil) + + _ = this.CloseRead(tt.ctx) + thisWriteErr := keepWriting(this) + otherReadErr := keepReading(other) + + err := this.Close(websocket.StatusNormalClosure, "") + assert.Success(t, err) + + select { + case err := <-thisWriteErr: + assert.ErrorIs(t, net.ErrClosed, err) + case <-tt.ctx.Done(): + t.Fatal(tt.ctx.Err()) + } + + select { + case err := <-otherReadErr: + checkReadErr(t, err) + case <-tt.ctx.Done(): + t.Fatal(tt.ctx.Err()) + } + + checkConnErrs(t, this, other) + }) + t.Run("CloseOtherSideDuringRead", func(t *testing.T) { + tt, this, other := newConnTest(t, nil, nil) + + _ = other.CloseRead(tt.ctx) + errs := keepReading(this) + + err := other.Write(tt.ctx, websocket.MessageText, want) + assert.Success(t, err) + + err = other.Close(websocket.StatusNormalClosure, "") + assert.Success(t, err) + + select { + case err := <-errs: + checkReadErr(t, err) + case <-tt.ctx.Done(): + t.Fatal(tt.ctx.Err()) + } + + checkConnErrs(t, this, other) + }) + t.Run("CloseThisSideDuringRead", func(t *testing.T) { + tt, this, other := newConnTest(t, nil, nil) + + thisReadErr := keepReading(this) + otherReadErr := keepReading(other) + + err := other.Write(tt.ctx, websocket.MessageText, want) + assert.Success(t, err) + + err = this.Close(websocket.StatusNormalClosure, "") + assert.Success(t, err) + + select { + case err := <-thisReadErr: + checkReadErr(t, err) + case <-tt.ctx.Done(): + t.Fatal(tt.ctx.Err()) + } + + select { + case err := <-otherReadErr: + checkReadErr(t, err) + case <-tt.ctx.Done(): + t.Fatal(tt.ctx.Err()) + } + + checkConnErrs(t, this, other) + }) +} diff --git a/read.go b/read.go index e2699da5..1267b5b9 100644 --- a/read.go +++ b/read.go @@ -217,57 +217,68 @@ func (c *Conn) readLoop(ctx context.Context) (header, error) { } } -func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { +// prepareRead sets the readTimeout context and returns a done function +// to be called after the read is done. It also returns an error if the +// connection is closed. The reference to the error is used to assign +// an error depending on if the connection closed or the context timed +// out during use. Typically the referenced error is a named return +// variable of the function calling this method. +func (c *Conn) prepareRead(ctx context.Context, err *error) (func(), error) { select { case <-c.closed: - return header{}, net.ErrClosed + return nil, net.ErrClosed case c.readTimeout <- ctx: } - h, err := readFrameHeader(c.br, c.readHeaderBuf[:]) - if err != nil { + done := func() { select { case <-c.closed: - return header{}, net.ErrClosed - case <-ctx.Done(): - return header{}, ctx.Err() - default: - return header{}, err + if *err != nil { + *err = net.ErrClosed + } + case c.readTimeout <- context.Background(): + } + if *err != nil && ctx.Err() != nil { + *err = ctx.Err() } } - select { - case <-c.closed: - return header{}, net.ErrClosed - case c.readTimeout <- context.Background(): + c.closeStateMu.Lock() + closeReceivedErr := c.closeReceivedErr + c.closeStateMu.Unlock() + if closeReceivedErr != nil { + defer done() + return nil, closeReceivedErr } - return h, nil + return done, nil } -func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { - select { - case <-c.closed: - return 0, net.ErrClosed - case c.readTimeout <- ctx: +func (c *Conn) readFrameHeader(ctx context.Context) (_ header, err error) { + readDone, err := c.prepareRead(ctx, &err) + if err != nil { + return header{}, err } + defer readDone() - n, err := io.ReadFull(c.br, p) + h, err := readFrameHeader(c.br, c.readHeaderBuf[:]) if err != nil { - select { - case <-c.closed: - return n, net.ErrClosed - case <-ctx.Done(): - return n, ctx.Err() - default: - return n, fmt.Errorf("failed to read frame payload: %w", err) - } + return header{}, err } - select { - case <-c.closed: - return n, net.ErrClosed - case c.readTimeout <- context.Background(): + return h, nil +} + +func (c *Conn) readFramePayload(ctx context.Context, p []byte) (_ int, err error) { + readDone, err := c.prepareRead(ctx, &err) + if err != nil { + return 0, err + } + defer readDone() + + n, err := io.ReadFull(c.br, p) + if err != nil { + return n, fmt.Errorf("failed to read frame payload: %w", err) } return n, err @@ -325,9 +336,22 @@ func (c *Conn) handleControl(ctx context.Context, h header) (err error) { } err = fmt.Errorf("received close frame: %w", ce) - c.writeClose(ce.Code, ce.Reason) - c.readMu.unlock() - c.close() + c.closeStateMu.Lock() + c.closeReceivedErr = err + closeSent := c.closeSentErr != nil + c.closeStateMu.Unlock() + + // Only unlock readMu if this connection is being closed becaue + // c.close will try to acquire the readMu lock. We unlock for + // writeClose as well because it may also call c.close. + if !closeSent { + c.readMu.unlock() + _ = c.writeClose(ce.Code, ce.Reason) + } + if !c.casClosing() { + c.readMu.unlock() + _ = c.close() + } return err } diff --git a/write.go b/write.go index e294a680..7324de74 100644 --- a/write.go +++ b/write.go @@ -5,6 +5,7 @@ package websocket import ( "bufio" + "compress/flate" "context" "crypto/rand" "encoding/binary" @@ -14,8 +15,6 @@ import ( "net" "time" - "compress/flate" - "github.com/coder/websocket/internal/errd" "github.com/coder/websocket/internal/util" ) @@ -249,22 +248,36 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco } defer c.writeFrameMu.unlock() + defer func() { + if c.isClosed() && opcode == opClose { + err = nil + } + if err != nil { + if ctx.Err() != nil { + err = ctx.Err() + } else if c.isClosed() { + err = net.ErrClosed + } + err = fmt.Errorf("failed to write frame: %w", err) + } + }() + + c.closeStateMu.Lock() + closeSentErr := c.closeSentErr + c.closeStateMu.Unlock() + if closeSentErr != nil { + return 0, net.ErrClosed + } + select { case <-c.closed: return 0, net.ErrClosed case c.writeTimeout <- ctx: } - defer func() { - if err != nil { - select { - case <-c.closed: - err = net.ErrClosed - case <-ctx.Done(): - err = ctx.Err() - default: - } - err = fmt.Errorf("failed to write frame: %w", err) + select { + case <-c.closed: + case c.writeTimeout <- context.Background(): } }() @@ -303,13 +316,16 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco } } - select { - case <-c.closed: - if opcode == opClose { - return n, nil + if opcode == opClose { + c.closeStateMu.Lock() + c.closeSentErr = fmt.Errorf("sent close frame: %w", net.ErrClosed) + closeReceived := c.closeReceivedErr != nil + c.closeStateMu.Unlock() + + if closeReceived && !c.casClosing() { + c.writeFrameMu.unlock() + _ = c.close() } - return n, net.ErrClosed - case c.writeTimeout <- context.Background(): } return n, nil From 3e183a987fd3d73a937df5461883fcf963e3a11a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=C3=B1igo=20Garcia=20Olaizola?= <11333576+igolaizola@users.noreply.github.com> Date: Wed, 29 Jan 2025 12:04:40 +0100 Subject: [PATCH 506/519] ci: disable AppArmor to allow Chrome sandbox (#511) Fixes #512 --- .github/workflows/ci.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8450f14d..81f1eb3b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,12 @@ jobs: test: runs-on: ubuntu-latest steps: + - name: Disable AppArmor + if: runner.os == 'Linux' + run: | + # Disable AppArmor for Ubuntu 23.10+. + # https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md + echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: From 497ac50c0ad739cc13ffe4dc3d6ecc9ecbb97b5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=C3=B1igo=20Garcia=20Olaizola?= <11333576+igolaizola@users.noreply.github.com> Date: Wed, 29 Jan 2025 15:49:46 +0100 Subject: [PATCH 507/519] ci: disable AppArmor on daily and static workflows (#513) --- .github/workflows/daily.yml | 12 ++++++++++++ .github/workflows/static.yml | 6 ++++++ 2 files changed, 18 insertions(+) diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index 2ba9ce34..91af7ce5 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -19,6 +19,12 @@ jobs: test: runs-on: ubuntu-latest steps: + - name: Disable AppArmor + if: runner.os == 'Linux' + run: | + # Disable AppArmor for Ubuntu 23.10+. + # https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md + echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: @@ -41,6 +47,12 @@ jobs: test-dev: runs-on: ubuntu-latest steps: + - name: Disable AppArmor + if: runner.os == 'Linux' + run: | + # Disable AppArmor for Ubuntu 23.10+. + # https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md + echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns - uses: actions/checkout@v4 with: ref: dev diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index bbc03b39..6ea76ab6 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -22,6 +22,12 @@ jobs: url: ${{ steps.deployment.outputs.page_url }} runs-on: ubuntu-latest steps: + - name: Disable AppArmor + if: runner.os == 'Linux' + run: | + # Disable AppArmor for Ubuntu 23.10+. + # https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md + echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns - name: Checkout uses: actions/checkout@v4 - name: Setup Pages From aec630d59c74431ffd33ada5a75acf564d27863f Mon Sep 17 00:00:00 2001 From: "W. Michael Petullo" Date: Wed, 29 Jan 2025 08:51:48 -0600 Subject: [PATCH 508/519] fix: conform to stricter printf usage in Go 1.24 (#508) --- autobahn_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/autobahn_test.go b/autobahn_test.go index b1b3a7e9..cd0cc9bb 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -92,7 +92,7 @@ func TestAutobahn(t *testing.T) { } }) - c, _, err := websocket.Dial(ctx, fmt.Sprintf(wstestURL+"/updateReports?agent=main"), nil) + c, _, err := websocket.Dial(ctx, wstestURL+"/updateReports?agent=main", nil) assert.Success(t, err) c.Close(websocket.StatusNormalClosure, "") From 703784f0773aed414ec8e081a8fb0f0134ca631e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=C3=B1igo=20Garcia=20Olaizola?= <11333576+igolaizola@users.noreply.github.com> Date: Thu, 30 Jan 2025 13:13:36 +0100 Subject: [PATCH 509/519] feat: add ping and pong received callbacks (#509) This change adds two optional callbacks to both `DialOptions` and `AcceptOptions`. These callbacks are invoked synchronously when a ping or pong frame is received, allowing advanced users to log or inspect payloads for metrics or debugging. If the callback needs to perform more complex work or reuse the payload outside the callback, it is recommended to perform processing in a separate goroutine. The boolean return value of `OnPingReceived` is used to determine if the subsequent pong frame should be sent. If `false` is returned, the pong frame is not sent. Fixes #246 --- accept.go | 19 +++++++++++++ conn.go | 16 +++++++---- conn_test.go | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++ dial.go | 18 ++++++++++++ read.go | 8 ++++++ 5 files changed, 135 insertions(+), 5 deletions(-) diff --git a/accept.go b/accept.go index 774ea285..f45fdd0b 100644 --- a/accept.go +++ b/accept.go @@ -5,6 +5,7 @@ package websocket import ( "bytes" + "context" "crypto/sha1" "encoding/base64" "errors" @@ -62,6 +63,22 @@ type AcceptOptions struct { // Defaults to 512 bytes for CompressionNoContextTakeover and 128 bytes // for CompressionContextTakeover. CompressionThreshold int + + // OnPingReceived is an optional callback invoked synchronously when a ping frame is received. + // + // The payload contains the application data of the ping frame. + // If the callback returns false, the subsequent pong frame will not be sent. + // To avoid blocking, any expensive processing should be performed asynchronously using a goroutine. + OnPingReceived func(ctx context.Context, payload []byte) bool + + // OnPongReceived is an optional callback invoked synchronously when a pong frame is received. + // + // The payload contains the application data of the pong frame. + // To avoid blocking, any expensive processing should be performed asynchronously using a goroutine. + // + // Unlike OnPingReceived, this callback does not return a value because a pong frame + // is a response to a ping and does not trigger any further frame transmission. + OnPongReceived func(ctx context.Context, payload []byte) } func (opts *AcceptOptions) cloneWithDefaults() *AcceptOptions { @@ -156,6 +173,8 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con client: false, copts: copts, flateThreshold: opts.CompressionThreshold, + onPingReceived: opts.OnPingReceived, + onPongReceived: opts.OnPongReceived, br: brw.Reader, bw: brw.Writer, diff --git a/conn.go b/conn.go index 76b057dd..42fe89fe 100644 --- a/conn.go +++ b/conn.go @@ -83,9 +83,11 @@ type Conn struct { closeMu sync.Mutex // Protects following. closed chan struct{} - pingCounter atomic.Int64 - activePingsMu sync.Mutex - activePings map[string]chan<- struct{} + pingCounter atomic.Int64 + activePingsMu sync.Mutex + activePings map[string]chan<- struct{} + onPingReceived func(context.Context, []byte) bool + onPongReceived func(context.Context, []byte) } type connConfig struct { @@ -94,6 +96,8 @@ type connConfig struct { client bool copts *compressionOptions flateThreshold int + onPingReceived func(context.Context, []byte) bool + onPongReceived func(context.Context, []byte) br *bufio.Reader bw *bufio.Writer @@ -114,8 +118,10 @@ func newConn(cfg connConfig) *Conn { writeTimeout: make(chan context.Context), timeoutLoopDone: make(chan struct{}), - closed: make(chan struct{}), - activePings: make(map[string]chan<- struct{}), + closed: make(chan struct{}), + activePings: make(map[string]chan<- struct{}), + onPingReceived: cfg.onPingReceived, + onPongReceived: cfg.onPongReceived, } c.readMu = newMu(c) diff --git a/conn_test.go b/conn_test.go index 9ed8c7ea..45bb75be 100644 --- a/conn_test.go +++ b/conn_test.go @@ -97,6 +97,85 @@ func TestConn(t *testing.T) { assert.Contains(t, err, "failed to wait for pong") }) + t.Run("pingReceivedPongReceived", func(t *testing.T) { + var pingReceived1, pongReceived1 bool + var pingReceived2, pongReceived2 bool + tt, c1, c2 := newConnTest(t, + &websocket.DialOptions{ + OnPingReceived: func(ctx context.Context, payload []byte) bool { + pingReceived1 = true + return true + }, + OnPongReceived: func(ctx context.Context, payload []byte) { + pongReceived1 = true + }, + }, &websocket.AcceptOptions{ + OnPingReceived: func(ctx context.Context, payload []byte) bool { + pingReceived2 = true + return true + }, + OnPongReceived: func(ctx context.Context, payload []byte) { + pongReceived2 = true + }, + }, + ) + + c1.CloseRead(tt.ctx) + c2.CloseRead(tt.ctx) + + ctx, cancel := context.WithTimeout(tt.ctx, time.Millisecond*100) + defer cancel() + + err := c1.Ping(ctx) + assert.Success(t, err) + + c1.CloseNow() + c2.CloseNow() + + assert.Equal(t, "only one side receives the ping", false, pingReceived1 && pingReceived2) + assert.Equal(t, "only one side receives the pong", false, pongReceived1 && pongReceived2) + assert.Equal(t, "ping and pong received", true, (pingReceived1 && pongReceived2) || (pingReceived2 && pongReceived1)) + }) + + t.Run("pingReceivedPongNotReceived", func(t *testing.T) { + var pingReceived1, pongReceived1 bool + var pingReceived2, pongReceived2 bool + tt, c1, c2 := newConnTest(t, + &websocket.DialOptions{ + OnPingReceived: func(ctx context.Context, payload []byte) bool { + pingReceived1 = true + return false + }, + OnPongReceived: func(ctx context.Context, payload []byte) { + pongReceived1 = true + }, + }, &websocket.AcceptOptions{ + OnPingReceived: func(ctx context.Context, payload []byte) bool { + pingReceived2 = true + return false + }, + OnPongReceived: func(ctx context.Context, payload []byte) { + pongReceived2 = true + }, + }, + ) + + c1.CloseRead(tt.ctx) + c2.CloseRead(tt.ctx) + + ctx, cancel := context.WithTimeout(tt.ctx, time.Millisecond*100) + defer cancel() + + err := c1.Ping(ctx) + assert.Contains(t, err, "failed to wait for pong") + + c1.CloseNow() + c2.CloseNow() + + assert.Equal(t, "only one side receives the ping", false, pingReceived1 && pingReceived2) + assert.Equal(t, "ping received and pong not received", true, (pingReceived1 && !pongReceived2) || (pingReceived2 && !pongReceived1)) + }) + t.Run("concurrentWrite", func(t *testing.T) { tt, c1, c2 := newConnTest(t, nil, nil) diff --git a/dial.go b/dial.go index ad61a35d..0b11ecbb 100644 --- a/dial.go +++ b/dial.go @@ -48,6 +48,22 @@ type DialOptions struct { // Defaults to 512 bytes for CompressionNoContextTakeover and 128 bytes // for CompressionContextTakeover. CompressionThreshold int + + // OnPingReceived is an optional callback invoked synchronously when a ping frame is received. + // + // The payload contains the application data of the ping frame. + // If the callback returns false, the subsequent pong frame will not be sent. + // To avoid blocking, any expensive processing should be performed asynchronously using a goroutine. + OnPingReceived func(ctx context.Context, payload []byte) bool + + // OnPongReceived is an optional callback invoked synchronously when a pong frame is received. + // + // The payload contains the application data of the pong frame. + // To avoid blocking, any expensive processing should be performed asynchronously using a goroutine. + // + // Unlike OnPingReceived, this callback does not return a value because a pong frame + // is a response to a ping and does not trigger any further frame transmission. + OnPongReceived func(ctx context.Context, payload []byte) } func (opts *DialOptions) cloneWithDefaults(ctx context.Context) (context.Context, context.CancelFunc, *DialOptions) { @@ -163,6 +179,8 @@ func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) ( client: true, copts: copts, flateThreshold: opts.CompressionThreshold, + onPingReceived: opts.OnPingReceived, + onPongReceived: opts.OnPongReceived, br: getBufioReader(rwc), bw: getBufioWriter(rwc), }), resp, nil diff --git a/read.go b/read.go index 1267b5b9..2db22435 100644 --- a/read.go +++ b/read.go @@ -312,8 +312,16 @@ func (c *Conn) handleControl(ctx context.Context, h header) (err error) { switch h.opcode { case opPing: + if c.onPingReceived != nil { + if !c.onPingReceived(ctx, b) { + return nil + } + } return c.writeControl(ctx, opPong, b) case opPong: + if c.onPongReceived != nil { + c.onPongReceived(ctx, b) + } c.activePingsMu.Lock() pong, ok := c.activePings[string(b)] c.activePingsMu.Unlock() From d1468a75eee5525d183123766fbf288dca1eed9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?I=C3=B1igo=20Garcia=20Olaizola?= <11333576+igolaizola@users.noreply.github.com> Date: Thu, 30 Jan 2025 13:32:49 +0100 Subject: [PATCH 510/519] ci: update wasmbrowsertest to a specific commit (#514) --- ci/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/test.sh b/ci/test.sh index a3007614..cc3c22d7 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -24,7 +24,7 @@ cd -- "$(dirname "$0")/.." ) -go install github.com/agnivade/wasmbrowsertest@latest +go install github.com/agnivade/wasmbrowsertest@8be019f6c6dceae821467b4c589eb195c2b761ce go test --race --bench=. --timeout=1h --covermode=atomic --coverprofile=ci/out/coverage.prof --coverpkg=./... "$@" ./... sed -i.bak '/stringer\.go/d' ci/out/coverage.prof sed -i.bak '/nhooyr.io\/websocket\/internal\/test/d' ci/out/coverage.prof From 64d7449933124ed1f0d779c477a9e9145bbdac38 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Fri, 14 Mar 2025 16:53:01 +0200 Subject: [PATCH 511/519] ci: lock down versions in lint.sh and fix ci (#523) --- .github/workflows/ci.yml | 4 +++- .github/workflows/daily.yml | 4 ++-- ci/lint.sh | 5 +++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 81f1eb3b..9f7aed46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,8 @@ jobs: - uses: actions/checkout@v4 - run: go version - uses: actions/setup-go@v5 + with: + go-version-file: ./go.mod - run: ./ci/lint.sh test: @@ -42,7 +44,7 @@ jobs: with: go-version-file: ./go.mod - run: ./ci/test.sh - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: coverage.html path: ./ci/out/coverage.html diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index 91af7ce5..0eac94cc 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -30,7 +30,7 @@ jobs: with: go-version-file: ./go.mod - run: AUTOBAHN=1 ./ci/test.sh - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: coverage.html path: ./ci/out/coverage.html @@ -60,7 +60,7 @@ jobs: with: go-version-file: ./go.mod - run: AUTOBAHN=1 ./ci/test.sh - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: coverage-dev.html path: ./ci/out/coverage.html diff --git a/ci/lint.sh b/ci/lint.sh index 3cf8eee4..cf9d1abd 100755 --- a/ci/lint.sh +++ b/ci/lint.sh @@ -1,11 +1,12 @@ #!/bin/sh +set -x set -eu cd -- "$(dirname "$0")/.." go vet ./... GOOS=js GOARCH=wasm go vet ./... -go install honnef.co/go/tools/cmd/staticcheck@latest +go install honnef.co/go/tools/cmd/staticcheck@v0.4.7 staticcheck ./... GOOS=js GOARCH=wasm staticcheck ./... @@ -15,7 +16,7 @@ govulncheck() { cat "$tmpf" fi } -go install golang.org/x/vuln/cmd/govulncheck@latest +go install golang.org/x/vuln/cmd/govulncheck@v1.1.1 govulncheck ./... GOOS=js GOARCH=wasm govulncheck ./... From 778d161bfd21f3cfa8052d8b84e8c86e08ce41c7 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Fri, 14 Mar 2025 17:38:18 +0200 Subject: [PATCH 512/519] build: update to Go 1.23 (#524) * build: update to Go 1.23 * ci: update tools --- ci/fmt.sh | 3 +-- ci/lint.sh | 7 +++++-- go.mod | 2 +- internal/examples/go.mod | 2 +- internal/thirdparty/go.mod | 2 +- internal/thirdparty/go.sum | 3 +++ 6 files changed, 12 insertions(+), 7 deletions(-) diff --git a/ci/fmt.sh b/ci/fmt.sh index e319a1e4..588510ba 100755 --- a/ci/fmt.sh +++ b/ci/fmt.sh @@ -2,8 +2,7 @@ set -eu cd -- "$(dirname "$0")/.." -# Pin golang.org/x/tools, the go.mod of v0.25.0 is incompatible with Go 1.19. -X_TOOLS_VERSION=v0.24.0 +X_TOOLS_VERSION=v0.31.0 go mod tidy (cd ./internal/thirdparty && go mod tidy) diff --git a/ci/lint.sh b/ci/lint.sh index cf9d1abd..20daff92 100755 --- a/ci/lint.sh +++ b/ci/lint.sh @@ -3,10 +3,13 @@ set -x set -eu cd -- "$(dirname "$0")/.." +STATICCHECK_VERSION=v0.6.1 +GOVULNCHECK_VERSION=v1.1.4 + go vet ./... GOOS=js GOARCH=wasm go vet ./... -go install honnef.co/go/tools/cmd/staticcheck@v0.4.7 +go install honnef.co/go/tools/cmd/staticcheck@${STATICCHECK_VERSION} staticcheck ./... GOOS=js GOARCH=wasm staticcheck ./... @@ -16,7 +19,7 @@ govulncheck() { cat "$tmpf" fi } -go install golang.org/x/vuln/cmd/govulncheck@v1.1.1 +go install golang.org/x/vuln/cmd/govulncheck@${GOVULNCHECK_VERSION} govulncheck ./... GOOS=js GOARCH=wasm govulncheck ./... diff --git a/go.mod b/go.mod index 336411a5..d32fbd77 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ module github.com/coder/websocket -go 1.19 +go 1.23 diff --git a/internal/examples/go.mod b/internal/examples/go.mod index 2aa1ee02..e368b76b 100644 --- a/internal/examples/go.mod +++ b/internal/examples/go.mod @@ -1,6 +1,6 @@ module github.com/coder/websocket/examples -go 1.19 +go 1.23 replace github.com/coder/websocket => ../.. diff --git a/internal/thirdparty/go.mod b/internal/thirdparty/go.mod index e060ce67..7a86aca9 100644 --- a/internal/thirdparty/go.mod +++ b/internal/thirdparty/go.mod @@ -1,6 +1,6 @@ module github.com/coder/websocket/internal/thirdparty -go 1.19 +go 1.23 replace github.com/coder/websocket => ../.. diff --git a/internal/thirdparty/go.sum b/internal/thirdparty/go.sum index 2352ac75..a7be7082 100644 --- a/internal/thirdparty/go.sum +++ b/internal/thirdparty/go.sum @@ -16,6 +16,7 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= @@ -31,6 +32,7 @@ github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakr github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= @@ -96,6 +98,7 @@ golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= From 246891f172ef96b0b5681c8e4d59dfd32ad1b091 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Fri, 14 Mar 2025 17:49:00 +0200 Subject: [PATCH 513/519] build: add Makefile (#525) --- .github/workflows/ci.yml | 8 ++++---- .github/workflows/daily.yml | 8 ++++---- .github/workflows/static.yml | 2 +- Makefile | 18 ++++++++++++++++++ ci/lint.sh | 1 - make.sh | 12 ------------ 6 files changed, 27 insertions(+), 22 deletions(-) create mode 100644 Makefile delete mode 100755 make.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f7aed46..836381ef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - - run: ./ci/fmt.sh + - run: make fmt lint: runs-on: ubuntu-latest @@ -28,7 +28,7 @@ jobs: - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - - run: ./ci/lint.sh + - run: make lint test: runs-on: ubuntu-latest @@ -43,7 +43,7 @@ jobs: - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - - run: ./ci/test.sh + - run: make test - uses: actions/upload-artifact@v4 with: name: coverage.html @@ -56,4 +56,4 @@ jobs: - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - - run: ./ci/bench.sh + - run: make bench diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index 0eac94cc..62e3d337 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -15,7 +15,7 @@ jobs: - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - - run: AUTOBAHN=1 ./ci/bench.sh + - run: AUTOBAHN=1 make bench test: runs-on: ubuntu-latest steps: @@ -29,7 +29,7 @@ jobs: - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - - run: AUTOBAHN=1 ./ci/test.sh + - run: AUTOBAHN=1 make test - uses: actions/upload-artifact@v4 with: name: coverage.html @@ -43,7 +43,7 @@ jobs: - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - - run: AUTOBAHN=1 ./ci/bench.sh + - run: AUTOBAHN=1 make bench test-dev: runs-on: ubuntu-latest steps: @@ -59,7 +59,7 @@ jobs: - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - - run: AUTOBAHN=1 ./ci/test.sh + - run: AUTOBAHN=1 make test - uses: actions/upload-artifact@v4 with: name: coverage-dev.html diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 6ea76ab6..a78ce1b9 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -38,7 +38,7 @@ jobs: go-version-file: ./go.mod - name: Generate coverage and badge run: | - ./ci/test.sh + make test mkdir -p ./ci/out/static cp ./ci/out/coverage.html ./ci/out/static/coverage.html percent=$(go tool cover -func ./ci/out/coverage.prof | tail -n1 | awk '{print $3}' | tr -d '%') diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..a3e4a20d --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +.PHONY: all +all: fmt lint test + +.PHONY: fmt +fmt: + ./ci/fmt.sh + +.PHONY: lint +lint: + ./ci/lint.sh + +.PHONY: test +test: + ./ci/test.sh + +.PHONY: bench +bench: + ./ci/bench.sh \ No newline at end of file diff --git a/ci/lint.sh b/ci/lint.sh index 20daff92..316b035d 100755 --- a/ci/lint.sh +++ b/ci/lint.sh @@ -1,5 +1,4 @@ #!/bin/sh -set -x set -eu cd -- "$(dirname "$0")/.." diff --git a/make.sh b/make.sh deleted file mode 100755 index 170d00a8..00000000 --- a/make.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh -set -eu -cd -- "$(dirname "$0")" - -echo "=== fmt.sh" -./ci/fmt.sh -echo "=== lint.sh" -./ci/lint.sh -echo "=== test.sh" -./ci/test.sh "$@" -echo "=== bench.sh" -./ci/bench.sh From efb626be44240d7979b57427265d9b6402166b96 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 25 Mar 2025 13:27:06 +0100 Subject: [PATCH 514/519] chore: update LICENSE file (#526) --- LICENSE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.txt b/LICENSE.txt index 77b5bef6..7e79329f 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2023 Anmol Sethi +Copyright (c) 2025 Coder Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above From 91013c129147ee6375b65cf150fa151d2fcc0bce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacob=20Alz=C3=A9n?= Date: Thu, 10 Jul 2025 10:49:59 +0200 Subject: [PATCH 515/519] chore: apply various modernisations (#531) * Use any instead of interface{} given Go > 1.18 * Minor tidies thanks to gofumpt * Run "go fix ./..." on the codebase * Apply a few modernisations suggested by gopls check * Remove unused parameter suggested by gopls check --- README.md | 2 +- accept.go | 1 - accept_test.go | 1 - autobahn_test.go | 5 ++--- close.go | 1 - close_test.go | 1 - compress.go | 7 ++++--- compress_test.go | 3 +-- conn.go | 1 - conn_test.go | 20 ++++++++++---------- dial.go | 1 - dial_test.go | 8 +++----- doc.go | 1 - example_test.go | 4 ++-- export_test.go | 11 ++++++----- frame_test.go | 3 +-- internal/errd/wrap.go | 2 +- internal/examples/chat/chat.go | 2 +- internal/examples/echo/server.go | 2 +- internal/test/assert/assert.go | 4 ++-- internal/test/wstest/pipe.go | 1 - internal/thirdparty/gin_test.go | 2 +- internal/wsjs/wsjs_js.go | 8 ++++---- mask_test.go | 2 +- netconn.go | 3 +-- netconn_notjs.go | 1 - read.go | 1 - write.go | 6 +----- ws_js.go | 4 ++-- ws_js_test.go | 2 +- wsjson/wsjson.go | 8 ++++---- 31 files changed, 50 insertions(+), 68 deletions(-) diff --git a/README.md b/README.md index 80d2b3cc..6e986897 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() - var v interface{} + var v any err = wsjson.Read(ctx, c, &v) if err != nil { // ... diff --git a/accept.go b/accept.go index f45fdd0b..0f3b0d16 100644 --- a/accept.go +++ b/accept.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js package websocket diff --git a/accept_test.go b/accept_test.go index 3b45ac5c..aeea1d8a 100644 --- a/accept_test.go +++ b/accept_test.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js package websocket diff --git a/autobahn_test.go b/autobahn_test.go index cd0cc9bb..20b89609 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js package websocket_test @@ -130,7 +129,7 @@ func wstestServer(tb testing.TB, ctx context.Context) (url string, closeFn func( url = "ws://" + serverAddr const outDir = "ci/out/autobahn-report" - specFile, err := tempJSONFile(map[string]interface{}{ + specFile, err := tempJSONFile(map[string]any{ "url": url, "outdir": outDir, "cases": autobahnCases, @@ -280,7 +279,7 @@ func unusedListenAddr() (_ string, err error) { return l.Addr().String(), nil } -func tempJSONFile(v interface{}) (string, error) { +func tempJSONFile(v any) (string, error) { f, err := os.CreateTemp("", "temp.json") if err != nil { return "", fmt.Errorf("temp file: %w", err) diff --git a/close.go b/close.go index f94951dc..2de1a5c2 100644 --- a/close.go +++ b/close.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js package websocket diff --git a/close_test.go b/close_test.go index aec582c1..1e04807e 100644 --- a/close_test.go +++ b/close_test.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js package websocket diff --git a/compress.go b/compress.go index 1f3adcfb..41bd5bdb 100644 --- a/compress.go +++ b/compress.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js package websocket @@ -168,8 +167,10 @@ type slidingWindow struct { buf []byte } -var swPoolMu sync.RWMutex -var swPool = map[int]*sync.Pool{} +var ( + swPoolMu sync.RWMutex + swPool = map[int]*sync.Pool{} +) func slidingWindowPool(n int) *sync.Pool { swPoolMu.RLock() diff --git a/compress_test.go b/compress_test.go index d97492cf..1964c84f 100644 --- a/compress_test.go +++ b/compress_test.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js package websocket @@ -19,7 +18,7 @@ func Test_slidingWindow(t *testing.T) { const testCount = 99 const maxWindow = 99999 - for i := 0; i < testCount; i++ { + for range testCount { t.Run("", func(t *testing.T) { t.Parallel() diff --git a/conn.go b/conn.go index 42fe89fe..5907bc81 100644 --- a/conn.go +++ b/conn.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js package websocket diff --git a/conn_test.go b/conn_test.go index 45bb75be..58ac394c 100644 --- a/conn_test.go +++ b/conn_test.go @@ -36,7 +36,7 @@ func TestConn(t *testing.T) { return websocket.CompressionMode(xrand.Int(int(websocket.CompressionContextTakeover) + 1)) } - for i := 0; i < 5; i++ { + for range 5 { t.Run("", func(t *testing.T) { tt, c1, c2 := newConnTest(t, &websocket.DialOptions{ CompressionMode: compressionMode(), @@ -50,7 +50,7 @@ func TestConn(t *testing.T) { c1.SetReadLimit(131072) - for i := 0; i < 5; i++ { + for range 5 { err := wstest.Echo(tt.ctx, c1, 131072) assert.Success(t, err) } @@ -76,7 +76,7 @@ func TestConn(t *testing.T) { c1.CloseRead(tt.ctx) c2.CloseRead(tt.ctx) - for i := 0; i < 10; i++ { + for range 10 { err := c1.Ping(tt.ctx) assert.Success(t, err) } @@ -185,7 +185,7 @@ func TestConn(t *testing.T) { const count = 100 errs := make(chan error, count) - for i := 0; i < count; i++ { + for range count { go func() { select { case errs <- c1.Write(tt.ctx, websocket.MessageBinary, msg): @@ -195,7 +195,7 @@ func TestConn(t *testing.T) { }() } - for i := 0; i < count; i++ { + for range count { select { case err := <-errs: assert.Success(t, err) @@ -341,7 +341,7 @@ func TestConn(t *testing.T) { return wsjson.Write(tt.ctx, c1, exp) }) - var act interface{} + var act any err := wsjson.Read(tt.ctx, c1, &act) assert.Success(t, err) assert.Equal(t, "read msg", exp, act) @@ -372,7 +372,7 @@ func TestConn(t *testing.T) { return wsjson.Write(tt.ctx, c1, exp) }) - var act interface{} + var act any err := wsjson.Read(tt.ctx, c1, &act) assert.Success(t, err) assert.Equal(t, "read msg", exp, act) @@ -408,7 +408,7 @@ func TestConn(t *testing.T) { c1.SetReadLimit(131072) - for i := 0; i < 5; i++ { + for range 5 { err := wstest.Echo(tt.ctx, c1, 131072) assert.Success(t, err) } @@ -660,7 +660,7 @@ func assertEcho(tb testing.TB, ctx context.Context, c *websocket.Conn) { return wsjson.Write(ctx, c, exp) }) - var act interface{} + var act any c.SetReadLimit(1 << 30) err := wsjson.Read(ctx, c, &act) assert.Success(tb, err) @@ -682,7 +682,7 @@ func assertClose(tb testing.TB, c *websocket.Conn) { func TestConcurrentClosePing(t *testing.T) { t.Parallel() - for i := 0; i < 64; i++ { + for range 64 { func() { c1, c2 := wstest.Pipe(nil, nil) defer c1.CloseNow() diff --git a/dial.go b/dial.go index 0b11ecbb..f5e4544b 100644 --- a/dial.go +++ b/dial.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js package websocket diff --git a/dial_test.go b/dial_test.go index f94cd73b..492ac6b3 100644 --- a/dial_test.go +++ b/dial_test.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js package websocket_test @@ -8,6 +7,7 @@ import ( "context" "crypto/rand" "io" + "maps" "net/http" "net/http/httptest" "net/url" @@ -172,7 +172,6 @@ func Test_verifyHostOverride(t *testing.T) { c.CloseNow() }) } - } type mockBody struct { @@ -357,11 +356,10 @@ func (fc *forwardProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { } defer resp.Body.Close() - for k, v := range resp.Header { - w.Header()[k] = v - } + maps.Copy(w.Header(), resp.Header) w.Header().Set("PROXIED", "true") w.WriteHeader(resp.StatusCode) + if resprw, ok := resp.Body.(io.ReadWriter); ok { c, brw, err := w.(http.Hijacker).Hijack() if err != nil { diff --git a/doc.go b/doc.go index 03edf129..0c7f8316 100644 --- a/doc.go +++ b/doc.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js // Package websocket implements the RFC 6455 WebSocket protocol. // diff --git a/example_test.go b/example_test.go index 4cc0cf11..7026e311 100644 --- a/example_test.go +++ b/example_test.go @@ -25,7 +25,7 @@ func ExampleAccept() { ctx, cancel := context.WithTimeout(r.Context(), time.Second*10) defer cancel() - var v interface{} + var v any err = wsjson.Read(ctx, c, &v) if err != nil { log.Println(err) @@ -150,7 +150,7 @@ func ExampleConn_Ping() { // Required to read the Pongs from the server. ctx = c.CloseRead(ctx) - for i := 0; i < 5; i++ { + for range 5 { err = c.Ping(ctx) if err != nil { log.Fatal(err) diff --git a/export_test.go b/export_test.go index d3443991..e0071922 100644 --- a/export_test.go +++ b/export_test.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js package websocket @@ -30,9 +29,11 @@ func (c *Conn) RecordBytesRead() *int { var ErrClosed = net.ErrClosed -var ExportedDial = dial -var SecWebSocketAccept = secWebSocketAccept -var SecWebSocketKey = secWebSocketKey -var VerifyServerResponse = verifyServerResponse +var ( + ExportedDial = dial + SecWebSocketAccept = secWebSocketAccept + SecWebSocketKey = secWebSocketKey + VerifyServerResponse = verifyServerResponse +) var CompressionModeOpts = CompressionMode.opts diff --git a/frame_test.go b/frame_test.go index 08874cb5..6b2e21f5 100644 --- a/frame_test.go +++ b/frame_test.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js package websocket @@ -54,7 +53,7 @@ func TestHeader(t *testing.T) { return r.Intn(2) == 0 } - for i := 0; i < 10000; i++ { + for range 10000 { h := header{ fin: randBool(), rsv1: randBool(), diff --git a/internal/errd/wrap.go b/internal/errd/wrap.go index 6e779131..c80d0a65 100644 --- a/internal/errd/wrap.go +++ b/internal/errd/wrap.go @@ -7,7 +7,7 @@ import ( // Wrap wraps err with fmt.Errorf if err is non nil. // Intended for use with defer and a named error return. // Inspired by https://github.com/golang/go/issues/32676. -func Wrap(err *error, f string, v ...interface{}) { +func Wrap(err *error, f string, v ...any) { if *err != nil { *err = fmt.Errorf(f+": %w", append(v, *err)...) } diff --git a/internal/examples/chat/chat.go b/internal/examples/chat/chat.go index 29f304b7..cc24ac7c 100644 --- a/internal/examples/chat/chat.go +++ b/internal/examples/chat/chat.go @@ -31,7 +31,7 @@ type chatServer struct { // logf controls where logs are sent. // Defaults to log.Printf. - logf func(f string, v ...interface{}) + logf func(f string, v ...any) // serveMux routes the various endpoints to the appropriate handler. serveMux http.ServeMux diff --git a/internal/examples/echo/server.go b/internal/examples/echo/server.go index 37e2f2c4..5748b110 100644 --- a/internal/examples/echo/server.go +++ b/internal/examples/echo/server.go @@ -17,7 +17,7 @@ import ( // only allows one message every 100ms with a 10 message burst. type echoServer struct { // logf controls where logs are sent. - logf func(f string, v ...interface{}) + logf func(f string, v ...any) } func (s echoServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { diff --git a/internal/test/assert/assert.go b/internal/test/assert/assert.go index 1b90cc9f..7fd98b08 100644 --- a/internal/test/assert/assert.go +++ b/internal/test/assert/assert.go @@ -9,7 +9,7 @@ import ( ) // Equal asserts exp == act. -func Equal(t testing.TB, name string, exp, got interface{}) { +func Equal(t testing.TB, name string, exp, got any) { t.Helper() if !reflect.DeepEqual(exp, got) { @@ -36,7 +36,7 @@ func Error(t testing.TB, err error) { } // Contains asserts the fmt.Sprint(v) contains sub. -func Contains(t testing.TB, v interface{}, sub string) { +func Contains(t testing.TB, v any, sub string) { t.Helper() s := fmt.Sprint(v) diff --git a/internal/test/wstest/pipe.go b/internal/test/wstest/pipe.go index b8cf094d..0e7fdb84 100644 --- a/internal/test/wstest/pipe.go +++ b/internal/test/wstest/pipe.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js package wstest diff --git a/internal/thirdparty/gin_test.go b/internal/thirdparty/gin_test.go index bd30ebdd..4c88337c 100644 --- a/internal/thirdparty/gin_test.go +++ b/internal/thirdparty/gin_test.go @@ -42,7 +42,7 @@ func TestGin(t *testing.T) { err = wsjson.Write(ctx, c, "hello") assert.Success(t, err) - var v interface{} + var v any err = wsjson.Read(ctx, c, &v) assert.Success(t, err) assert.Equal(t, "read msg", "hello", v) diff --git a/internal/wsjs/wsjs_js.go b/internal/wsjs/wsjs_js.go index 11eb59cb..45ecf49d 100644 --- a/internal/wsjs/wsjs_js.go +++ b/internal/wsjs/wsjs_js.go @@ -33,7 +33,7 @@ func New(url string, protocols []string) (c WebSocket, err error) { c = WebSocket{} }) - jsProtocols := make([]interface{}, len(protocols)) + jsProtocols := make([]any, len(protocols)) for i, p := range protocols { jsProtocols[i] = p } @@ -57,7 +57,7 @@ func (c WebSocket) setBinaryType(typ string) { } func (c WebSocket) addEventListener(eventType string, fn func(e js.Value)) func() { - f := js.FuncOf(func(this js.Value, args []js.Value) interface{} { + f := js.FuncOf(func(this js.Value, args []js.Value) any { fn(args[0]) return nil }) @@ -97,7 +97,7 @@ func (c WebSocket) OnError(fn func(e js.Value)) (remove func()) { // MessageEvent is the type passed to a message handler. type MessageEvent struct { // string or []byte. - Data interface{} + Data any // There are more fields to the interface but we don't use them. // See https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent @@ -106,7 +106,7 @@ type MessageEvent struct { // OnMessage registers a function to be called when the WebSocket receives a message. func (c WebSocket) OnMessage(fn func(m MessageEvent)) (remove func()) { return c.addEventListener("message", func(e js.Value) { - var data interface{} + var data any arrayBuffer := e.Get("data") if arrayBuffer.Type() == js.TypeString { diff --git a/mask_test.go b/mask_test.go index 00a9f0a2..7d6aedd7 100644 --- a/mask_test.go +++ b/mask_test.go @@ -40,7 +40,7 @@ func TestMask(t *testing.T) { func testMask(t *testing.T, name string, fn func(b []byte, key uint32) uint32) { t.Run(name, func(t *testing.T) { t.Parallel() - for i := 0; i < 9999; i++ { + for range 9999 { keyb := make([]byte, 4) _, err := rand.Read(keyb) assert.Success(t, err) diff --git a/netconn.go b/netconn.go index b118e4d3..1f73b04b 100644 --- a/netconn.go +++ b/netconn.go @@ -188,8 +188,7 @@ func (nc *netConn) read(p []byte) (int, error) { return n, err } -type websocketAddr struct { -} +type websocketAddr struct{} func (a websocketAddr) Network() string { return "websocket" diff --git a/netconn_notjs.go b/netconn_notjs.go index f3eb0d66..cab76349 100644 --- a/netconn_notjs.go +++ b/netconn_notjs.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js package websocket diff --git a/read.go b/read.go index 2db22435..aab9e141 100644 --- a/read.go +++ b/read.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js package websocket diff --git a/write.go b/write.go index 7324de74..7104b227 100644 --- a/write.go +++ b/write.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js package websocket @@ -351,10 +350,7 @@ func (c *Conn) writeFramePayload(p []byte) (n int, err error) { // Start of next write in the buffer. i := c.bw.Buffered() - j := len(p) - if j > c.bw.Available() { - j = c.bw.Available() - } + j := min(len(p), c.bw.Available()) _, err := c.bw.Write(p[:j]) if err != nil { diff --git a/ws_js.go b/ws_js.go index 5e324c47..8d52aeab 100644 --- a/ws_js.go +++ b/ws_js.go @@ -196,7 +196,7 @@ func (c *Conn) Ping(ctx context.Context) error { // Write writes a message of the given type to the connection. // Always non blocking. func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { - err := c.write(ctx, typ, p) + err := c.write(typ, p) if err != nil { // Have to ensure the WebSocket is closed after a write error // to match the Go API. It can only error if the message type @@ -210,7 +210,7 @@ func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { return nil } -func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) error { +func (c *Conn) write(typ MessageType, p []byte) error { if c.isClosed() { return net.ErrClosed } diff --git a/ws_js_test.go b/ws_js_test.go index b56ad16b..1fa242f4 100644 --- a/ws_js_test.go +++ b/ws_js_test.go @@ -28,7 +28,7 @@ func TestWasm(t *testing.T) { assert.Equal(t, "response code", http.StatusSwitchingProtocols, resp.StatusCode) c.SetReadLimit(65536) - for i := 0; i < 10; i++ { + for range 10 { err = wstest.Echo(ctx, c, 65536) assert.Success(t, err) } diff --git a/wsjson/wsjson.go b/wsjson/wsjson.go index 05e7cfa1..ffa068bf 100644 --- a/wsjson/wsjson.go +++ b/wsjson/wsjson.go @@ -14,11 +14,11 @@ import ( // Read reads a JSON message from c into v. // It will reuse buffers in between calls to avoid allocations. -func Read(ctx context.Context, c *websocket.Conn, v interface{}) error { +func Read(ctx context.Context, c *websocket.Conn, v any) error { return read(ctx, c, v) } -func read(ctx context.Context, c *websocket.Conn, v interface{}) (err error) { +func read(ctx context.Context, c *websocket.Conn, v any) (err error) { defer errd.Wrap(&err, "failed to read JSON message") _, r, err := c.Reader(ctx) @@ -45,11 +45,11 @@ func read(ctx context.Context, c *websocket.Conn, v interface{}) (err error) { // Write writes the JSON message v to c. // It will reuse buffers in between calls to avoid allocations. -func Write(ctx context.Context, c *websocket.Conn, v interface{}) error { +func Write(ctx context.Context, c *websocket.Conn, v any) error { return write(ctx, c, v) } -func write(ctx context.Context, c *websocket.Conn, v interface{}) (err error) { +func write(ctx context.Context, c *websocket.Conn, v any) (err error) { defer errd.Wrap(&err, "failed to write JSON message") // json.Marshal cannot reuse buffers between calls as it has to return From e11dd4e741173381256344d57b3bfc3fa38a9d25 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Wed, 3 Sep 2025 11:05:14 +0300 Subject: [PATCH 516/519] fix: match Origin scheme if defined in OriginPatterns (#536) This change aligns origin checks with RFC 6454 by adding scheme-aware matching to OriginPatterns. Fixes #529 --- accept.go | 13 +++++++++---- accept_test.go | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/accept.go b/accept.go index 0f3b0d16..cc990428 100644 --- a/accept.go +++ b/accept.go @@ -40,9 +40,10 @@ type AcceptOptions struct { // In such a case, example.com is the origin and chat.example.com is the request host. // One would set this field to []string{"example.com"} to authorize example.com to connect. // - // Each pattern is matched case insensitively against the request origin host - // with path.Match. - // See https://golang.org/pkg/path/#Match + // Each pattern is matched case insensitively with path.Match (see + // https://golang.org/pkg/path/#Match). By default, it is matched + // against the request origin host. If the pattern contains a URI + // scheme ("://"), it will be matched against "scheme://host". // // Please ensure you understand the ramifications of enabling this. // If used incorrectly your WebSocket server will be open to CSRF attacks. @@ -240,7 +241,11 @@ func authenticateOrigin(r *http.Request, originHosts []string) error { } for _, hostPattern := range originHosts { - matched, err := match(hostPattern, u.Host) + target := u.Host + if strings.Contains(hostPattern, "://") { + target = u.Scheme + "://" + u.Host + } + matched, err := match(hostPattern, target) if err != nil { return fmt.Errorf("failed to parse path pattern %q: %w", hostPattern, err) } diff --git a/accept_test.go b/accept_test.go index aeea1d8a..92dbfcc7 100644 --- a/accept_test.go +++ b/accept_test.go @@ -466,6 +466,42 @@ func Test_authenticateOrigin(t *testing.T) { }, success: false, }, + { + name: "originPatternsWithSchemeHttps", + origin: "https://two.example.com", + host: "example.com", + originPatterns: []string{ + "https://*.example.com", + }, + success: true, + }, + { + name: "originPatternsWithSchemeMismatch", + origin: "https://two.example.com", + host: "example.com", + originPatterns: []string{ + "http://*.example.com", + }, + success: false, + }, + { + name: "originPatternsWithSchemeAndPort", + origin: "https://example.com:8443", + host: "example.com", + originPatterns: []string{ + "https://example.com:8443", + }, + success: true, + }, + { + name: "backwardsCompatHostOnlyPattern", + origin: "http://two.example.com", + host: "example.com", + originPatterns: []string{ + "*.example.com", + }, + success: true, + }, } for _, tc := range testCases { From c7846ea6a8849fe21aed3b8319bac906e8cb1dca Mon Sep 17 00:00:00 2001 From: Mikhail Mazurskiy <126021+ash2k@users.noreply.github.com> Date: Thu, 4 Sep 2025 20:37:38 +1000 Subject: [PATCH 517/519] refactor: use `context.AfterFunc` to track timeouts instead of goroutine (#532) Closes #411 Closes #501 --- close.go | 6 ------ conn.go | 56 ++++++++++++++++++++++++++++---------------------------- read.go | 10 ++++++---- write.go | 10 +++------- 4 files changed, 37 insertions(+), 45 deletions(-) diff --git a/close.go b/close.go index 2de1a5c2..fcc68065 100644 --- a/close.go +++ b/close.go @@ -231,12 +231,6 @@ func (c *Conn) waitGoroutines() error { t := time.NewTimer(time.Second * 15) defer t.Stop() - select { - case <-c.timeoutLoopDone: - case <-t.C: - return errors.New("failed to wait for timeoutLoop goroutine to exit") - } - c.closeReadMu.Lock() closeRead := c.closeReadCtx != nil c.closeReadMu.Unlock() diff --git a/conn.go b/conn.go index 5907bc81..09234871 100644 --- a/conn.go +++ b/conn.go @@ -51,9 +51,8 @@ type Conn struct { br *bufio.Reader bw *bufio.Writer - readTimeout chan context.Context - writeTimeout chan context.Context - timeoutLoopDone chan struct{} + readTimeoutStop atomic.Pointer[func() bool] + writeTimeoutStop atomic.Pointer[func() bool] // Read state. readMu *mu @@ -113,10 +112,6 @@ func newConn(cfg connConfig) *Conn { br: cfg.br, bw: cfg.bw, - readTimeout: make(chan context.Context), - writeTimeout: make(chan context.Context), - timeoutLoopDone: make(chan struct{}), - closed: make(chan struct{}), activePings: make(map[string]chan<- struct{}), onPingReceived: cfg.onPingReceived, @@ -144,8 +139,6 @@ func newConn(cfg connConfig) *Conn { c.close() }) - go c.timeoutLoop() - return c } @@ -175,27 +168,34 @@ func (c *Conn) close() error { return err } -func (c *Conn) timeoutLoop() { - defer close(c.timeoutLoopDone) +func (c *Conn) setupWriteTimeout(ctx context.Context) { + stop := context.AfterFunc(ctx, func() { + c.clearWriteTimeout() + c.close() + }) + swapTimeoutStop(&c.writeTimeoutStop, &stop) +} - readCtx := context.Background() - writeCtx := context.Background() +func (c *Conn) clearWriteTimeout() { + swapTimeoutStop(&c.writeTimeoutStop, nil) +} - for { - select { - case <-c.closed: - return - - case writeCtx = <-c.writeTimeout: - case readCtx = <-c.readTimeout: - - case <-readCtx.Done(): - c.close() - return - case <-writeCtx.Done(): - c.close() - return - } +func (c *Conn) setupReadTimeout(ctx context.Context) { + stop := context.AfterFunc(ctx, func() { + c.clearReadTimeout() + c.close() + }) + swapTimeoutStop(&c.readTimeoutStop, &stop) +} + +func (c *Conn) clearReadTimeout() { + swapTimeoutStop(&c.readTimeoutStop, nil) +} + +func swapTimeoutStop(p *atomic.Pointer[func() bool], newStop *func() bool) { + oldStop := p.Swap(newStop) + if oldStop != nil { + (*oldStop)() } } diff --git a/read.go b/read.go index aab9e141..7ae0db18 100644 --- a/read.go +++ b/read.go @@ -220,22 +220,24 @@ func (c *Conn) readLoop(ctx context.Context) (header, error) { // to be called after the read is done. It also returns an error if the // connection is closed. The reference to the error is used to assign // an error depending on if the connection closed or the context timed -// out during use. Typically the referenced error is a named return +// out during use. Typically, the referenced error is a named return // variable of the function calling this method. func (c *Conn) prepareRead(ctx context.Context, err *error) (func(), error) { select { case <-c.closed: return nil, net.ErrClosed - case c.readTimeout <- ctx: + default: } + c.setupReadTimeout(ctx) done := func() { + c.clearReadTimeout() select { case <-c.closed: if *err != nil { *err = net.ErrClosed } - case c.readTimeout <- context.Background(): + default: } if *err != nil && ctx.Err() != nil { *err = ctx.Err() @@ -280,7 +282,7 @@ func (c *Conn) readFramePayload(ctx context.Context, p []byte) (_ int, err error return n, fmt.Errorf("failed to read frame payload: %w", err) } - return n, err + return n, nil } func (c *Conn) handleControl(ctx context.Context, h header) (err error) { diff --git a/write.go b/write.go index 7104b227..d7172a7b 100644 --- a/write.go +++ b/write.go @@ -271,14 +271,10 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco select { case <-c.closed: return 0, net.ErrClosed - case c.writeTimeout <- ctx: + default: } - defer func() { - select { - case <-c.closed: - case c.writeTimeout <- context.Background(): - } - }() + c.setupWriteTimeout(ctx) + defer c.clearWriteTimeout() c.writeHeader.fin = fin c.writeHeader.opcode = opcode From 7d7c644330e727379c3e33fddc154ac208b925f3 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Fri, 5 Sep 2025 09:16:22 +0100 Subject: [PATCH 518/519] refactor: add `ErrMessageTooBig` sentinel error for limited reads (#535) --------- Co-authored-by: Mathias Fredriksson --- conn_test.go | 19 +++++++++++++++++++ errors.go | 8 ++++++++ read.go | 9 +++++---- ws_js.go | 6 +++--- 4 files changed, 35 insertions(+), 7 deletions(-) create mode 100644 errors.go diff --git a/conn_test.go b/conn_test.go index 58ac394c..c3ccc886 100644 --- a/conn_test.go +++ b/conn_test.go @@ -421,6 +421,25 @@ func TestConn(t *testing.T) { err = c1.Close(websocket.StatusNormalClosure, "") assert.Success(t, err) }) + + t.Run("ReadLimitExceededReturnsErrMessageTooBig", func(t *testing.T) { + tt, c1, c2 := newConnTest(t, nil, nil) + + c1.SetReadLimit(1024) + _ = c2.CloseRead(tt.ctx) + + writeDone := xsync.Go(func() error { + payload := strings.Repeat("x", 4096) + return c2.Write(tt.ctx, websocket.MessageText, []byte(payload)) + }) + + _, _, err := c1.Read(tt.ctx) + assert.ErrorIs(t, websocket.ErrMessageTooBig, err) + assert.Contains(t, err, "read limited at 1025 bytes") + + _ = c2.CloseNow() + <-writeDone + }) } func TestWasm(t *testing.T) { diff --git a/errors.go b/errors.go new file mode 100644 index 00000000..bf4fc2b0 --- /dev/null +++ b/errors.go @@ -0,0 +1,8 @@ +package websocket + +import ( + "errors" +) + +// ErrMessageTooBig is returned when a message exceeds the read limit. +var ErrMessageTooBig = errors.New("websocket: message too big") diff --git a/read.go b/read.go index 7ae0db18..64822511 100644 --- a/read.go +++ b/read.go @@ -90,7 +90,8 @@ func (c *Conn) CloseRead(ctx context.Context) context.Context { // // By default, the connection has a message read limit of 32768 bytes. // -// When the limit is hit, the connection will be closed with StatusMessageTooBig. +// When the limit is hit, reads return an error wrapping ErrMessageTooBig and +// the connection is closed with StatusMessageTooBig. // // Set to -1 to disable. func (c *Conn) SetReadLimit(n int64) { @@ -522,9 +523,9 @@ func (lr *limitReader) Read(p []byte) (int, error) { } if lr.n == 0 { - err := fmt.Errorf("read limited at %v bytes", lr.limit.Load()) - lr.c.writeError(StatusMessageTooBig, err) - return 0, err + reason := fmt.Errorf("read limited at %d bytes", lr.limit.Load()) + lr.c.writeError(StatusMessageTooBig, reason) + return 0, fmt.Errorf("%w: %v", ErrMessageTooBig, reason) } if int64(len(p)) > lr.n { diff --git a/ws_js.go b/ws_js.go index 8d52aeab..026b75fc 100644 --- a/ws_js.go +++ b/ws_js.go @@ -144,9 +144,9 @@ func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { } readLimit := c.msgReadLimit.Load() if readLimit >= 0 && int64(len(p)) > readLimit { - err := fmt.Errorf("read limited at %v bytes", c.msgReadLimit.Load()) - c.Close(StatusMessageTooBig, err.Error()) - return 0, nil, err + reason := fmt.Errorf("read limited at %d bytes", c.msgReadLimit.Load()) + c.Close(StatusMessageTooBig, reason.Error()) + return 0, nil, fmt.Errorf("%w: %v", ErrMessageTooBig, reason) } return typ, p, nil } From 8bf6dd28ef54e75c9e6e0ea90547f6d4e5e75795 Mon Sep 17 00:00:00 2001 From: Igor Silvestre Date: Mon, 17 Nov 2025 06:12:40 -0300 Subject: [PATCH 519/519] chore: fix typo in Accept function (#544) --- accept.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accept.go b/accept.go index cc990428..bcfd38e8 100644 --- a/accept.go +++ b/accept.go @@ -90,7 +90,7 @@ func (opts *AcceptOptions) cloneWithDefaults() *AcceptOptions { } // Accept accepts a WebSocket handshake from a client and upgrades the -// the connection to a WebSocket. +// connection to a WebSocket. // // Accept will not allow cross origin requests by default. // See the InsecureSkipVerify and OriginPatterns options to allow cross origin requests.