diff --git a/.github/fmt/entrypoint.sh b/.github/fmt/entrypoint.sh index ca3741cd..f75e9366 100755 --- a/.github/fmt/entrypoint.sh +++ b/.github/fmt/entrypoint.sh @@ -1,26 +1,33 @@ #!/usr/bin/env bash -source .github/lib.sh +source .github/lib.sh || exit 1 -if [[ $(gofmt -l -s .) != "" ]]; then - echo "files are not formatted correctly" - echo "please run:" - echo "gofmt -w -s ." - exit 1 -fi +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 -out=$(go run golang.org/x/tools/cmd/goimports -l -local=nhooyr.io/ws .) -if [[ $out != "" ]]; then - echo "imports are not formatted correctly" - echo "please run:" - echo "goimports -w -local=nhooyr.io/ws ." - exit 1 -fi + go install golang.org/x/tools/cmd/stringer + 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 -out=$(go run mvdan.cc/sh/cmd/shfmt -l -s -sr .) -if [[ $out != "" ]]; then - echo "shell scripts are not formatted correctly" +if [[ $CI && $(unstaged_files) != "" ]]; then + set +x + echo + echo "files either need generation or are formatted incorrectly" echo "please run:" - echo "shfmt -w -s -sr ." + echo "./test.sh" + echo + git status exit 1 fi diff --git a/.github/lib.sh b/.github/lib.sh old mode 100755 new mode 100644 index 39030e99..06c8f4a5 --- a/.github/lib.sh +++ b/.github/lib.sh @@ -3,4 +3,16 @@ set -euxo pipefail export GO111MODULE=on -export GOFLAGS=-mod=readonly +export PAGER=cat + +# shellcheck disable=SC2034 +# CI is used by the scripts that source this file. +export CI=${GITHUB_ACTION-} + +if [[ $CI ]]; then + export GOFLAGS=-mod=readonly +fi + +unstaged_files() { + git ls-files --other --modified --exclude-standard +} diff --git a/.github/lint/Dockerfile b/.github/lint/Dockerfile new file mode 100644 index 00000000..a1c92a91 --- /dev/null +++ b/.github/lint/Dockerfile @@ -0,0 +1,10 @@ +FROM codercom/playcicache + +LABEL "com.github.actions.name"="lint" +LABEL "com.github.actions.description"="lint" +LABEL "com.github.actions.icon"="code" +LABEL "com.github.actions.color"="purple" + +COPY entrypoint.sh /entrypoint.sh + +CMD ["/entrypoint.sh"] diff --git a/.github/lint/entrypoint.sh b/.github/lint/entrypoint.sh new file mode 100755 index 00000000..c81f1f11 --- /dev/null +++ b/.github/lint/entrypoint.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +source .github/lib.sh || exit 1 + +( + shopt -s globstar nullglob dotglob + shellcheck ./**/*.sh +) + +go vet -composites=false ./... +go run golang.org/x/lint/golint -set_exit_status ./... diff --git a/.github/main.workflow b/.github/main.workflow index ebd3d575..c4947b00 100644 --- a/.github/main.workflow +++ b/.github/main.workflow @@ -1,6 +1,10 @@ workflow "main" { on = "push" - resolves = ["fmt", "test"] + resolves = ["fmt", "lint", "test"] +} + +action "lint" { + uses = "./.github/lint" } action "fmt" { diff --git a/.github/test/entrypoint.sh b/.github/test/entrypoint.sh index cf4b14d0..09adedf6 100755 --- a/.github/test/entrypoint.sh +++ b/.github/test/entrypoint.sh @@ -1,37 +1,17 @@ #!/usr/bin/env bash -source .github/lib.sh +source .github/lib.sh || exit 1 -function gomod_help() { - echo - echo "you may need to update go.mod/go.sum via:" - echo "go list all > /dev/null" - echo "go mod tidy" - echo - echo "or git add files to staging" - exit 1 -} - -go list ./... > /dev/null || gomod_help -go mod tidy - -# Until https://github.com/golang/go/issues/27005 the previous command can actually modify go.sum so we need to ensure its not changed. -if [[ $(git diff --name-only) != "" ]]; then - git diff - gomod_help -fi - -mapfile -t scripts <<< "$(find . -type f -name "*.sh")" -shellcheck "${scripts[@]}" +COVERAGE_PROFILE=$(mktemp) +go test -race -v "-coverprofile=${COVERAGE_PROFILE}" -vet=off ./... +go tool cover "-func=${COVERAGE_PROFILE}" -go vet -composites=false ./... - -go test -race -v -coverprofile=coverage.out -vet=off ./... - -if [[ -z ${GITHUB_ACTION-} ]]; then - go tool cover -html=coverage.out +if [[ $CI ]]; then + bash <(curl -s https://codecov.io/bash) -f "$COVERAGE_PROFILE" else - bash <(curl -s https://codecov.io/bash) -fi + go tool cover "-html=${COVERAGE_PROFILE}" -o=coverage.html -rm coverage.out + set +x + echo + echo "please open coverage.html to see detailed test coverage stats" +fi diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..4383ca89 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +coverage.html diff --git a/README.md b/README.md index d4ac3f7b..410740fd 100644 --- a/README.md +++ b/README.md @@ -13,3 +13,32 @@ This library is in heavy development. ```bash go get nhooyr.io/ws ``` + +## Why + +There is no other Go WebSocket library with a clean API. + +Comparisons with existing WebSocket libraries below. + +### [x/net/websocket](https://godoc.org/golang.org/x/net/websocket) + + +Unmaintained and the API does not reflect WebSocket semantics. + +See https://github.com/golang/go/issues/18152 + +### [gorilla/websocket](https://github.com/gorilla/websocket) + +This package is the community standard but it is very old and over time +has accumulated cruft. There are many ways to do the same thing and the API +overall is just not very clear. + +The callback hooks are also confusing. The API for this library has been designed +such that there is only one way to do things and callbacks have been avoided. + +Performance sensitive applications should use ws/wscore directly. + +## [gobwas/ws](https://github.com/gobwas/ws) + +This library has an extremely flexible API but that comes at a cost of usability +and clarity. Its just not clear and simple how to do things in a safe manner. diff --git a/accept.go b/accept.go new file mode 100644 index 00000000..fb227eb3 --- /dev/null +++ b/accept.go @@ -0,0 +1,37 @@ +package ws + +import ( + "fmt" + "net/http" +) + +// AcceptOption is an option that can be passed to Accept. +type AcceptOption interface { + acceptOption() + fmt.Stringer +} + +// AcceptSubprotocols list the subprotocols that Accept will negotiate with a client. +// The first protocol that a client supports will be negotiated. +// Pass "" as a subprotocol if you would like to allow the default protocol. +func AcceptSubprotocols(subprotocols ...string) AcceptOption { + panic("TODO") +} + +// AcceptOrigins lists the origins that Accept will accept. +// Accept will always accept r.Host as the origin so you do not need to +// specify that with this option. +// Use this option with caution to avoid exposing your WebSocket +// server to a CSRF attack. +// See https://stackoverflow.com/a/37837709/4283659 +func AcceptOrigins(origins ...string) AcceptOption { + panic("TODO") +} + +// Accept accepts a WebSocket handshake from a client and upgrades the +// the connection to WebSocket. +// Accept will reject the handshake if the Origin is not the same as the Host unless +// InsecureAcceptOrigin is passed. +func Accept(w http.ResponseWriter, r *http.Request, opts ...AcceptOption) (*Conn, error) { + panic("TODO") +} diff --git a/datatype.go b/datatype.go new file mode 100644 index 00000000..ae62f0ea --- /dev/null +++ b/datatype.go @@ -0,0 +1,15 @@ +package ws + +import ( + "nhooyr.io/ws/wscore" +) + +// DataType represents the Opcode of a WebSocket data frame. +//go:generate stringer -type=DataType +type DataType int + +// DataType constants. +const ( + Text DataType = DataType(wscore.OpText) + Binary DataType = DataType(wscore.OpBinary) +) diff --git a/datatype_string.go b/datatype_string.go new file mode 100644 index 00000000..e4fdf8f7 --- /dev/null +++ b/datatype_string.go @@ -0,0 +1,25 @@ +// Code generated by "stringer -type=DataType"; DO NOT EDIT. + +package ws + +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[Text-1] + _ = x[Binary-2] +} + +const _DataType_name = "TextBinary" + +var _DataType_index = [...]uint8{0, 4, 10} + +func (i DataType) String() string { + i -= 1 + if i < 0 || i >= DataType(len(_DataType_index)-1) { + return "DataType(" + strconv.FormatInt(int64(i+1), 10) + ")" + } + return _DataType_name[_DataType_index[i]:_DataType_index[i+1]] +} diff --git a/dial.go b/dial.go new file mode 100644 index 00000000..119dbff9 --- /dev/null +++ b/dial.go @@ -0,0 +1,41 @@ +package ws + +import ( + "context" + "encoding/base64" + "fmt" + "net/http" +) + +// DialOption represents a dial option that can be passed to Dial. +type DialOption interface { + dialOption() + fmt.Stringer +} + +// DialHTTPClient is the http client used for the handshake. +// Its Transport must use HTTP/1.1 and must return writable bodies +// for WebSocket handshakes. +// http.Transport does this correctly. +func DialHTTPClient(h *http.Client) DialOption { + panic("TODO") +} + +// DialHeader are the HTTP headers included in the handshake request. +func DialHeader(h http.Header) DialOption { + panic("TODO") +} + +// DialSubprotocols accepts a slice of protcols to include in the Sec-WebSocket-Protocol header. +func DialSubprotocols(subprotocols ...string) DialOption { + panic("TODO") +} + +// We use this key for all client requests as the Sec-WebSocket-Key header is useless. +// See https://stackoverflow.com/a/37074398/4283659. +var secWebSocketKey = base64.StdEncoding.EncodeToString(make([]byte, 16)) + +// Dial performs a websocket handshake on the given url with the given options. +func Dial(ctx context.Context, u string, opts ...DialOption) (*Conn, *http.Response, error) { + panic("TODO") +} diff --git a/doc.go b/doc.go new file mode 100644 index 00000000..d746de69 --- /dev/null +++ b/doc.go @@ -0,0 +1,2 @@ +// Package ws implements the WebSocket protocol defined in RFC 6455. +package ws diff --git a/example_test.go b/example_test.go new file mode 100644 index 00000000..f0c2303a --- /dev/null +++ b/example_test.go @@ -0,0 +1,131 @@ +package ws_test + +import ( + "context" + "io" + "log" + "net/http" + "time" + + "golang.org/x/time/rate" + + "nhooyr.io/ws" + "nhooyr.io/ws/wsjson" +) + +func ExampleAccept_echo() { + fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c, err := ws.Accept(w, r, + ws.AcceptSubprotocols("echo"), + ) + if err != nil { + log.Printf("server handshake failed: %v", err) + return + } + defer c.Close(ws.StatusInternalError, "") + + ctx := context.Background() + + echo := func() error { + ctx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + + typ, r, err := c.ReadMessage(ctx) + if err != nil { + return err + } + + ctx, cancel = context.WithTimeout(ctx, time.Second*10) + defer cancel() + + r.SetContext(ctx) + r.Limit(32768) + + w := c.MessageWriter(ctx, typ) + _, err = io.Copy(w, r) + if err != nil { + return err + } + + err = w.Close() + if err != nil { + return err + } + + return nil + } + + l := rate.NewLimiter(rate.Every(time.Millisecond*100), 10) + for l.Allow() { + err := echo() + if err != nil { + log.Printf("failed to read message: %v", err) + return + } + } + }) + // For production deployments, use a net/http.Server configured + // with the appropriate timeouts. + err := http.ListenAndServe("localhost:8080", fn) + if err != nil { + log.Fatalf("failed to listen and serve: %v", err) + } +} + +func ExampleAccept() { + fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c, err := ws.Accept(w, r, + ws.AcceptSubprotocols("echo"), + ) + if err != nil { + log.Printf("server handshake failed: %v", err) + return + } + defer c.Close(ws.StatusInternalError, "") + + type myJsonStruct struct { + MyField string `json:"my_field"` + } + err = wsjson.Write(r.Context(), c, myJsonStruct{ + MyField: "foo", + }) + if err != nil { + log.Printf("failed to write json struct: %v", err) + return + } + + c.Close(ws.StatusNormalClosure, "") + }) + // For production deployments, use a net/http.Server configured + // with the appropriate timeouts. + err := http.ListenAndServe("localhost:8080", fn) + if err != nil { + log.Fatalf("failed to listen and serve: %v", err) + } +} + +func ExampleDial() { + ctx := context.Background() + ctx, cancel := context.WithTimeout(ctx, time.Minute) + defer cancel() + + c, _, err := ws.Dial(ctx, "ws://localhost:8080") + if err != nil { + log.Fatalf("failed to ws dial: %v", err) + return + } + defer c.Close(ws.StatusInternalError, "") + + type myJsonStruct struct { + MyField string `json:"my_field"` + } + err = wsjson.Write(ctx, c, myJsonStruct{ + MyField: "foo", + }) + if err != nil { + log.Fatalf("failed to write json struct: %v", err) + return + } + + c.Close(ws.StatusNormalClosure, "") +} diff --git a/go.mod b/go.mod index a049474e..4749de6d 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,11 @@ module nhooyr.io/ws go 1.12 require ( + github.com/golang/protobuf v1.3.1 github.com/kr/pretty v0.1.0 // indirect - golang.org/x/tools v0.0.0-20190315191501-e6df0c1bb376 + go.coder.com/go-tools v0.0.0-20190317003359-0c6a35b74a16 + golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 + golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 + golang.org/x/tools v0.0.0-20190329215204-73054e8977d1 mvdan.cc/sh v2.6.4+incompatible ) diff --git a/go.sum b/go.sum index 0690b06a..b5d8f2fc 100644 --- a/go.sum +++ b/go.sum @@ -1,14 +1,24 @@ +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/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= +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-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +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/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= -golang.org/x/tools v0.0.0-20190315191501-e6df0c1bb376 h1:GfNg/J4IAJguoN+DWPTZ54ElycoBxtQkpxhrbA5edVA= -golang.org/x/tools v0.0.0-20190315191501-e6df0c1bb376/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +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-20190329215204-73054e8977d1 h1:rLRH2E2wN5JjGJSVlBe1ioUkCKgb6eoL9X8bDmtEpsk= +golang.org/x/tools v0.0.0-20190329215204-73054e8977d1/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 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/statuscode.go b/statuscode.go new file mode 100644 index 00000000..5ceb24ac --- /dev/null +++ b/statuscode.go @@ -0,0 +1,69 @@ +package ws + +import ( + "encoding/binary" + "errors" + "fmt" + "math/bits" +) + +// StatusCode represents a WebSocket status code. +//go:generate stringer -type=StatusCode +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 StatusCode = 1005 + iota + StatusAbnormalClosure + StatusInvalidFramePayloadData + StatusPolicyViolation + StatusMessageTooBig + StatusMandatoryExtension + StatusInternalError + StatusServiceRestart + StatusTryAgainLater + StatusBadGateway + StatusTLSHandshake +) + +// CloseError represents an error from a WebSocket close frame. +// Methods on the Conn will only return this for a non normal close code. +type CloseError struct { + Code StatusCode + Reason string +} + +func (e CloseError) Error() string { + return fmt.Sprintf("WebSocket closed with status = %v and reason = %q", e.Code, e.Reason) +} + +func parseClosePayload(p []byte) (code StatusCode, reason []byte, err error) { + if len(p) < 2 { + return 0, nil, fmt.Errorf("close payload too small, cannot even contain the 2 byte status code") + } + + code = StatusCode(binary.BigEndian.Uint16(p)) + reason = p[2:] + + return code, reason, nil +} + +func closePayload(code StatusCode, reason []byte) ([]byte, error) { + if bits.Len(uint(code)) > 16 { + return nil, errors.New("status code is larger than 2 bytes") + } + if code == StatusNoStatusRcvd || code == StatusAbnormalClosure { + return nil, fmt.Errorf("status code %v cannot be set by applications", code) + } + + buf := make([]byte, 2+len(reason)) + binary.BigEndian.PutUint16(buf[:], uint16(code)) + copy(buf[2:], reason) + return buf, nil +} diff --git a/statuscode_string.go b/statuscode_string.go new file mode 100644 index 00000000..2d4ab765 --- /dev/null +++ b/statuscode_string.go @@ -0,0 +1,49 @@ +// Code generated by "stringer -type=StatusCode"; DO NOT EDIT. + +package ws + +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[StatusNormalClosure-1000] + _ = x[StatusGoingAway-1001] + _ = x[StatusProtocolError-1002] + _ = x[StatusUnsupportedData-1003] + _ = x[StatusNoStatusRcvd-1009] + _ = x[StatusAbnormalClosure-1010] + _ = x[StatusInvalidFramePayloadData-1011] + _ = x[StatusPolicyViolation-1012] + _ = x[StatusMessageTooBig-1013] + _ = x[StatusMandatoryExtension-1014] + _ = x[StatusInternalError-1015] + _ = x[StatusServiceRestart-1016] + _ = x[StatusTryAgainLater-1017] + _ = x[StatusBadGateway-1018] + _ = x[StatusTLSHandshake-1019] +} + +const ( + _StatusCode_name_0 = "StatusNormalClosureStatusGoingAwayStatusProtocolErrorStatusUnsupportedData" + _StatusCode_name_1 = "StatusNoStatusRcvdStatusAbnormalClosureStatusInvalidFramePayloadDataStatusPolicyViolationStatusMessageTooBigStatusMandatoryExtensionStatusInternalErrorStatusServiceRestartStatusTryAgainLaterStatusBadGatewayStatusTLSHandshake" +) + +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} +) + +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 1009 <= i && i <= 1019: + i -= 1009 + return _StatusCode_name_1[_StatusCode_index_1[i]:_StatusCode_index_1[i+1]] + default: + return "StatusCode(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/test.sh b/test.sh index 8eadf573..736dcda7 100755 --- a/test.sh +++ b/test.sh @@ -1,7 +1,27 @@ #!/usr/bin/env bash -# This is for local testing. See .github for CI. -source ./.github/lib.sh +# This script is for local testing. See .github for CI. -./.github/fmt/entrypoint.sh -./.github/test/entrypoint.sh +source .github/lib.sh || exit 1 +cd "$(dirname "${0}")" + +function docker_run() { + local DIR="$1" + local IMAGE + IMAGE="$(docker build -q "$DIR")" + docker run \ + -v "${PWD}:/repo" \ + -v "$(go env GOPATH):/go" \ + -v "$(go env GOCACHE):/root/.cache/go-build" \ + -w /repo \ + "${IMAGE}" +} + +if [[ $# -gt 0 ]]; then + docker_run ".github/$*" + exit 0 +fi + +docker_run .github/fmt +docker_run .github/lint +docker_run .github/test diff --git a/tools.go b/tools.go index 5ef1d873..c78042b7 100644 --- a/tools.go +++ b/tools.go @@ -4,6 +4,8 @@ package tools // See https://github.com/go-modules-by-example/index/blob/master/010_tools/README.md import ( - _ "golang.org/x/tools/cmd/goimports" + _ "go.coder.com/go-tools/cmd/goimports" + _ "golang.org/x/lint/golint" + _ "golang.org/x/tools/cmd/stringer" _ "mvdan.cc/sh/cmd/shfmt" ) diff --git a/ws.go b/ws.go new file mode 100644 index 00000000..7ec54ed7 --- /dev/null +++ b/ws.go @@ -0,0 +1,86 @@ +package ws + +import ( + "context" + "net" +) + +const ( + secWebSocketProtocol = "Sec-WebSocket-Protocol" +) + +// Conn represents a WebSocket connection. +type Conn struct { +} + +// Subprotocol returns the negotiated subprotocol. +// An empty string means the default protocol. +func (c *Conn) Subprotocol() string { + panic("TODO") +} + +// NetConn returns the net.Conn underlying the Conn. +func (c *Conn) NetConn() net.Conn { + panic("TODO") +} + +// MessageWriter returns a writer bounded by the context that will write +// a WebSocket data frame of type dataType to the connection. +// Ensure you close the MessageWriter once you have written to entire message. +// Concurrent calls to MessageWriter are ok. +func (c *Conn) MessageWriter(ctx context.Context, dataType DataType) *MessageWriter { + panic("TODO") +} + +// ReadMessage will wait until there is a WebSocket data frame to read from the connection. +// It returns the type of the data, a reader to read it and also an error. +// Please use SetContext on the reader to bound the read operation. +func (c *Conn) ReadMessage(ctx context.Context) (DataType, *MessageReader, error) { + panic("TODO") +} + +// 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. +func (c *Conn) Close(code StatusCode, reason string) error { + panic("TODO") +} + +// MessageWriter enables writing to a WebSocket connection. +// Ensure you close the MessageWriter once you have written to entire message. +type MessageWriter struct { +} + +// Write writes the given bytes to the WebSocket connection. +// The frame will automatically be fragmented as appropriate +// with the buffers obtained from http.Hijacker. +// Please ensure you call Close once you have written the full message. +func (w *MessageWriter) Write(p []byte) (n int, err error) { + panic("TODO") +} + +// Close flushes the frame to the connection. +// This must be called for every MessageWriter. +func (w *MessageWriter) Close() error { + panic("TODO") +} + +// MessageReader enables reading a data frame from the WebSocket connection. +type MessageReader struct { +} + +// SetContext bounds the read operation to the ctx. +// By default, the context is the one passed to conn.ReadMessage. +// You still almost always want a separate context for reading the message though. +func (r *MessageReader) SetContext(ctx context.Context) { + panic("TODO") +} + +// Limit limits the number of bytes read by the reader. +func (r *MessageReader) Limit(bytes int) { + panic("TODO") +} + +// Read reads as many bytes as possible into p. +func (r *MessageReader) Read(p []byte) (n int, err error) { + panic("TODO") +} diff --git a/wscore/header.go b/wscore/header.go new file mode 100644 index 00000000..bcbd41eb --- /dev/null +++ b/wscore/header.go @@ -0,0 +1,30 @@ +package wscore + +import ( + "io" +) + +// 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 +} + +// Bytes returns the bytes of the header. +func (h Header) Bytes() []byte { + panic("TODO") +} + +// ReadHeader reads a header from the reader. +func ReadHeader(r io.Reader) []byte { + panic("TODO") +} diff --git a/wscore/mask.go b/wscore/mask.go new file mode 100644 index 00000000..c6c08ccd --- /dev/null +++ b/wscore/mask.go @@ -0,0 +1,15 @@ +package wscore + +// Mask 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 +// +// It is highly optimized to mask per word with the usage +// of unsafe. +// +// For targets that do not support unsafe, please report an issue. +// There is a mask by byte function below that will be used for such targets. +func Mask(key [4]byte, pos int, p []byte) int { + panic("TODO") +} diff --git a/wscore/opcode.go b/wscore/opcode.go new file mode 100644 index 00000000..878eb80e --- /dev/null +++ b/wscore/opcode.go @@ -0,0 +1,17 @@ +package wscore + +// Opcode represents a WebSocket Opcode. +//go:generate stringer -type=Opcode +type Opcode int + +// Opcode constants. +const ( + OpContinuation Opcode = iota + OpText + OpBinary + // 3 - 7 are reserved for further non-control frames. + OpClose Opcode = 8 + iota + OpPing + OpPong + // 11-16 are reserved for further control frames. +) diff --git a/wscore/opcode_string.go b/wscore/opcode_string.go new file mode 100644 index 00000000..8ed5c1c7 --- /dev/null +++ b/wscore/opcode_string.go @@ -0,0 +1,39 @@ +// Code generated by "stringer -type=Opcode"; DO NOT EDIT. + +package wscore + +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-11] + _ = x[OpPing-12] + _ = x[OpPong-13] +} + +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 11 <= i && i <= 13: + i -= 11 + return _Opcode_name_1[_Opcode_index_1[i]:_Opcode_index_1[i+1]] + default: + return "Opcode(" + strconv.FormatInt(int64(i), 10) + ")" + } +} diff --git a/wsjson/wsjon.go b/wsjson/wsjon.go new file mode 100644 index 00000000..33dc0154 --- /dev/null +++ b/wsjson/wsjon.go @@ -0,0 +1,17 @@ +package wsjson + +import ( + "context" + + "nhooyr.io/ws" +) + +// Read reads a json message from c into v. +func Read(ctx context.Context, c *ws.Conn, v interface{}) error { + panic("TODO") +} + +// Write writes the json message v into c. +func Write(ctx context.Context, c *ws.Conn, v interface{}) error { + panic("TODO") +} diff --git a/wspb/wspb.go b/wspb/wspb.go new file mode 100644 index 00000000..9c7b1549 --- /dev/null +++ b/wspb/wspb.go @@ -0,0 +1,19 @@ +package wspb + +import ( + "context" + + "github.com/golang/protobuf/proto" + + "nhooyr.io/ws" +) + +// Read reads a protobuf message from c into v. +func Read(ctx context.Context, c *ws.Conn, v proto.Message) error { + panic("TODO") +} + +// Write writes the protobuf message into c. +func Write(ctx context.Context, c *ws.Conn, v proto.Message) error { + panic("TODO") +}