From 599534b9f2b28d520d1476faa5febc4c8b9031ed Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 21 Sep 2019 13:20:45 -0500 Subject: [PATCH 1/5] 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 2/5] 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 3/5] 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 4/5] 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 5/5] 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)