diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml deleted file mode 100644 index fb83c3a9..00000000 --- a/.github/FUNDING.yml +++ /dev/null @@ -1 +0,0 @@ -github: nhooyr diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..fb0a4558 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,24 @@ +version: 2 +updates: + # Track in case we ever add dependencies. + - package-ecosystem: 'gomod' + directory: '/' + schedule: + interval: 'weekly' + commit-message: + prefix: 'chore' + + # Keep example and test/benchmark deps up-to-date. + - package-ecosystem: 'gomod' + directories: + - '/internal/examples' + - '/internal/thirdparty' + schedule: + interval: 'monthly' + commit-message: + prefix: 'chore' + labels: [] + groups: + internal-deps: + patterns: + - '*' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e9b4b5f6..836381ef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,11 @@ name: ci -on: [push, pull_request] +on: + push: + branches: + - master + pull_request: + branches: + - master concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} cancel-in-progress: true @@ -12,7 +18,7 @@ jobs: - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - - run: ./ci/fmt.sh + - run: make fmt lint: runs-on: ubuntu-latest @@ -22,17 +28,23 @@ jobs: - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - - run: ./ci/lint.sh + - run: make lint test: runs-on: ubuntu-latest steps: + - name: Disable AppArmor + if: runner.os == 'Linux' + run: | + # Disable AppArmor for Ubuntu 23.10+. + # https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md + echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - - run: ./ci/test.sh - - uses: actions/upload-artifact@v3 + - run: make test + - uses: actions/upload-artifact@v4 with: name: coverage.html path: ./ci/out/coverage.html @@ -44,4 +56,4 @@ jobs: - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - - run: ./ci/bench.sh + - run: make bench diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index 2ba9ce34..62e3d337 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -15,16 +15,22 @@ jobs: - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - - run: AUTOBAHN=1 ./ci/bench.sh + - run: AUTOBAHN=1 make bench test: runs-on: ubuntu-latest steps: + - name: Disable AppArmor + if: runner.os == 'Linux' + run: | + # Disable AppArmor for Ubuntu 23.10+. + # https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md + echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - - run: AUTOBAHN=1 ./ci/test.sh - - uses: actions/upload-artifact@v3 + - run: AUTOBAHN=1 make test + - uses: actions/upload-artifact@v4 with: name: coverage.html path: ./ci/out/coverage.html @@ -37,18 +43,24 @@ jobs: - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - - run: AUTOBAHN=1 ./ci/bench.sh + - run: AUTOBAHN=1 make bench test-dev: runs-on: ubuntu-latest steps: + - name: Disable AppArmor + if: runner.os == 'Linux' + run: | + # Disable AppArmor for Ubuntu 23.10+. + # https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md + echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns - uses: actions/checkout@v4 with: ref: dev - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - - run: AUTOBAHN=1 ./ci/test.sh - - uses: actions/upload-artifact@v3 + - run: AUTOBAHN=1 make test + - uses: actions/upload-artifact@v4 with: name: coverage-dev.html path: ./ci/out/coverage.html diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml new file mode 100644 index 00000000..a78ce1b9 --- /dev/null +++ b/.github/workflows/static.yml @@ -0,0 +1,52 @@ +name: static + +on: + push: + branches: ['master'] + workflow_dispatch: + +# Set permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages. +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: true + +jobs: + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + steps: + - name: Disable AppArmor + if: runner.os == 'Linux' + run: | + # Disable AppArmor for Ubuntu 23.10+. + # https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md + echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns + - name: Checkout + uses: actions/checkout@v4 + - name: Setup Pages + uses: actions/configure-pages@v5 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: ./go.mod + - name: Generate coverage and badge + run: | + make test + mkdir -p ./ci/out/static + cp ./ci/out/coverage.html ./ci/out/static/coverage.html + percent=$(go tool cover -func ./ci/out/coverage.prof | tail -n1 | awk '{print $3}' | tr -d '%') + wget -O ./ci/out/static/coverage.svg "https://img.shields.io/badge/coverage-${percent}%25-success" + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./ci/out/static/ + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/LICENSE.txt b/LICENSE.txt index 77b5bef6..7e79329f 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2023 Anmol Sethi +Copyright (c) 2025 Coder Permission to use, copy, modify, and distribute this software for any purpose with or without fee is hereby granted, provided that the above diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..a3e4a20d --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +.PHONY: all +all: fmt lint test + +.PHONY: fmt +fmt: + ./ci/fmt.sh + +.PHONY: lint +lint: + ./ci/lint.sh + +.PHONY: test +test: + ./ci/test.sh + +.PHONY: bench +bench: + ./ci/bench.sh \ No newline at end of file diff --git a/README.md b/README.md index d093746d..6e986897 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,36 @@ # websocket -[![Go Reference](https://pkg.go.dev/badge/nhooyr.io/websocket.svg)](https://pkg.go.dev/nhooyr.io/websocket) -[![Go Coverage](https://img.shields.io/badge/coverage-91%25-success)](https://nhooyr.io/websocket/coverage.html) +[![Go Reference](https://pkg.go.dev/badge/github.com/coder/websocket.svg)](https://pkg.go.dev/github.com/coder/websocket) +[![Go Coverage](https://coder.github.io/websocket/coverage.svg)](https://coder.github.io/websocket/coverage.html) websocket is a minimal and idiomatic WebSocket library for Go. ## Install ```sh -go get nhooyr.io/websocket +go get github.com/coder/websocket ``` +> [!NOTE] +> Coder now maintains this project as explained in [this blog post](https://coder.com/blog/websocket). +> We're grateful to [nhooyr](https://github.com/nhooyr) for authoring and maintaining this project from +> 2019 to 2024. + ## Highlights - Minimal and idiomatic API - First class [context.Context](https://blog.golang.org/context) support - Fully passes the WebSocket [autobahn-testsuite](https://github.com/crossbario/autobahn-testsuite) -- [Zero dependencies](https://pkg.go.dev/nhooyr.io/websocket?tab=imports) -- JSON helpers in the [wsjson](https://pkg.go.dev/nhooyr.io/websocket/wsjson) subpackage +- [Zero dependencies](https://pkg.go.dev/github.com/coder/websocket?tab=imports) +- JSON helpers in the [wsjson](https://pkg.go.dev/github.com/coder/websocket/wsjson) subpackage - Zero alloc reads and writes - Concurrent writes -- [Close handshake](https://pkg.go.dev/nhooyr.io/websocket#Conn.Close) -- [net.Conn](https://pkg.go.dev/nhooyr.io/websocket#NetConn) wrapper -- [Ping pong](https://pkg.go.dev/nhooyr.io/websocket#Conn.Ping) API +- [Close handshake](https://pkg.go.dev/github.com/coder/websocket#Conn.Close) +- [net.Conn](https://pkg.go.dev/github.com/coder/websocket#NetConn) wrapper +- [Ping pong](https://pkg.go.dev/github.com/coder/websocket#Conn.Ping) API - [RFC 7692](https://tools.ietf.org/html/rfc7692) permessage-deflate compression -- [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper for write only connections -- Compile to [Wasm](https://pkg.go.dev/nhooyr.io/websocket#hdr-Wasm) +- [CloseRead](https://pkg.go.dev/github.com/coder/websocket#Conn.CloseRead) helper for write only connections +- Compile to [Wasm](https://pkg.go.dev/github.com/coder/websocket#hdr-Wasm) ## Roadmap @@ -58,10 +63,12 @@ http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) { } defer c.CloseNow() - ctx, cancel := context.WithTimeout(r.Context(), time.Second*10) + // Set the context as needed. Use of r.Context() is not recommended + // to avoid surprising behavior (see http.Hijacker). + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() - var v interface{} + var v any err = wsjson.Read(ctx, c, &v) if err != nil { // ... @@ -102,14 +109,14 @@ Advantages of [gorilla/websocket](https://github.com/gorilla/websocket): - Mature and widely used - [Prepared writes](https://pkg.go.dev/github.com/gorilla/websocket#PreparedMessage) - Configurable [buffer sizes](https://pkg.go.dev/github.com/gorilla/websocket#hdr-Buffers) -- No extra goroutine per connection to support cancellation with context.Context. This costs nhooyr.io/websocket 2 KB of memory per connection. +- No extra goroutine per connection to support cancellation with context.Context. This costs github.com/coder/websocket 2 KB of memory per connection. - Will be removed soon with [context.AfterFunc](https://github.com/golang/go/issues/57928). See [#411](https://github.com/nhooyr/websocket/issues/411) -Advantages of nhooyr.io/websocket: +Advantages of github.com/coder/websocket: - Minimal and idiomatic API - - Compare godoc of [nhooyr.io/websocket](https://pkg.go.dev/nhooyr.io/websocket) with [gorilla/websocket](https://pkg.go.dev/github.com/gorilla/websocket) side by side. -- [net.Conn](https://pkg.go.dev/nhooyr.io/websocket#NetConn) wrapper + - Compare godoc of [github.com/coder/websocket](https://pkg.go.dev/github.com/coder/websocket) with [gorilla/websocket](https://pkg.go.dev/github.com/gorilla/websocket) side by side. +- [net.Conn](https://pkg.go.dev/github.com/coder/websocket#NetConn) wrapper - Zero alloc reads and writes ([gorilla/websocket#535](https://github.com/gorilla/websocket/issues/535)) - Full [context.Context](https://blog.golang.org/context) support - Dial uses [net/http.Client](https://golang.org/pkg/net/http/#Client) @@ -117,24 +124,24 @@ Advantages of nhooyr.io/websocket: - Gorilla writes directly to a net.Conn and so duplicates features of net/http.Client. - Concurrent writes - Close handshake ([gorilla/websocket#448](https://github.com/gorilla/websocket/issues/448)) -- Idiomatic [ping pong](https://pkg.go.dev/nhooyr.io/websocket#Conn.Ping) API +- Idiomatic [ping pong](https://pkg.go.dev/github.com/coder/websocket#Conn.Ping) API - Gorilla requires registering a pong callback before sending a Ping - Can target Wasm ([gorilla/websocket#432](https://github.com/gorilla/websocket/issues/432)) -- Transparent message buffer reuse with [wsjson](https://pkg.go.dev/nhooyr.io/websocket/wsjson) subpackage +- Transparent message buffer reuse with [wsjson](https://pkg.go.dev/github.com/coder/websocket/wsjson) subpackage - [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster WebSocket masking implementation in pure Go - Gorilla's implementation is slower and uses [unsafe](https://golang.org/pkg/unsafe/). Soon we'll have assembly and be 3x faster [#326](https://github.com/nhooyr/websocket/pull/326) - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support - Gorilla only supports no context takeover mode -- [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper for write only connections ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492)) +- [CloseRead](https://pkg.go.dev/github.com/coder/websocket#Conn.CloseRead) helper for write only connections ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492)) #### golang.org/x/net/websocket [golang.org/x/net/websocket](https://pkg.go.dev/golang.org/x/net/websocket) is deprecated. See [golang/go/issues/18152](https://github.com/golang/go/issues/18152). -The [net.Conn](https://pkg.go.dev/nhooyr.io/websocket#NetConn) can help in transitioning -to nhooyr.io/websocket. +The [net.Conn](https://pkg.go.dev/github.com/coder/websocket#NetConn) can help in transitioning +to github.com/coder/websocket. #### gobwas/ws @@ -143,7 +150,7 @@ in an event driven style for performance. See the author's [blog post](https://m However it is quite bloated. See https://pkg.go.dev/github.com/gobwas/ws -When writing idiomatic Go, nhooyr.io/websocket will be faster and easier to use. +When writing idiomatic Go, github.com/coder/websocket will be faster and easier to use. #### lesismal/nbio @@ -152,4 +159,4 @@ event driven for performance reasons. However it is quite bloated. See https://pkg.go.dev/github.com/lesismal/nbio -When writing idiomatic Go, nhooyr.io/websocket will be faster and easier to use. +When writing idiomatic Go, github.com/coder/websocket will be faster and easier to use. diff --git a/accept.go b/accept.go index 285b3103..cc990428 100644 --- a/accept.go +++ b/accept.go @@ -1,10 +1,10 @@ //go:build !js -// +build !js package websocket import ( "bytes" + "context" "crypto/sha1" "encoding/base64" "errors" @@ -14,10 +14,10 @@ import ( "net/http" "net/textproto" "net/url" - "path/filepath" + "path" "strings" - "nhooyr.io/websocket/internal/errd" + "github.com/coder/websocket/internal/errd" ) // AcceptOptions represents Accept's options. @@ -40,9 +40,10 @@ type AcceptOptions struct { // In such a case, example.com is the origin and chat.example.com is the request host. // One would set this field to []string{"example.com"} to authorize example.com to connect. // - // Each pattern is matched case insensitively against the request origin host - // with filepath.Match. - // See https://golang.org/pkg/path/filepath/#Match + // Each pattern is matched case insensitively with path.Match (see + // https://golang.org/pkg/path/#Match). By default, it is matched + // against the request origin host. If the pattern contains a URI + // scheme ("://"), it will be matched against "scheme://host". // // Please ensure you understand the ramifications of enabling this. // If used incorrectly your WebSocket server will be open to CSRF attacks. @@ -62,6 +63,22 @@ type AcceptOptions struct { // Defaults to 512 bytes for CompressionNoContextTakeover and 128 bytes // for CompressionContextTakeover. CompressionThreshold int + + // OnPingReceived is an optional callback invoked synchronously when a ping frame is received. + // + // The payload contains the application data of the ping frame. + // If the callback returns false, the subsequent pong frame will not be sent. + // To avoid blocking, any expensive processing should be performed asynchronously using a goroutine. + OnPingReceived func(ctx context.Context, payload []byte) bool + + // OnPongReceived is an optional callback invoked synchronously when a pong frame is received. + // + // The payload contains the application data of the pong frame. + // To avoid blocking, any expensive processing should be performed asynchronously using a goroutine. + // + // Unlike OnPingReceived, this callback does not return a value because a pong frame + // is a response to a ping and does not trigger any further frame transmission. + OnPongReceived func(ctx context.Context, payload []byte) } func (opts *AcceptOptions) cloneWithDefaults() *AcceptOptions { @@ -79,6 +96,9 @@ func (opts *AcceptOptions) cloneWithDefaults() *AcceptOptions { // See the InsecureSkipVerify and OriginPatterns options to allow cross origin requests. // // Accept will write a response to w on all errors. +// +// Note that using the http.Request Context after Accept returns may lead to +// unexpected behavior (see http.Hijacker). func Accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, error) { return accept(w, r, opts) } @@ -96,7 +116,7 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con if !opts.InsecureSkipVerify { err = authenticateOrigin(r, opts.OriginPatterns) if err != nil { - if errors.Is(err, filepath.ErrBadPattern) { + if errors.Is(err, path.ErrBadPattern) { log.Printf("websocket: %v", err) err = errors.New(http.StatusText(http.StatusForbidden)) } @@ -105,7 +125,7 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con } } - hj, ok := w.(http.Hijacker) + hj, ok := hijacker(w) if !ok { err = errors.New("http.ResponseWriter does not implement http.Hijacker") http.Error(w, http.StatusText(http.StatusNotImplemented), http.StatusNotImplemented) @@ -153,6 +173,8 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con client: false, copts: copts, flateThreshold: opts.CompressionThreshold, + onPingReceived: opts.OnPingReceived, + onPongReceived: opts.OnPongReceived, br: brw.Reader, bw: brw.Writer, @@ -219,9 +241,13 @@ func authenticateOrigin(r *http.Request, originHosts []string) error { } for _, hostPattern := range originHosts { - matched, err := match(hostPattern, u.Host) + target := u.Host + if strings.Contains(hostPattern, "://") { + target = u.Scheme + "://" + u.Host + } + matched, err := match(hostPattern, target) if err != nil { - return fmt.Errorf("failed to parse filepath pattern %q: %w", hostPattern, err) + return fmt.Errorf("failed to parse path pattern %q: %w", hostPattern, err) } if matched { return nil @@ -234,7 +260,7 @@ func authenticateOrigin(r *http.Request, originHosts []string) error { } func match(pattern, s string) (bool, error) { - return filepath.Match(strings.ToLower(pattern), strings.ToLower(s)) + return path.Match(strings.ToLower(pattern), strings.ToLower(s)) } func selectSubprotocol(r *http.Request, subprotocols []string) string { diff --git a/accept_test.go b/accept_test.go index 18233b1e..92dbfcc7 100644 --- a/accept_test.go +++ b/accept_test.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js package websocket @@ -13,8 +12,8 @@ import ( "sync" "testing" - "nhooyr.io/websocket/internal/test/assert" - "nhooyr.io/websocket/internal/test/xrand" + "github.com/coder/websocket/internal/test/assert" + "github.com/coder/websocket/internal/test/xrand" ) func TestAccept(t *testing.T) { @@ -143,6 +142,33 @@ func TestAccept(t *testing.T) { _, err := Accept(w, r, nil) assert.Contains(t, err, `failed to hijack connection`) }) + + t.Run("wrapperHijackerIsUnwrapped", func(t *testing.T) { + t.Parallel() + + rr := httptest.NewRecorder() + w := mockUnwrapper{ + ResponseWriter: rr, + unwrap: func() http.ResponseWriter { + return mockHijacker{ + ResponseWriter: rr, + hijack: func() (conn net.Conn, writer *bufio.ReadWriter, err error) { + return nil, nil, errors.New("haha") + }, + } + }, + } + + r := httptest.NewRequest("GET", "/", nil) + r.Header.Set("Connection", "Upgrade") + r.Header.Set("Upgrade", "websocket") + r.Header.Set("Sec-WebSocket-Version", "13") + r.Header.Set("Sec-WebSocket-Key", xrand.Base64(16)) + + _, err := Accept(w, r, nil) + assert.Contains(t, err, "failed to hijack connection") + }) + t.Run("closeRace", func(t *testing.T) { t.Parallel() @@ -440,6 +466,42 @@ func Test_authenticateOrigin(t *testing.T) { }, success: false, }, + { + name: "originPatternsWithSchemeHttps", + origin: "https://two.example.com", + host: "example.com", + originPatterns: []string{ + "https://*.example.com", + }, + success: true, + }, + { + name: "originPatternsWithSchemeMismatch", + origin: "https://two.example.com", + host: "example.com", + originPatterns: []string{ + "http://*.example.com", + }, + success: false, + }, + { + name: "originPatternsWithSchemeAndPort", + origin: "https://example.com:8443", + host: "example.com", + originPatterns: []string{ + "https://example.com:8443", + }, + success: true, + }, + { + name: "backwardsCompatHostOnlyPattern", + origin: "http://two.example.com", + host: "example.com", + originPatterns: []string{ + "*.example.com", + }, + success: true, + }, } for _, tc := range testCases { @@ -534,3 +596,14 @@ var _ http.Hijacker = mockHijacker{} func (mj mockHijacker) Hijack() (net.Conn, *bufio.ReadWriter, error) { return mj.hijack() } + +type mockUnwrapper struct { + http.ResponseWriter + unwrap func() http.ResponseWriter +} + +var _ rwUnwrapper = mockUnwrapper{} + +func (mu mockUnwrapper) Unwrap() http.ResponseWriter { + return mu.unwrap() +} diff --git a/autobahn_test.go b/autobahn_test.go index 57ceebd5..20b89609 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js package websocket_test @@ -17,11 +16,11 @@ import ( "testing" "time" - "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/errd" - "nhooyr.io/websocket/internal/test/assert" - "nhooyr.io/websocket/internal/test/wstest" - "nhooyr.io/websocket/internal/util" + "github.com/coder/websocket" + "github.com/coder/websocket/internal/errd" + "github.com/coder/websocket/internal/test/assert" + "github.com/coder/websocket/internal/test/wstest" + "github.com/coder/websocket/internal/util" ) var excludedAutobahnCases = []string{ @@ -92,7 +91,7 @@ func TestAutobahn(t *testing.T) { } }) - c, _, err := websocket.Dial(ctx, fmt.Sprintf(wstestURL+"/updateReports?agent=main"), nil) + c, _, err := websocket.Dial(ctx, wstestURL+"/updateReports?agent=main", nil) assert.Success(t, err) c.Close(websocket.StatusNormalClosure, "") @@ -130,7 +129,7 @@ func wstestServer(tb testing.TB, ctx context.Context) (url string, closeFn func( url = "ws://" + serverAddr const outDir = "ci/out/autobahn-report" - specFile, err := tempJSONFile(map[string]interface{}{ + specFile, err := tempJSONFile(map[string]any{ "url": url, "outdir": outDir, "cases": autobahnCases, @@ -280,7 +279,7 @@ func unusedListenAddr() (_ string, err error) { return l.Addr().String(), nil } -func tempJSONFile(v interface{}) (string, error) { +func tempJSONFile(v any) (string, error) { f, err := os.CreateTemp("", "temp.json") if err != nil { return "", fmt.Errorf("temp file: %w", err) diff --git a/ci/fmt.sh b/ci/fmt.sh index 31d0c15d..588510ba 100755 --- a/ci/fmt.sh +++ b/ci/fmt.sh @@ -2,22 +2,23 @@ set -eu cd -- "$(dirname "$0")/.." +X_TOOLS_VERSION=v0.31.0 + go mod tidy (cd ./internal/thirdparty && go mod tidy) (cd ./internal/examples && go mod tidy) gofmt -w -s . -go run golang.org/x/tools/cmd/goimports@latest -w "-local=$(go list -m)" . +go run golang.org/x/tools/cmd/goimports@${X_TOOLS_VERSION} -w "-local=$(go list -m)" . -npx prettier@3.0.3 \ - --write \ +git ls-files "*.yml" "*.md" "*.js" "*.css" "*.html" | xargs npx prettier@3.3.3 \ + --check \ --log-level=warn \ --print-width=90 \ --no-semi \ --single-quote \ - --arrow-parens=avoid \ - $(git ls-files "*.yml" "*.md" "*.js" "*.css" "*.html") + --arrow-parens=avoid -go run golang.org/x/tools/cmd/stringer@latest -type=opcode,MessageType,StatusCode -output=stringer.go +go run golang.org/x/tools/cmd/stringer@${X_TOOLS_VERSION} -type=opcode,MessageType,StatusCode -output=stringer.go if [ "${CI-}" ]; then git diff --exit-code diff --git a/ci/lint.sh b/ci/lint.sh index 3cf8eee4..316b035d 100755 --- a/ci/lint.sh +++ b/ci/lint.sh @@ -2,10 +2,13 @@ set -eu cd -- "$(dirname "$0")/.." +STATICCHECK_VERSION=v0.6.1 +GOVULNCHECK_VERSION=v1.1.4 + go vet ./... GOOS=js GOARCH=wasm go vet ./... -go install honnef.co/go/tools/cmd/staticcheck@latest +go install honnef.co/go/tools/cmd/staticcheck@${STATICCHECK_VERSION} staticcheck ./... GOOS=js GOARCH=wasm staticcheck ./... @@ -15,7 +18,7 @@ govulncheck() { cat "$tmpf" fi } -go install golang.org/x/vuln/cmd/govulncheck@latest +go install golang.org/x/vuln/cmd/govulncheck@${GOVULNCHECK_VERSION} govulncheck ./... GOOS=js GOARCH=wasm govulncheck ./... diff --git a/ci/test.sh b/ci/test.sh index a3007614..cc3c22d7 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -24,7 +24,7 @@ cd -- "$(dirname "$0")/.." ) -go install github.com/agnivade/wasmbrowsertest@latest +go install github.com/agnivade/wasmbrowsertest@8be019f6c6dceae821467b4c589eb195c2b761ce go test --race --bench=. --timeout=1h --covermode=atomic --coverprofile=ci/out/coverage.prof --coverpkg=./... "$@" ./... sed -i.bak '/stringer\.go/d' ci/out/coverage.prof sed -i.bak '/nhooyr.io\/websocket\/internal\/test/d' ci/out/coverage.prof diff --git a/close.go b/close.go index 31504b0e..fcc68065 100644 --- a/close.go +++ b/close.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js package websocket @@ -11,7 +10,7 @@ import ( "net" "time" - "nhooyr.io/websocket/internal/errd" + "github.com/coder/websocket/internal/errd" ) // StatusCode represents a WebSocket status code. @@ -100,7 +99,7 @@ func CloseStatus(err error) StatusCode { func (c *Conn) Close(code StatusCode, reason string) (err error) { defer errd.Wrap(&err, "failed to close WebSocket") - if !c.casClosing() { + if c.casClosing() { err = c.waitGoroutines() if err != nil { return err @@ -133,7 +132,7 @@ func (c *Conn) Close(code StatusCode, reason string) (err error) { func (c *Conn) CloseNow() (err error) { defer errd.Wrap(&err, "failed to immediately close WebSocket") - if !c.casClosing() { + if c.casClosing() { err = c.waitGoroutines() if err != nil { return err @@ -232,12 +231,6 @@ func (c *Conn) waitGoroutines() error { t := time.NewTimer(time.Second * 15) defer t.Stop() - select { - case <-c.timeoutLoopDone: - case <-t.C: - return errors.New("failed to wait for timeoutLoop goroutine to exit") - } - c.closeReadMu.Lock() closeRead := c.closeReadCtx != nil c.closeReadMu.Unlock() @@ -329,13 +322,7 @@ func (ce CloseError) bytesErr() ([]byte, error) { } func (c *Conn) casClosing() bool { - c.closeMu.Lock() - defer c.closeMu.Unlock() - if !c.closing { - c.closing = true - return true - } - return false + return c.closing.Swap(true) } func (c *Conn) isClosed() bool { diff --git a/close_test.go b/close_test.go index 6bf3c256..1e04807e 100644 --- a/close_test.go +++ b/close_test.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js package websocket @@ -9,7 +8,7 @@ import ( "strings" "testing" - "nhooyr.io/websocket/internal/test/assert" + "github.com/coder/websocket/internal/test/assert" ) func TestCloseError(t *testing.T) { diff --git a/compress.go b/compress.go index 1f3adcfb..41bd5bdb 100644 --- a/compress.go +++ b/compress.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js package websocket @@ -168,8 +167,10 @@ type slidingWindow struct { buf []byte } -var swPoolMu sync.RWMutex -var swPool = map[int]*sync.Pool{} +var ( + swPoolMu sync.RWMutex + swPool = map[int]*sync.Pool{} +) func slidingWindowPool(n int) *sync.Pool { swPoolMu.RLock() diff --git a/compress_test.go b/compress_test.go index 667e1408..1964c84f 100644 --- a/compress_test.go +++ b/compress_test.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js package websocket @@ -10,8 +9,8 @@ import ( "strings" "testing" - "nhooyr.io/websocket/internal/test/assert" - "nhooyr.io/websocket/internal/test/xrand" + "github.com/coder/websocket/internal/test/assert" + "github.com/coder/websocket/internal/test/xrand" ) func Test_slidingWindow(t *testing.T) { @@ -19,7 +18,7 @@ func Test_slidingWindow(t *testing.T) { const testCount = 99 const maxWindow = 99999 - for i := 0; i < testCount; i++ { + for range testCount { t.Run("", func(t *testing.T) { t.Parallel() diff --git a/conn.go b/conn.go index 8690fb3b..09234871 100644 --- a/conn.go +++ b/conn.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js package websocket @@ -52,9 +51,8 @@ type Conn struct { br *bufio.Reader bw *bufio.Writer - readTimeout chan context.Context - writeTimeout chan context.Context - timeoutLoopDone chan struct{} + readTimeoutStop atomic.Pointer[func() bool] + writeTimeoutStop atomic.Pointer[func() bool] // Read state. readMu *mu @@ -69,17 +67,25 @@ type Conn struct { writeHeaderBuf [8]byte writeHeader header + // Close handshake state. + closeStateMu sync.RWMutex + closeReceivedErr error + closeSentErr error + + // CloseRead state. closeReadMu sync.Mutex closeReadCtx context.Context closeReadDone chan struct{} + closing atomic.Bool + closeMu sync.Mutex // Protects following. closed chan struct{} - closeMu sync.Mutex - closing bool - pingCounter int32 - activePingsMu sync.Mutex - activePings map[string]chan<- struct{} + pingCounter atomic.Int64 + activePingsMu sync.Mutex + activePings map[string]chan<- struct{} + onPingReceived func(context.Context, []byte) bool + onPongReceived func(context.Context, []byte) } type connConfig struct { @@ -88,6 +94,8 @@ type connConfig struct { client bool copts *compressionOptions flateThreshold int + onPingReceived func(context.Context, []byte) bool + onPongReceived func(context.Context, []byte) br *bufio.Reader bw *bufio.Writer @@ -104,12 +112,10 @@ func newConn(cfg connConfig) *Conn { br: cfg.br, bw: cfg.bw, - readTimeout: make(chan context.Context), - writeTimeout: make(chan context.Context), - timeoutLoopDone: make(chan struct{}), - - closed: make(chan struct{}), - activePings: make(map[string]chan<- struct{}), + closed: make(chan struct{}), + activePings: make(map[string]chan<- struct{}), + onPingReceived: cfg.onPingReceived, + onPongReceived: cfg.onPongReceived, } c.readMu = newMu(c) @@ -133,8 +139,6 @@ func newConn(cfg connConfig) *Conn { c.close() }) - go c.timeoutLoop() - return c } @@ -164,27 +168,34 @@ func (c *Conn) close() error { return err } -func (c *Conn) timeoutLoop() { - defer close(c.timeoutLoopDone) +func (c *Conn) setupWriteTimeout(ctx context.Context) { + stop := context.AfterFunc(ctx, func() { + c.clearWriteTimeout() + c.close() + }) + swapTimeoutStop(&c.writeTimeoutStop, &stop) +} - readCtx := context.Background() - writeCtx := context.Background() +func (c *Conn) clearWriteTimeout() { + swapTimeoutStop(&c.writeTimeoutStop, nil) +} - for { - select { - case <-c.closed: - return - - case writeCtx = <-c.writeTimeout: - case readCtx = <-c.readTimeout: - - case <-readCtx.Done(): - c.close() - return - case <-writeCtx.Done(): - c.close() - return - } +func (c *Conn) setupReadTimeout(ctx context.Context) { + stop := context.AfterFunc(ctx, func() { + c.clearReadTimeout() + c.close() + }) + swapTimeoutStop(&c.readTimeoutStop, &stop) +} + +func (c *Conn) clearReadTimeout() { + swapTimeoutStop(&c.readTimeoutStop, nil) +} + +func swapTimeoutStop(p *atomic.Pointer[func() bool], newStop *func() bool) { + oldStop := p.Swap(newStop) + if oldStop != nil { + (*oldStop)() } } @@ -200,9 +211,9 @@ func (c *Conn) flate() bool { // // TCP Keepalives should suffice for most use cases. func (c *Conn) Ping(ctx context.Context) error { - p := atomic.AddInt32(&c.pingCounter, 1) + p := c.pingCounter.Add(1) - err := c.ping(ctx, strconv.Itoa(int(p))) + err := c.ping(ctx, strconv.FormatInt(p, 10)) if err != nil { return fmt.Errorf("failed to ping: %w", err) } diff --git a/conn_test.go b/conn_test.go index 2b44ad22..c3ccc886 100644 --- a/conn_test.go +++ b/conn_test.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "io" + "net" "net/http" "net/http/httptest" "os" @@ -16,13 +17,13 @@ import ( "testing" "time" - "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/errd" - "nhooyr.io/websocket/internal/test/assert" - "nhooyr.io/websocket/internal/test/wstest" - "nhooyr.io/websocket/internal/test/xrand" - "nhooyr.io/websocket/internal/xsync" - "nhooyr.io/websocket/wsjson" + "github.com/coder/websocket" + "github.com/coder/websocket/internal/errd" + "github.com/coder/websocket/internal/test/assert" + "github.com/coder/websocket/internal/test/wstest" + "github.com/coder/websocket/internal/test/xrand" + "github.com/coder/websocket/internal/xsync" + "github.com/coder/websocket/wsjson" ) func TestConn(t *testing.T) { @@ -35,7 +36,7 @@ func TestConn(t *testing.T) { return websocket.CompressionMode(xrand.Int(int(websocket.CompressionContextTakeover) + 1)) } - for i := 0; i < 5; i++ { + for range 5 { t.Run("", func(t *testing.T) { tt, c1, c2 := newConnTest(t, &websocket.DialOptions{ CompressionMode: compressionMode(), @@ -49,7 +50,7 @@ func TestConn(t *testing.T) { c1.SetReadLimit(131072) - for i := 0; i < 5; i++ { + for range 5 { err := wstest.Echo(tt.ctx, c1, 131072) assert.Success(t, err) } @@ -75,7 +76,7 @@ func TestConn(t *testing.T) { c1.CloseRead(tt.ctx) c2.CloseRead(tt.ctx) - for i := 0; i < 10; i++ { + for range 10 { err := c1.Ping(tt.ctx) assert.Success(t, err) } @@ -96,6 +97,85 @@ func TestConn(t *testing.T) { assert.Contains(t, err, "failed to wait for pong") }) + t.Run("pingReceivedPongReceived", func(t *testing.T) { + var pingReceived1, pongReceived1 bool + var pingReceived2, pongReceived2 bool + tt, c1, c2 := newConnTest(t, + &websocket.DialOptions{ + OnPingReceived: func(ctx context.Context, payload []byte) bool { + pingReceived1 = true + return true + }, + OnPongReceived: func(ctx context.Context, payload []byte) { + pongReceived1 = true + }, + }, &websocket.AcceptOptions{ + OnPingReceived: func(ctx context.Context, payload []byte) bool { + pingReceived2 = true + return true + }, + OnPongReceived: func(ctx context.Context, payload []byte) { + pongReceived2 = true + }, + }, + ) + + c1.CloseRead(tt.ctx) + c2.CloseRead(tt.ctx) + + ctx, cancel := context.WithTimeout(tt.ctx, time.Millisecond*100) + defer cancel() + + err := c1.Ping(ctx) + assert.Success(t, err) + + c1.CloseNow() + c2.CloseNow() + + assert.Equal(t, "only one side receives the ping", false, pingReceived1 && pingReceived2) + assert.Equal(t, "only one side receives the pong", false, pongReceived1 && pongReceived2) + assert.Equal(t, "ping and pong received", true, (pingReceived1 && pongReceived2) || (pingReceived2 && pongReceived1)) + }) + + t.Run("pingReceivedPongNotReceived", func(t *testing.T) { + var pingReceived1, pongReceived1 bool + var pingReceived2, pongReceived2 bool + tt, c1, c2 := newConnTest(t, + &websocket.DialOptions{ + OnPingReceived: func(ctx context.Context, payload []byte) bool { + pingReceived1 = true + return false + }, + OnPongReceived: func(ctx context.Context, payload []byte) { + pongReceived1 = true + }, + }, &websocket.AcceptOptions{ + OnPingReceived: func(ctx context.Context, payload []byte) bool { + pingReceived2 = true + return false + }, + OnPongReceived: func(ctx context.Context, payload []byte) { + pongReceived2 = true + }, + }, + ) + + c1.CloseRead(tt.ctx) + c2.CloseRead(tt.ctx) + + ctx, cancel := context.WithTimeout(tt.ctx, time.Millisecond*100) + defer cancel() + + err := c1.Ping(ctx) + assert.Contains(t, err, "failed to wait for pong") + + c1.CloseNow() + c2.CloseNow() + + assert.Equal(t, "only one side receives the ping", false, pingReceived1 && pingReceived2) + assert.Equal(t, "ping received and pong not received", true, (pingReceived1 && !pongReceived2) || (pingReceived2 && !pongReceived1)) + }) + t.Run("concurrentWrite", func(t *testing.T) { tt, c1, c2 := newConnTest(t, nil, nil) @@ -105,7 +185,7 @@ func TestConn(t *testing.T) { const count = 100 errs := make(chan error, count) - for i := 0; i < count; i++ { + for range count { go func() { select { case errs <- c1.Write(tt.ctx, websocket.MessageBinary, msg): @@ -115,7 +195,7 @@ func TestConn(t *testing.T) { }() } - for i := 0; i < count; i++ { + for range count { select { case err := <-errs: assert.Success(t, err) @@ -261,7 +341,7 @@ func TestConn(t *testing.T) { return wsjson.Write(tt.ctx, c1, exp) }) - var act interface{} + var act any err := wsjson.Read(tt.ctx, c1, &act) assert.Success(t, err) assert.Equal(t, "read msg", exp, act) @@ -292,7 +372,7 @@ func TestConn(t *testing.T) { return wsjson.Write(tt.ctx, c1, exp) }) - var act interface{} + var act any err := wsjson.Read(tt.ctx, c1, &act) assert.Success(t, err) assert.Equal(t, "read msg", exp, act) @@ -328,7 +408,7 @@ func TestConn(t *testing.T) { c1.SetReadLimit(131072) - for i := 0; i < 5; i++ { + for range 5 { err := wstest.Echo(tt.ctx, c1, 131072) assert.Success(t, err) } @@ -341,6 +421,25 @@ func TestConn(t *testing.T) { err = c1.Close(websocket.StatusNormalClosure, "") assert.Success(t, err) }) + + t.Run("ReadLimitExceededReturnsErrMessageTooBig", func(t *testing.T) { + tt, c1, c2 := newConnTest(t, nil, nil) + + c1.SetReadLimit(1024) + _ = c2.CloseRead(tt.ctx) + + writeDone := xsync.Go(func() error { + payload := strings.Repeat("x", 4096) + return c2.Write(tt.ctx, websocket.MessageText, []byte(payload)) + }) + + _, _, err := c1.Read(tt.ctx) + assert.ErrorIs(t, websocket.ErrMessageTooBig, err) + assert.Contains(t, err, "read limited at 1025 bytes") + + _ = c2.CloseNow() + <-writeDone + }) } func TestWasm(t *testing.T) { @@ -364,7 +463,7 @@ func TestWasm(t *testing.T) { defer cancel() cmd := exec.CommandContext(ctx, "go", "test", "-exec=wasmbrowsertest", ".", "-v") - cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm", fmt.Sprintf("WS_ECHO_SERVER_URL=%v", s.URL)) + cmd.Env = append(cleanEnv(os.Environ()), "GOOS=js", "GOARCH=wasm", fmt.Sprintf("WS_ECHO_SERVER_URL=%v", s.URL)) b, err := cmd.CombinedOutput() if err != nil { @@ -372,6 +471,18 @@ func TestWasm(t *testing.T) { } } +func cleanEnv(env []string) (out []string) { + for _, e := range env { + // Filter out GITHUB envs and anything with token in it, + // especially GITHUB_TOKEN in CI as it breaks TestWasm. + if strings.HasPrefix(e, "GITHUB") || strings.Contains(e, "TOKEN") { + continue + } + out = append(out, e) + } + return out +} + func assertCloseStatus(exp websocket.StatusCode, err error) error { if websocket.CloseStatus(err) == -1 { return fmt.Errorf("expected websocket.CloseError: %T %v", err, err) @@ -448,7 +559,7 @@ func (tt *connTest) goDiscardLoop(c *websocket.Conn) { } func BenchmarkConn(b *testing.B) { - var benchCases = []struct { + benchCases := []struct { name string mode websocket.CompressionMode }{ @@ -568,7 +679,7 @@ func assertEcho(tb testing.TB, ctx context.Context, c *websocket.Conn) { return wsjson.Write(ctx, c, exp) }) - var act interface{} + var act any c.SetReadLimit(1 << 30) err := wsjson.Read(ctx, c, &act) assert.Success(tb, err) @@ -590,7 +701,7 @@ func assertClose(tb testing.TB, c *websocket.Conn) { func TestConcurrentClosePing(t *testing.T) { t.Parallel() - for i := 0; i < 64; i++ { + for range 64 { func() { c1, c2 := wstest.Pipe(nil, nil) defer c1.CloseNow() @@ -613,3 +724,149 @@ func TestConcurrentClosePing(t *testing.T) { }() } } + +func TestConnClosePropagation(t *testing.T) { + t.Parallel() + + want := []byte("hello") + keepWriting := func(c *websocket.Conn) <-chan error { + return xsync.Go(func() error { + for { + err := c.Write(context.Background(), websocket.MessageText, want) + if err != nil { + return err + } + } + }) + } + keepReading := func(c *websocket.Conn) <-chan error { + return xsync.Go(func() error { + for { + _, got, err := c.Read(context.Background()) + if err != nil { + return err + } + if !bytes.Equal(want, got) { + return fmt.Errorf("unexpected message: want %q, got %q", want, got) + } + } + }) + } + checkReadErr := func(t *testing.T, err error) { + // Check read error (output depends on when read is called in relation to connection closure). + var ce websocket.CloseError + if errors.As(err, &ce) { + assert.Equal(t, "", websocket.StatusNormalClosure, ce.Code) + } else { + assert.ErrorIs(t, net.ErrClosed, err) + } + } + checkConnErrs := func(t *testing.T, conn ...*websocket.Conn) { + for _, c := range conn { + // Check write error. + err := c.Write(context.Background(), websocket.MessageText, want) + assert.ErrorIs(t, net.ErrClosed, err) + + _, _, err = c.Read(context.Background()) + checkReadErr(t, err) + } + } + + t.Run("CloseOtherSideDuringWrite", func(t *testing.T) { + tt, this, other := newConnTest(t, nil, nil) + + _ = this.CloseRead(tt.ctx) + thisWriteErr := keepWriting(this) + + _, got, err := other.Read(tt.ctx) + assert.Success(t, err) + assert.Equal(t, "msg", want, got) + + err = other.Close(websocket.StatusNormalClosure, "") + assert.Success(t, err) + + select { + case err := <-thisWriteErr: + assert.ErrorIs(t, net.ErrClosed, err) + case <-tt.ctx.Done(): + t.Fatal(tt.ctx.Err()) + } + + checkConnErrs(t, this, other) + }) + t.Run("CloseThisSideDuringWrite", func(t *testing.T) { + tt, this, other := newConnTest(t, nil, nil) + + _ = this.CloseRead(tt.ctx) + thisWriteErr := keepWriting(this) + otherReadErr := keepReading(other) + + err := this.Close(websocket.StatusNormalClosure, "") + assert.Success(t, err) + + select { + case err := <-thisWriteErr: + assert.ErrorIs(t, net.ErrClosed, err) + case <-tt.ctx.Done(): + t.Fatal(tt.ctx.Err()) + } + + select { + case err := <-otherReadErr: + checkReadErr(t, err) + case <-tt.ctx.Done(): + t.Fatal(tt.ctx.Err()) + } + + checkConnErrs(t, this, other) + }) + t.Run("CloseOtherSideDuringRead", func(t *testing.T) { + tt, this, other := newConnTest(t, nil, nil) + + _ = other.CloseRead(tt.ctx) + errs := keepReading(this) + + err := other.Write(tt.ctx, websocket.MessageText, want) + assert.Success(t, err) + + err = other.Close(websocket.StatusNormalClosure, "") + assert.Success(t, err) + + select { + case err := <-errs: + checkReadErr(t, err) + case <-tt.ctx.Done(): + t.Fatal(tt.ctx.Err()) + } + + checkConnErrs(t, this, other) + }) + t.Run("CloseThisSideDuringRead", func(t *testing.T) { + tt, this, other := newConnTest(t, nil, nil) + + thisReadErr := keepReading(this) + otherReadErr := keepReading(other) + + err := other.Write(tt.ctx, websocket.MessageText, want) + assert.Success(t, err) + + err = this.Close(websocket.StatusNormalClosure, "") + assert.Success(t, err) + + select { + case err := <-thisReadErr: + checkReadErr(t, err) + case <-tt.ctx.Done(): + t.Fatal(tt.ctx.Err()) + } + + select { + case err := <-otherReadErr: + checkReadErr(t, err) + case <-tt.ctx.Done(): + t.Fatal(tt.ctx.Err()) + } + + checkConnErrs(t, this, other) + }) +} diff --git a/dial.go b/dial.go index e4c4daa1..f5e4544b 100644 --- a/dial.go +++ b/dial.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js package websocket @@ -17,7 +16,7 @@ import ( "sync" "time" - "nhooyr.io/websocket/internal/errd" + "github.com/coder/websocket/internal/errd" ) // DialOptions represents Dial's options. @@ -48,6 +47,22 @@ type DialOptions struct { // Defaults to 512 bytes for CompressionNoContextTakeover and 128 bytes // for CompressionContextTakeover. CompressionThreshold int + + // OnPingReceived is an optional callback invoked synchronously when a ping frame is received. + // + // The payload contains the application data of the ping frame. + // If the callback returns false, the subsequent pong frame will not be sent. + // To avoid blocking, any expensive processing should be performed asynchronously using a goroutine. + OnPingReceived func(ctx context.Context, payload []byte) bool + + // OnPongReceived is an optional callback invoked synchronously when a pong frame is received. + // + // The payload contains the application data of the pong frame. + // To avoid blocking, any expensive processing should be performed asynchronously using a goroutine. + // + // Unlike OnPingReceived, this callback does not return a value because a pong frame + // is a response to a ping and does not trigger any further frame transmission. + OnPongReceived func(ctx context.Context, payload []byte) } func (opts *DialOptions) cloneWithDefaults(ctx context.Context) (context.Context, context.CancelFunc, *DialOptions) { @@ -163,6 +178,8 @@ func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) ( client: true, copts: copts, flateThreshold: opts.CompressionThreshold, + onPingReceived: opts.OnPingReceived, + onPongReceived: opts.OnPongReceived, br: getBufioReader(rwc), bw: getBufioWriter(rwc), }), resp, nil diff --git a/dial_test.go b/dial_test.go index 237a2874..492ac6b3 100644 --- a/dial_test.go +++ b/dial_test.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js package websocket_test @@ -8,6 +7,7 @@ import ( "context" "crypto/rand" "io" + "maps" "net/http" "net/http/httptest" "net/url" @@ -15,10 +15,10 @@ import ( "testing" "time" - "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/test/assert" - "nhooyr.io/websocket/internal/util" - "nhooyr.io/websocket/internal/xsync" + "github.com/coder/websocket" + "github.com/coder/websocket/internal/test/assert" + "github.com/coder/websocket/internal/util" + "github.com/coder/websocket/internal/xsync" ) func TestBadDials(t *testing.T) { @@ -172,7 +172,6 @@ func Test_verifyHostOverride(t *testing.T) { c.CloseNow() }) } - } type mockBody struct { @@ -357,11 +356,10 @@ func (fc *forwardProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { } defer resp.Body.Close() - for k, v := range resp.Header { - w.Header()[k] = v - } + maps.Copy(w.Header(), resp.Header) w.Header().Set("PROXIED", "true") w.WriteHeader(resp.StatusCode) + if resprw, ok := resp.Body.(io.ReadWriter); ok { c, brw, err := w.(http.Hijacker).Hijack() if err != nil { diff --git a/doc.go b/doc.go index 2ab648a6..0c7f8316 100644 --- a/doc.go +++ b/doc.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js // Package websocket implements the RFC 6455 WebSocket protocol. // @@ -15,7 +14,7 @@ // // The wsjson subpackage contain helpers for JSON and protobuf messages. // -// More documentation at https://nhooyr.io/websocket. +// More documentation at https://github.com/coder/websocket. // // # Wasm // @@ -31,4 +30,4 @@ // - Conn.CloseNow is Close(StatusGoingAway, "") // - HTTPClient, HTTPHeader and CompressionMode in DialOptions are no-op // - *http.Response from Dial is &http.Response{} with a 101 status code on success -package websocket // import "nhooyr.io/websocket" +package websocket // import "github.com/coder/websocket" diff --git a/errors.go b/errors.go new file mode 100644 index 00000000..bf4fc2b0 --- /dev/null +++ b/errors.go @@ -0,0 +1,8 @@ +package websocket + +import ( + "errors" +) + +// ErrMessageTooBig is returned when a message exceeds the read limit. +var ErrMessageTooBig = errors.New("websocket: message too big") diff --git a/example_test.go b/example_test.go index 590c0411..7026e311 100644 --- a/example_test.go +++ b/example_test.go @@ -6,8 +6,8 @@ import ( "net/http" "time" - "nhooyr.io/websocket" - "nhooyr.io/websocket/wsjson" + "github.com/coder/websocket" + "github.com/coder/websocket/wsjson" ) func ExampleAccept() { @@ -25,7 +25,7 @@ func ExampleAccept() { ctx, cancel := context.WithTimeout(r.Context(), time.Second*10) defer cancel() - var v interface{} + var v any err = wsjson.Read(ctx, c, &v) if err != nil { log.Println(err) @@ -150,7 +150,7 @@ func ExampleConn_Ping() { // Required to read the Pongs from the server. ctx = c.CloseRead(ctx) - for i := 0; i < 5; i++ { + for range 5 { err = c.Ping(ctx) if err != nil { log.Fatal(err) diff --git a/export_test.go b/export_test.go index a644d8f0..e0071922 100644 --- a/export_test.go +++ b/export_test.go @@ -1,12 +1,11 @@ //go:build !js -// +build !js package websocket import ( "net" - "nhooyr.io/websocket/internal/util" + "github.com/coder/websocket/internal/util" ) func (c *Conn) RecordBytesWritten() *int { @@ -30,9 +29,11 @@ func (c *Conn) RecordBytesRead() *int { var ErrClosed = net.ErrClosed -var ExportedDial = dial -var SecWebSocketAccept = secWebSocketAccept -var SecWebSocketKey = secWebSocketKey -var VerifyServerResponse = verifyServerResponse +var ( + ExportedDial = dial + SecWebSocketAccept = secWebSocketAccept + SecWebSocketKey = secWebSocketKey + VerifyServerResponse = verifyServerResponse +) var CompressionModeOpts = CompressionMode.opts diff --git a/frame.go b/frame.go index d5631863..e7ab76be 100644 --- a/frame.go +++ b/frame.go @@ -9,7 +9,7 @@ import ( "io" "math" - "nhooyr.io/websocket/internal/errd" + "github.com/coder/websocket/internal/errd" ) // opcode represents a WebSocket opcode. diff --git a/frame_test.go b/frame_test.go index bd626358..6b2e21f5 100644 --- a/frame_test.go +++ b/frame_test.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js package websocket @@ -13,7 +12,7 @@ import ( "testing" "time" - "nhooyr.io/websocket/internal/test/assert" + "github.com/coder/websocket/internal/test/assert" ) func TestHeader(t *testing.T) { @@ -54,7 +53,7 @@ func TestHeader(t *testing.T) { return r.Intn(2) == 0 } - for i := 0; i < 10000; i++ { + for range 10000 { h := header{ fin: randBool(), rsv1: randBool(), diff --git a/go.mod b/go.mod index 715a9f7a..d32fbd77 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module nhooyr.io/websocket +module github.com/coder/websocket -go 1.19 +go 1.23 diff --git a/hijack.go b/hijack.go new file mode 100644 index 00000000..9cce45ca --- /dev/null +++ b/hijack.go @@ -0,0 +1,33 @@ +//go:build !js + +package websocket + +import ( + "net/http" +) + +type rwUnwrapper interface { + Unwrap() http.ResponseWriter +} + +// hijacker returns the Hijacker interface of the http.ResponseWriter. +// It follows the Unwrap method of the http.ResponseWriter if available, +// matching the behavior of http.ResponseController. If the Hijacker +// interface is not found, it returns false. +// +// Since the http.ResponseController is not available in Go 1.19, and +// does not support checking the presence of the Hijacker interface, +// this function is used to provide a consistent way to check for the +// Hijacker interface across Go versions. +func hijacker(rw http.ResponseWriter) (http.Hijacker, bool) { + for { + switch t := rw.(type) { + case http.Hijacker: + return t, true + case rwUnwrapper: + rw = t.Unwrap() + default: + return nil, false + } + } +} diff --git a/hijack_go120_test.go b/hijack_go120_test.go new file mode 100644 index 00000000..0f0673a9 --- /dev/null +++ b/hijack_go120_test.go @@ -0,0 +1,38 @@ +//go:build !js && go1.20 + +package websocket + +import ( + "bufio" + "errors" + "net" + "net/http" + "net/http/httptest" + "testing" + + "github.com/coder/websocket/internal/test/assert" +) + +func Test_hijackerHTTPResponseControllerCompatibility(t *testing.T) { + t.Parallel() + + rr := httptest.NewRecorder() + w := mockUnwrapper{ + ResponseWriter: rr, + unwrap: func() http.ResponseWriter { + return mockHijacker{ + ResponseWriter: rr, + hijack: func() (conn net.Conn, writer *bufio.ReadWriter, err error) { + return nil, nil, errors.New("haha") + }, + } + }, + } + + _, _, err := http.NewResponseController(w).Hijack() + assert.Contains(t, err, "haha") + hj, ok := hijacker(w) + assert.Equal(t, "hijacker found", ok, true) + _, _, err = hj.Hijack() + assert.Contains(t, err, "haha") +} diff --git a/internal/bpool/bpool.go b/internal/bpool/bpool.go index aa826fba..12cf577a 100644 --- a/internal/bpool/bpool.go +++ b/internal/bpool/bpool.go @@ -5,15 +5,16 @@ import ( "sync" ) -var bpool sync.Pool +var bpool = sync.Pool{ + New: func() any { + return &bytes.Buffer{} + }, +} // Get returns a buffer from the pool or creates a new one if // the pool is empty. func Get() *bytes.Buffer { b := bpool.Get() - if b == nil { - return &bytes.Buffer{} - } return b.(*bytes.Buffer) } diff --git a/internal/errd/wrap.go b/internal/errd/wrap.go index 6e779131..c80d0a65 100644 --- a/internal/errd/wrap.go +++ b/internal/errd/wrap.go @@ -7,7 +7,7 @@ import ( // Wrap wraps err with fmt.Errorf if err is non nil. // Intended for use with defer and a named error return. // Inspired by https://github.com/golang/go/issues/32676. -func Wrap(err *error, f string, v ...interface{}) { +func Wrap(err *error, f string, v ...any) { if *err != nil { *err = fmt.Errorf(f+": %w", append(v, *err)...) } diff --git a/internal/examples/chat/README.md b/internal/examples/chat/README.md index 574c6994..4d354586 100644 --- a/internal/examples/chat/README.md +++ b/internal/examples/chat/README.md @@ -1,6 +1,6 @@ # Chat Example -This directory contains a full stack example of a simple chat webapp using nhooyr.io/websocket. +This directory contains a full stack example of a simple chat webapp using github.com/coder/websocket. ```bash $ cd examples/chat diff --git a/internal/examples/chat/chat.go b/internal/examples/chat/chat.go index 8b1e30c1..cc24ac7c 100644 --- a/internal/examples/chat/chat.go +++ b/internal/examples/chat/chat.go @@ -12,7 +12,7 @@ import ( "golang.org/x/time/rate" - "nhooyr.io/websocket" + "github.com/coder/websocket" ) // chatServer enables broadcasting to a set of subscribers. @@ -31,7 +31,7 @@ type chatServer struct { // logf controls where logs are sent. // Defaults to log.Printf. - logf func(f string, v ...interface{}) + logf func(f string, v ...any) // serveMux routes the various endpoints to the appropriate handler. serveMux http.ServeMux @@ -70,7 +70,7 @@ func (cs *chatServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { // subscribeHandler accepts the WebSocket connection and then subscribes // it to all future messages. func (cs *chatServer) subscribeHandler(w http.ResponseWriter, r *http.Request) { - err := cs.subscribe(r.Context(), w, r) + err := cs.subscribe(w, r) if errors.Is(err, context.Canceled) { return } @@ -111,7 +111,7 @@ func (cs *chatServer) publishHandler(w http.ResponseWriter, r *http.Request) { // // It uses CloseRead to keep reading from the connection to process control // messages and cancel the context if the connection drops. -func (cs *chatServer) subscribe(ctx context.Context, w http.ResponseWriter, r *http.Request) error { +func (cs *chatServer) subscribe(w http.ResponseWriter, r *http.Request) error { var mu sync.Mutex var c *websocket.Conn var closed bool @@ -142,7 +142,7 @@ func (cs *chatServer) subscribe(ctx context.Context, w http.ResponseWriter, r *h mu.Unlock() defer c.CloseNow() - ctx = c.CloseRead(ctx) + ctx := c.CloseRead(context.Background()) for { select { diff --git a/internal/examples/chat/chat_test.go b/internal/examples/chat/chat_test.go index f80f1de1..dcada0b2 100644 --- a/internal/examples/chat/chat_test.go +++ b/internal/examples/chat/chat_test.go @@ -14,7 +14,7 @@ import ( "golang.org/x/time/rate" - "nhooyr.io/websocket" + "github.com/coder/websocket" ) func Test_chatServer(t *testing.T) { @@ -52,7 +52,7 @@ func Test_chatServer(t *testing.T) { // 10 clients are started that send 128 different // messages of max 128 bytes concurrently. // - // The test verifies that every message is seen by ever client + // The test verifies that every message is seen by every client // and no errors occur anywhere. t.Run("concurrency", func(t *testing.T) { t.Parallel() diff --git a/internal/examples/chat/index.html b/internal/examples/chat/index.html index 64edd286..7038342d 100644 --- a/internal/examples/chat/index.html +++ b/internal/examples/chat/index.html @@ -2,7 +2,7 @@ - Codestin Search App + Codestin Search App diff --git a/internal/examples/echo/README.md b/internal/examples/echo/README.md index ac03f640..3abbbb57 100644 --- a/internal/examples/echo/README.md +++ b/internal/examples/echo/README.md @@ -1,6 +1,6 @@ # Echo Example -This directory contains a echo server example using nhooyr.io/websocket. +This directory contains a echo server example using github.com/coder/websocket. ```bash $ cd examples/echo diff --git a/internal/examples/echo/server.go b/internal/examples/echo/server.go index 246ad582..5748b110 100644 --- a/internal/examples/echo/server.go +++ b/internal/examples/echo/server.go @@ -9,7 +9,7 @@ import ( "golang.org/x/time/rate" - "nhooyr.io/websocket" + "github.com/coder/websocket" ) // echoServer is the WebSocket echo server implementation. @@ -17,7 +17,7 @@ import ( // only allows one message every 100ms with a 10 message burst. type echoServer struct { // logf controls where logs are sent. - logf func(f string, v ...interface{}) + logf func(f string, v ...any) } func (s echoServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { @@ -37,7 +37,7 @@ func (s echoServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { l := rate.NewLimiter(rate.Every(time.Millisecond*100), 10) for { - err = echo(r.Context(), c, l) + err = echo(c, l) if websocket.CloseStatus(err) == websocket.StatusNormalClosure { return } @@ -51,8 +51,8 @@ func (s echoServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { // echo reads from the WebSocket connection and then writes // the received message back to it. // The entire function has 10s to complete. -func echo(ctx context.Context, c *websocket.Conn, l *rate.Limiter) error { - ctx, cancel := context.WithTimeout(ctx, time.Second*10) +func echo(c *websocket.Conn, l *rate.Limiter) error { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) defer cancel() err := l.Wait(ctx) diff --git a/internal/examples/echo/server_test.go b/internal/examples/echo/server_test.go index 9b608301..81e8cfc2 100644 --- a/internal/examples/echo/server_test.go +++ b/internal/examples/echo/server_test.go @@ -6,8 +6,8 @@ import ( "testing" "time" - "nhooyr.io/websocket" - "nhooyr.io/websocket/wsjson" + "github.com/coder/websocket" + "github.com/coder/websocket/wsjson" ) // Test_echoServer tests the echoServer by sending it 5 different messages diff --git a/internal/examples/go.mod b/internal/examples/go.mod index c98b81ce..e368b76b 100644 --- a/internal/examples/go.mod +++ b/internal/examples/go.mod @@ -1,10 +1,10 @@ -module nhooyr.io/websocket/examples +module github.com/coder/websocket/examples -go 1.19 +go 1.23 -replace nhooyr.io/websocket => ../.. +replace github.com/coder/websocket => ../.. require ( - golang.org/x/time v0.3.0 - nhooyr.io/websocket v0.0.0-00010101000000-000000000000 + github.com/coder/websocket v0.0.0-00010101000000-000000000000 + golang.org/x/time v0.7.0 ) diff --git a/internal/examples/go.sum b/internal/examples/go.sum index f8a07e82..60aa8f9a 100644 --- a/internal/examples/go.sum +++ b/internal/examples/go.sum @@ -1,2 +1,2 @@ -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= diff --git a/internal/test/assert/assert.go b/internal/test/assert/assert.go index 1b90cc9f..7fd98b08 100644 --- a/internal/test/assert/assert.go +++ b/internal/test/assert/assert.go @@ -9,7 +9,7 @@ import ( ) // Equal asserts exp == act. -func Equal(t testing.TB, name string, exp, got interface{}) { +func Equal(t testing.TB, name string, exp, got any) { t.Helper() if !reflect.DeepEqual(exp, got) { @@ -36,7 +36,7 @@ func Error(t testing.TB, err error) { } // Contains asserts the fmt.Sprint(v) contains sub. -func Contains(t testing.TB, v interface{}, sub string) { +func Contains(t testing.TB, v any, sub string) { t.Helper() s := fmt.Sprint(v) diff --git a/internal/test/wstest/echo.go b/internal/test/wstest/echo.go index dc21a8f0..c0c8dcd7 100644 --- a/internal/test/wstest/echo.go +++ b/internal/test/wstest/echo.go @@ -7,9 +7,9 @@ import ( "io" "time" - "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/test/xrand" - "nhooyr.io/websocket/internal/xsync" + "github.com/coder/websocket" + "github.com/coder/websocket/internal/test/xrand" + "github.com/coder/websocket/internal/xsync" ) // EchoLoop echos every msg received from c until an error diff --git a/internal/test/wstest/pipe.go b/internal/test/wstest/pipe.go index 8e1deb47..0e7fdb84 100644 --- a/internal/test/wstest/pipe.go +++ b/internal/test/wstest/pipe.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js package wstest @@ -10,7 +9,7 @@ import ( "net/http" "net/http/httptest" - "nhooyr.io/websocket" + "github.com/coder/websocket" ) // Pipe is used to create an in memory connection diff --git a/internal/thirdparty/frame_test.go b/internal/thirdparty/frame_test.go index 89042e53..75b05291 100644 --- a/internal/thirdparty/frame_test.go +++ b/internal/thirdparty/frame_test.go @@ -11,7 +11,7 @@ import ( _ "github.com/gorilla/websocket" _ "github.com/lesismal/nbio/nbhttp/websocket" - _ "nhooyr.io/websocket" + _ "github.com/coder/websocket" ) func basicMask(b []byte, maskKey [4]byte, pos int) int { @@ -22,10 +22,10 @@ func basicMask(b []byte, maskKey [4]byte, pos int) int { return pos & 3 } -//go:linkname maskGo nhooyr.io/websocket.maskGo +//go:linkname maskGo github.com/coder/websocket.maskGo func maskGo(b []byte, key32 uint32) int -//go:linkname maskAsm nhooyr.io/websocket.maskAsm +//go:linkname maskAsm github.com/coder/websocket.maskAsm func maskAsm(b *byte, len int, key32 uint32) uint32 //go:linkname nbioMaskBytes github.com/lesismal/nbio/nbhttp/websocket.maskXOR diff --git a/internal/thirdparty/gin_test.go b/internal/thirdparty/gin_test.go index 6d59578d..4c88337c 100644 --- a/internal/thirdparty/gin_test.go +++ b/internal/thirdparty/gin_test.go @@ -10,11 +10,11 @@ import ( "github.com/gin-gonic/gin" - "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/errd" - "nhooyr.io/websocket/internal/test/assert" - "nhooyr.io/websocket/internal/test/wstest" - "nhooyr.io/websocket/wsjson" + "github.com/coder/websocket" + "github.com/coder/websocket/internal/errd" + "github.com/coder/websocket/internal/test/assert" + "github.com/coder/websocket/internal/test/wstest" + "github.com/coder/websocket/wsjson" ) func TestGin(t *testing.T) { @@ -42,7 +42,7 @@ func TestGin(t *testing.T) { err = wsjson.Write(ctx, c, "hello") assert.Success(t, err) - var v interface{} + var v any err = wsjson.Read(ctx, c, &v) assert.Success(t, err) assert.Equal(t, "read msg", "hello", v) diff --git a/internal/thirdparty/go.mod b/internal/thirdparty/go.mod index d991dd64..7a86aca9 100644 --- a/internal/thirdparty/go.mod +++ b/internal/thirdparty/go.mod @@ -1,43 +1,45 @@ -module nhooyr.io/websocket/internal/thirdparty +module github.com/coder/websocket/internal/thirdparty -go 1.19 +go 1.23 -replace nhooyr.io/websocket => ../.. +replace github.com/coder/websocket => ../.. require ( - github.com/gin-gonic/gin v1.9.1 - github.com/gobwas/ws v1.3.0 - github.com/gorilla/websocket v1.5.0 - github.com/lesismal/nbio v1.3.18 - nhooyr.io/websocket v0.0.0-00010101000000-000000000000 + github.com/coder/websocket v0.0.0-00010101000000-000000000000 + github.com/gin-gonic/gin v1.10.0 + github.com/gobwas/ws v1.4.0 + github.com/gorilla/websocket v1.5.3 + github.com/lesismal/nbio v1.5.12 ) require ( - github.com/bytedance/sonic v1.9.1 // indirect - github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect - github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/goccy/go-json v0.10.2 // indirect github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.4 // indirect - github.com/leodido/go-urn v1.2.4 // indirect - github.com/lesismal/llib v1.1.12 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/lesismal/llib v1.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.11 // indirect - golang.org/x/arch v0.3.0 // indirect - golang.org/x/crypto v0.9.0 // indirect - golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.17.0 // indirect - golang.org/x/text v0.9.0 // indirect - google.golang.org/protobuf v1.30.0 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/internal/thirdparty/go.sum b/internal/thirdparty/go.sum index 1f542103..a7be7082 100644 --- a/internal/thirdparty/go.sum +++ b/internal/thirdparty/go.sum @@ -1,129 +1,110 @@ -github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= -github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= -github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= -github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= -github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= -github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= -github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.3.0 h1:sbeU3Y4Qzlb+MOzIe6mQGf7QR4Hkv6ZD0qhGkBFL2O0= -github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/gobwas/ws v1.4.0 h1:CTaoG1tojrh4ucGPcoJFiAQUAsEWekEWvLy7GsVNqGs= +github.com/gobwas/ws v1.4.0/go.mod h1:G3gNqMNtPppf5XUz7O4shetPpcZ1VJ7zt18dlUeakrc= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= -github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= -github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= -github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= -github.com/lesismal/llib v1.1.12 h1:KJFB8bL02V+QGIvILEw/w7s6bKj9Ps9Px97MZP2EOk0= -github.com/lesismal/llib v1.1.12/go.mod h1:70tFXXe7P1FZ02AU9l8LgSOK7d7sRrpnkUr3rd3gKSg= -github.com/lesismal/nbio v1.3.18 h1:kmJZlxjQpVfuCPYcXdv0Biv9LHVViJZet5K99Xs3RAs= -github.com/lesismal/nbio v1.3.18/go.mod h1:KWlouFT5cgDdW5sMX8RsHASUMGniea9X0XIellZ0B38= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/lesismal/llib v1.1.13 h1:+w1+t0PykXpj2dXQck0+p6vdC9/mnbEXHgUy/HXDGfE= +github.com/lesismal/llib v1.1.13/go.mod h1:70tFXXe7P1FZ02AU9l8LgSOK7d7sRrpnkUr3rd3gKSg= +github.com/lesismal/nbio v1.5.12 h1:YcUjjmOvmKEANs6Oo175JogXvHy8CuE7i6ccjM2/tv4= +github.com/lesismal/nbio v1.5.12/go.mod h1:QsxE0fKFe1PioyjuHVDn2y8ktYK7xv9MFbpkoRFj8vI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= -github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= -golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20210513122933-cd7d49e622d5/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= -golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/internal/wsjs/wsjs_js.go b/internal/wsjs/wsjs_js.go index 11eb59cb..45ecf49d 100644 --- a/internal/wsjs/wsjs_js.go +++ b/internal/wsjs/wsjs_js.go @@ -33,7 +33,7 @@ func New(url string, protocols []string) (c WebSocket, err error) { c = WebSocket{} }) - jsProtocols := make([]interface{}, len(protocols)) + jsProtocols := make([]any, len(protocols)) for i, p := range protocols { jsProtocols[i] = p } @@ -57,7 +57,7 @@ func (c WebSocket) setBinaryType(typ string) { } func (c WebSocket) addEventListener(eventType string, fn func(e js.Value)) func() { - f := js.FuncOf(func(this js.Value, args []js.Value) interface{} { + f := js.FuncOf(func(this js.Value, args []js.Value) any { fn(args[0]) return nil }) @@ -97,7 +97,7 @@ func (c WebSocket) OnError(fn func(e js.Value)) (remove func()) { // MessageEvent is the type passed to a message handler. type MessageEvent struct { // string or []byte. - Data interface{} + Data any // There are more fields to the interface but we don't use them. // See https://developer.mozilla.org/en-US/docs/Web/API/MessageEvent @@ -106,7 +106,7 @@ type MessageEvent struct { // OnMessage registers a function to be called when the WebSocket receives a message. func (c WebSocket) OnMessage(fn func(m MessageEvent)) (remove func()) { return c.addEventListener("message", func(e js.Value) { - var data interface{} + var data any arrayBuffer := e.Get("data") if arrayBuffer.Type() == js.TypeString { diff --git a/internal/xsync/go_test.go b/internal/xsync/go_test.go index dabea8a5..a3f7053b 100644 --- a/internal/xsync/go_test.go +++ b/internal/xsync/go_test.go @@ -3,7 +3,7 @@ package xsync import ( "testing" - "nhooyr.io/websocket/internal/test/assert" + "github.com/coder/websocket/internal/test/assert" ) func TestGoRecover(t *testing.T) { diff --git a/internal/xsync/int64.go b/internal/xsync/int64.go deleted file mode 100644 index a0c40204..00000000 --- a/internal/xsync/int64.go +++ /dev/null @@ -1,23 +0,0 @@ -package xsync - -import ( - "sync/atomic" -) - -// Int64 represents an atomic int64. -type Int64 struct { - // We do not use atomic.Load/StoreInt64 since it does not - // work on 32 bit computers but we need 64 bit integers. - i atomic.Value -} - -// Load loads the int64. -func (v *Int64) Load() int64 { - i, _ := v.i.Load().(int64) - return i -} - -// Store stores the int64. -func (v *Int64) Store(i int64) { - v.i.Store(i) -} diff --git a/make.sh b/make.sh deleted file mode 100755 index 170d00a8..00000000 --- a/make.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/bin/sh -set -eu -cd -- "$(dirname "$0")" - -echo "=== fmt.sh" -./ci/fmt.sh -echo "=== lint.sh" -./ci/lint.sh -echo "=== test.sh" -./ci/test.sh "$@" -echo "=== bench.sh" -./ci/bench.sh diff --git a/mask_test.go b/mask_test.go index 54f55e43..7d6aedd7 100644 --- a/mask_test.go +++ b/mask_test.go @@ -8,7 +8,7 @@ import ( "math/bits" "testing" - "nhooyr.io/websocket/internal/test/assert" + "github.com/coder/websocket/internal/test/assert" ) func basicMask(b []byte, key uint32) uint32 { @@ -40,7 +40,7 @@ func TestMask(t *testing.T) { func testMask(t *testing.T, name string, fn func(b []byte, key uint32) uint32) { t.Run(name, func(t *testing.T) { t.Parallel() - for i := 0; i < 9999; i++ { + for range 9999 { keyb := make([]byte, 4) _, err := rand.Read(keyb) assert.Success(t, err) diff --git a/netconn.go b/netconn.go index 86f7dadb..1f73b04b 100644 --- a/netconn.go +++ b/netconn.go @@ -68,7 +68,7 @@ func NetConn(ctx context.Context, c *Conn, msgType MessageType) net.Conn { defer nc.writeMu.unlock() // Prevents future writes from writing until the deadline is reset. - atomic.StoreInt64(&nc.writeExpired, 1) + nc.writeExpired.Store(1) }) if !nc.writeTimer.Stop() { <-nc.writeTimer.C @@ -84,7 +84,7 @@ func NetConn(ctx context.Context, c *Conn, msgType MessageType) net.Conn { defer nc.readMu.unlock() // Prevents future reads from reading until the deadline is reset. - atomic.StoreInt64(&nc.readExpired, 1) + nc.readExpired.Store(1) }) if !nc.readTimer.Stop() { <-nc.readTimer.C @@ -94,25 +94,22 @@ func NetConn(ctx context.Context, c *Conn, msgType MessageType) net.Conn { } type netConn struct { - // These must be first to be aligned on 32 bit platforms. - // https://github.com/nhooyr/websocket/pull/438 - readExpired int64 - writeExpired int64 - c *Conn msgType MessageType - writeTimer *time.Timer - writeMu *mu - writeCtx context.Context - writeCancel context.CancelFunc - - readTimer *time.Timer - readMu *mu - readCtx context.Context - readCancel context.CancelFunc - readEOFed bool - reader io.Reader + writeTimer *time.Timer + writeMu *mu + writeExpired atomic.Int64 + writeCtx context.Context + writeCancel context.CancelFunc + + readTimer *time.Timer + readMu *mu + readExpired atomic.Int64 + readCtx context.Context + readCancel context.CancelFunc + readEOFed bool + reader io.Reader } var _ net.Conn = &netConn{} @@ -129,7 +126,7 @@ func (nc *netConn) Write(p []byte) (int, error) { nc.writeMu.forceLock() defer nc.writeMu.unlock() - if atomic.LoadInt64(&nc.writeExpired) == 1 { + if nc.writeExpired.Load() == 1 { return 0, fmt.Errorf("failed to write: %w", context.DeadlineExceeded) } @@ -157,7 +154,7 @@ func (nc *netConn) Read(p []byte) (int, error) { } func (nc *netConn) read(p []byte) (int, error) { - if atomic.LoadInt64(&nc.readExpired) == 1 { + if nc.readExpired.Load() == 1 { return 0, fmt.Errorf("failed to read: %w", context.DeadlineExceeded) } @@ -191,8 +188,7 @@ func (nc *netConn) read(p []byte) (int, error) { return n, err } -type websocketAddr struct { -} +type websocketAddr struct{} func (a websocketAddr) Network() string { return "websocket" @@ -209,7 +205,7 @@ func (nc *netConn) SetDeadline(t time.Time) error { } func (nc *netConn) SetWriteDeadline(t time.Time) error { - atomic.StoreInt64(&nc.writeExpired, 0) + nc.writeExpired.Store(0) if t.IsZero() { nc.writeTimer.Stop() } else { @@ -223,7 +219,7 @@ func (nc *netConn) SetWriteDeadline(t time.Time) error { } func (nc *netConn) SetReadDeadline(t time.Time) error { - atomic.StoreInt64(&nc.readExpired, 0) + nc.readExpired.Store(0) if t.IsZero() { nc.readTimer.Stop() } else { diff --git a/netconn_notjs.go b/netconn_notjs.go index f3eb0d66..cab76349 100644 --- a/netconn_notjs.go +++ b/netconn_notjs.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js package websocket diff --git a/read.go b/read.go index a59e71d9..64822511 100644 --- a/read.go +++ b/read.go @@ -1,5 +1,4 @@ //go:build !js -// +build !js package websocket @@ -11,11 +10,11 @@ import ( "io" "net" "strings" + "sync/atomic" "time" - "nhooyr.io/websocket/internal/errd" - "nhooyr.io/websocket/internal/util" - "nhooyr.io/websocket/internal/xsync" + "github.com/coder/websocket/internal/errd" + "github.com/coder/websocket/internal/util" ) // Reader reads from the connection until there is a WebSocket @@ -91,7 +90,8 @@ func (c *Conn) CloseRead(ctx context.Context) context.Context { // // By default, the connection has a message read limit of 32768 bytes. // -// When the limit is hit, the connection will be closed with StatusMessageTooBig. +// When the limit is hit, reads return an error wrapping ErrMessageTooBig and +// the connection is closed with StatusMessageTooBig. // // Set to -1 to disable. func (c *Conn) SetReadLimit(n int64) { @@ -217,60 +217,73 @@ func (c *Conn) readLoop(ctx context.Context) (header, error) { } } -func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { +// prepareRead sets the readTimeout context and returns a done function +// to be called after the read is done. It also returns an error if the +// connection is closed. The reference to the error is used to assign +// an error depending on if the connection closed or the context timed +// out during use. Typically, the referenced error is a named return +// variable of the function calling this method. +func (c *Conn) prepareRead(ctx context.Context, err *error) (func(), error) { select { case <-c.closed: - return header{}, net.ErrClosed - case c.readTimeout <- ctx: + return nil, net.ErrClosed + default: } + c.setupReadTimeout(ctx) - h, err := readFrameHeader(c.br, c.readHeaderBuf[:]) - if err != nil { + done := func() { + c.clearReadTimeout() select { case <-c.closed: - return header{}, net.ErrClosed - case <-ctx.Done(): - return header{}, ctx.Err() + if *err != nil { + *err = net.ErrClosed + } default: - return header{}, err + } + if *err != nil && ctx.Err() != nil { + *err = ctx.Err() } } - select { - case <-c.closed: - return header{}, net.ErrClosed - case c.readTimeout <- context.Background(): + c.closeStateMu.Lock() + closeReceivedErr := c.closeReceivedErr + c.closeStateMu.Unlock() + if closeReceivedErr != nil { + defer done() + return nil, closeReceivedErr } - return h, nil + return done, nil } -func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { - select { - case <-c.closed: - return 0, net.ErrClosed - case c.readTimeout <- ctx: +func (c *Conn) readFrameHeader(ctx context.Context) (_ header, err error) { + readDone, err := c.prepareRead(ctx, &err) + if err != nil { + return header{}, err } + defer readDone() - n, err := io.ReadFull(c.br, p) + h, err := readFrameHeader(c.br, c.readHeaderBuf[:]) if err != nil { - select { - case <-c.closed: - return n, net.ErrClosed - case <-ctx.Done(): - return n, ctx.Err() - default: - return n, fmt.Errorf("failed to read frame payload: %w", err) - } + return header{}, err } - select { - case <-c.closed: - return n, net.ErrClosed - case c.readTimeout <- context.Background(): + return h, nil +} + +func (c *Conn) readFramePayload(ctx context.Context, p []byte) (_ int, err error) { + readDone, err := c.prepareRead(ctx, &err) + if err != nil { + return 0, err } + defer readDone() - return n, err + n, err := io.ReadFull(c.br, p) + if err != nil { + return n, fmt.Errorf("failed to read frame payload: %w", err) + } + + return n, nil } func (c *Conn) handleControl(ctx context.Context, h header) (err error) { @@ -301,8 +314,16 @@ func (c *Conn) handleControl(ctx context.Context, h header) (err error) { switch h.opcode { case opPing: + if c.onPingReceived != nil { + if !c.onPingReceived(ctx, b) { + return nil + } + } return c.writeControl(ctx, opPong, b) case opPong: + if c.onPongReceived != nil { + c.onPongReceived(ctx, b) + } c.activePingsMu.Lock() pong, ok := c.activePings[string(b)] c.activePingsMu.Unlock() @@ -325,9 +346,22 @@ func (c *Conn) handleControl(ctx context.Context, h header) (err error) { } err = fmt.Errorf("received close frame: %w", ce) - c.writeClose(ce.Code, ce.Reason) - c.readMu.unlock() - c.close() + c.closeStateMu.Lock() + c.closeReceivedErr = err + closeSent := c.closeSentErr != nil + c.closeStateMu.Unlock() + + // Only unlock readMu if this connection is being closed becaue + // c.close will try to acquire the readMu lock. We unlock for + // writeClose as well because it may also call c.close. + if !closeSent { + c.readMu.unlock() + _ = c.writeClose(ce.Code, ce.Reason) + } + if !c.casClosing() { + c.readMu.unlock() + _ = c.close() + } return err } @@ -465,7 +499,7 @@ func (mr *msgReader) read(p []byte) (int, error) { type limitReader struct { c *Conn r io.Reader - limit xsync.Int64 + limit atomic.Int64 n int64 } @@ -489,9 +523,9 @@ func (lr *limitReader) Read(p []byte) (int, error) { } if lr.n == 0 { - err := fmt.Errorf("read limited at %v bytes", lr.limit.Load()) - lr.c.writeError(StatusMessageTooBig, err) - return 0, err + reason := fmt.Errorf("read limited at %d bytes", lr.limit.Load()) + lr.c.writeError(StatusMessageTooBig, reason) + return 0, fmt.Errorf("%w: %v", ErrMessageTooBig, reason) } if int64(len(p)) > lr.n { diff --git a/write.go b/write.go index d7222f2d..d7172a7b 100644 --- a/write.go +++ b/write.go @@ -1,10 +1,10 @@ //go:build !js -// +build !js package websocket import ( "bufio" + "compress/flate" "context" "crypto/rand" "encoding/binary" @@ -14,10 +14,8 @@ import ( "net" "time" - "compress/flate" - - "nhooyr.io/websocket/internal/errd" - "nhooyr.io/websocket/internal/util" + "github.com/coder/websocket/internal/errd" + "github.com/coder/websocket/internal/util" ) // Writer returns a writer bounded by the context that will write @@ -249,25 +247,35 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco } defer c.writeFrameMu.unlock() - select { - case <-c.closed: - return 0, net.ErrClosed - case c.writeTimeout <- ctx: - } - defer func() { + if c.isClosed() && opcode == opClose { + err = nil + } if err != nil { - select { - case <-c.closed: - err = net.ErrClosed - case <-ctx.Done(): + if ctx.Err() != nil { err = ctx.Err() - default: + } else if c.isClosed() { + err = net.ErrClosed } err = fmt.Errorf("failed to write frame: %w", err) } }() + c.closeStateMu.Lock() + closeSentErr := c.closeSentErr + c.closeStateMu.Unlock() + if closeSentErr != nil { + return 0, net.ErrClosed + } + + select { + case <-c.closed: + return 0, net.ErrClosed + default: + } + c.setupWriteTimeout(ctx) + defer c.clearWriteTimeout() + c.writeHeader.fin = fin c.writeHeader.opcode = opcode c.writeHeader.payloadLength = int64(len(p)) @@ -303,13 +311,16 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco } } - select { - case <-c.closed: - if opcode == opClose { - return n, nil + if opcode == opClose { + c.closeStateMu.Lock() + c.closeSentErr = fmt.Errorf("sent close frame: %w", net.ErrClosed) + closeReceived := c.closeReceivedErr != nil + c.closeStateMu.Unlock() + + if closeReceived && !c.casClosing() { + c.writeFrameMu.unlock() + _ = c.close() } - return n, net.ErrClosed - case c.writeTimeout <- context.Background(): } return n, nil @@ -335,10 +346,7 @@ func (c *Conn) writeFramePayload(p []byte) (n int, err error) { // Start of next write in the buffer. i := c.bw.Buffered() - j := len(p) - if j > c.bw.Available() { - j = c.bw.Available() - } + j := min(len(p), c.bw.Available()) _, err := c.bw.Write(p[:j]) if err != nil { diff --git a/ws_js.go b/ws_js.go index 02d61f28..026b75fc 100644 --- a/ws_js.go +++ b/ws_js.go @@ -1,4 +1,4 @@ -package websocket // import "nhooyr.io/websocket" +package websocket // import "github.com/coder/websocket" import ( "bytes" @@ -12,11 +12,11 @@ import ( "runtime" "strings" "sync" + "sync/atomic" "syscall/js" - "nhooyr.io/websocket/internal/bpool" - "nhooyr.io/websocket/internal/wsjs" - "nhooyr.io/websocket/internal/xsync" + "github.com/coder/websocket/internal/bpool" + "github.com/coder/websocket/internal/wsjs" ) // opcode represents a WebSocket opcode. @@ -45,7 +45,7 @@ type Conn struct { ws wsjs.WebSocket // read limit for a message in bytes. - msgReadLimit xsync.Int64 + msgReadLimit atomic.Int64 closeReadMu sync.Mutex closeReadCtx context.Context @@ -144,9 +144,9 @@ func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { } readLimit := c.msgReadLimit.Load() if readLimit >= 0 && int64(len(p)) > readLimit { - err := fmt.Errorf("read limited at %v bytes", c.msgReadLimit.Load()) - c.Close(StatusMessageTooBig, err.Error()) - return 0, nil, err + reason := fmt.Errorf("read limited at %d bytes", c.msgReadLimit.Load()) + c.Close(StatusMessageTooBig, reason.Error()) + return 0, nil, fmt.Errorf("%w: %v", ErrMessageTooBig, reason) } return typ, p, nil } @@ -196,7 +196,7 @@ func (c *Conn) Ping(ctx context.Context) error { // Write writes a message of the given type to the connection. // Always non blocking. func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { - err := c.write(ctx, typ, p) + err := c.write(typ, p) if err != nil { // Have to ensure the WebSocket is closed after a write error // to match the Go API. It can only error if the message type @@ -210,7 +210,7 @@ func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { return nil } -func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) error { +func (c *Conn) write(typ MessageType, p []byte) error { if c.isClosed() { return net.ErrClosed } diff --git a/ws_js_test.go b/ws_js_test.go index ba98b9a0..1fa242f4 100644 --- a/ws_js_test.go +++ b/ws_js_test.go @@ -7,9 +7,9 @@ import ( "testing" "time" - "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/test/assert" - "nhooyr.io/websocket/internal/test/wstest" + "github.com/coder/websocket" + "github.com/coder/websocket/internal/test/assert" + "github.com/coder/websocket/internal/test/wstest" ) func TestWasm(t *testing.T) { @@ -28,7 +28,7 @@ func TestWasm(t *testing.T) { assert.Equal(t, "response code", http.StatusSwitchingProtocols, resp.StatusCode) c.SetReadLimit(65536) - for i := 0; i < 10; i++ { + for range 10 { err = wstest.Echo(ctx, c, 65536) assert.Success(t, err) } diff --git a/wsjson/wsjson.go b/wsjson/wsjson.go index 7c986a0d..ffa068bf 100644 --- a/wsjson/wsjson.go +++ b/wsjson/wsjson.go @@ -1,24 +1,24 @@ // Package wsjson provides helpers for reading and writing JSON messages. -package wsjson // import "nhooyr.io/websocket/wsjson" +package wsjson // import "github.com/coder/websocket/wsjson" import ( "context" "encoding/json" "fmt" - "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/bpool" - "nhooyr.io/websocket/internal/errd" - "nhooyr.io/websocket/internal/util" + "github.com/coder/websocket" + "github.com/coder/websocket/internal/bpool" + "github.com/coder/websocket/internal/errd" + "github.com/coder/websocket/internal/util" ) // Read reads a JSON message from c into v. // It will reuse buffers in between calls to avoid allocations. -func Read(ctx context.Context, c *websocket.Conn, v interface{}) error { +func Read(ctx context.Context, c *websocket.Conn, v any) error { return read(ctx, c, v) } -func read(ctx context.Context, c *websocket.Conn, v interface{}) (err error) { +func read(ctx context.Context, c *websocket.Conn, v any) (err error) { defer errd.Wrap(&err, "failed to read JSON message") _, r, err := c.Reader(ctx) @@ -45,11 +45,11 @@ func read(ctx context.Context, c *websocket.Conn, v interface{}) (err error) { // Write writes the JSON message v to c. // It will reuse buffers in between calls to avoid allocations. -func Write(ctx context.Context, c *websocket.Conn, v interface{}) error { +func Write(ctx context.Context, c *websocket.Conn, v any) error { return write(ctx, c, v) } -func write(ctx context.Context, c *websocket.Conn, v interface{}) (err error) { +func write(ctx context.Context, c *websocket.Conn, v any) (err error) { defer errd.Wrap(&err, "failed to write JSON message") // json.Marshal cannot reuse buffers between calls as it has to return diff --git a/wsjson/wsjson_test.go b/wsjson/wsjson_test.go index 080ab38d..87a72854 100644 --- a/wsjson/wsjson_test.go +++ b/wsjson/wsjson_test.go @@ -6,7 +6,7 @@ import ( "strconv" "testing" - "nhooyr.io/websocket/internal/test/xrand" + "github.com/coder/websocket/internal/test/xrand" ) func BenchmarkJSON(b *testing.B) {