Skip to content

Implement core API for WASM #142

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Sep 22, 2019
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,23 @@ 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:
fetch-depth: 1
- 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:
fetch-depth: 1
- 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:
Expand All @@ -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:
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,18 @@ 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)
- 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: This is an abbreviation (not an acronym) so it's usually spelled "Wasm".

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.


## Roadmap

- [ ] WebSockets over HTTP/2 [#4](https://github.com/nhooyr/websocket/issues/4)
- [ ] WASM Compilation [#121](https://github.com/nhooyr/websocket/issues/121)

## Examples

Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions accept.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// +build !js

package websocket

import (
Expand Down Expand Up @@ -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)
Expand Down
19 changes: 19 additions & 0 deletions accept_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// +build !js

package websocket

import (
Expand Down Expand Up @@ -45,6 +47,7 @@ func Test_verifyClientHandshake(t *testing.T) {
testCases := []struct {
name string
method string
http1 bool
h map[string]string
success bool
}{
Expand Down Expand Up @@ -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{
Expand All @@ -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)
}
Expand Down
46 changes: 46 additions & 0 deletions cmp_test.go → assert_test.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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))
}
2 changes: 1 addition & 1 deletion ci/fmt.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 \
Expand Down
7 changes: 7 additions & 0 deletions ci/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
10 changes: 7 additions & 3 deletions ci/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@ if [[ ${CI-} ]]; then
)
fi

argv+=(
"$@"
)
if [[ $# -gt 0 ]]; then
argv+=(
"$@"
)
else
argv+=(./...)
fi

mkdir -p ci/out/websocket
"${argv[@]}"
Expand Down
1 change: 1 addition & 0 deletions ci/tools.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
22 changes: 19 additions & 3 deletions ci/wasm.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,23 @@ 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
GOOS=js GOARCH=wasm golint -set_exit_status ./...

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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will improve this test to assert proper closure of the connection.

6 changes: 6 additions & 0 deletions dial.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// +build !js

package websocket

import (
Expand Down Expand Up @@ -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
}

Expand Down
12 changes: 12 additions & 0 deletions dial_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// +build !js

package websocket

import (
Expand Down Expand Up @@ -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) {
Expand Down
25 changes: 25 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// +build !js

// Package websocket is a minimal and idiomatic implementation of the WebSocket protocol.
//
// https://tools.ietf.org/html/rfc6455
Expand All @@ -15,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"
2 changes: 2 additions & 0 deletions example_echo_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// +build !js

package websocket_test

import (
Expand Down
2 changes: 2 additions & 0 deletions example_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// +build !js

package websocket_test

import (
Expand Down
2 changes: 2 additions & 0 deletions export_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
// +build !js

package websocket

import (
Expand Down
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -22,6 +23,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
Expand Down
Loading