@pi-in-go/pigpen-ahp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CREDITS.md +27 -0
- package/LICENSE +23 -0
- package/README.md +148 -0
- package/extensions/ahp/access_options_test.go +116 -0
- package/extensions/ahp/extension.go +54 -0
- package/extensions/ahp/go.mod +12 -0
- package/extensions/ahp/go.sum +2 -0
- package/extensions/ahp/go.work +8 -0
- package/extensions/ahp/internal/channels/chat.go +110 -0
- package/extensions/ahp/internal/channels/root.go +32 -0
- package/extensions/ahp/internal/channels/session.go +128 -0
- package/extensions/ahp/internal/channels/session_test.go +108 -0
- package/extensions/ahp/internal/compose/compose.go +153 -0
- package/extensions/ahp/internal/compose/compose_test.go +120 -0
- package/extensions/ahp/internal/gaps/gaps_test.go +88 -0
- package/extensions/ahp/internal/host/handshake_test.go +206 -0
- package/extensions/ahp/internal/host/host.go +1305 -0
- package/extensions/ahp/internal/host/reconnect_test.go +197 -0
- package/extensions/ahp/internal/host/replaywindow_test.go +31 -0
- package/extensions/ahp/internal/host/schema_test.go +79 -0
- package/extensions/ahp/internal/host/store.go +291 -0
- package/extensions/ahp/internal/host/store_update_test.go +85 -0
- package/extensions/ahp/internal/host/subscriptions_test.go +160 -0
- package/extensions/ahp/internal/host/surface_test.go +95 -0
- package/extensions/ahp/internal/host/workarounds.go +458 -0
- package/extensions/ahp/internal/host/workarounds_test.go +96 -0
- package/extensions/ahp/internal/live/live.go +328 -0
- package/extensions/ahp/internal/live/live_test.go +91 -0
- package/extensions/ahp/internal/live/store.go +113 -0
- package/extensions/ahp/internal/mapper/activity.go +199 -0
- package/extensions/ahp/internal/mapper/activity_test.go +188 -0
- package/extensions/ahp/internal/mapper/bench_test.go +38 -0
- package/extensions/ahp/internal/mapper/event_mapper_test.go +380 -0
- package/extensions/ahp/internal/mapper/fixtures_test.go +387 -0
- package/extensions/ahp/internal/mapper/helpers_test.go +251 -0
- package/extensions/ahp/internal/mapper/js.go +131 -0
- package/extensions/ahp/internal/mapper/mapper.go +600 -0
- package/extensions/ahp/internal/mapper/message_input_test.go +196 -0
- package/extensions/ahp/internal/mapper/messageinput.go +272 -0
- package/extensions/ahp/internal/mapper/schema_helpers_test.go +5 -0
- package/extensions/ahp/internal/mapper/testdata/LICENSE-pi-ahp +21 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/abort.json +711 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/bash-long-output.json +1312 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/compaction.json +6285 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/parallel-tools.json +2622 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/plain-text.json +1856 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/single-tool.json +1128 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/steering.json +3926 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-bash.json +2372 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-edit.json +3983 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-error.json +2850 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-find.json +1162 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-grep.json +2783 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-loop.json +1693 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-ls.json +2427 -0
- package/extensions/ahp/internal/mapper/testdata/fixtures/tool-write.json +1271 -0
- package/extensions/ahp/internal/mapper/title.go +34 -0
- package/extensions/ahp/internal/mapper/usermsg.go +87 -0
- package/extensions/ahp/internal/pi/activeturn_test.go +296 -0
- package/extensions/ahp/internal/pi/backend.go +64 -0
- package/extensions/ahp/internal/pi/catalogue.go +370 -0
- package/extensions/ahp/internal/pi/catalogue_test.go +271 -0
- package/extensions/ahp/internal/pi/chatdriver.go +533 -0
- package/extensions/ahp/internal/pi/chatdriver_test.go +416 -0
- package/extensions/ahp/internal/pi/clientactions.go +212 -0
- package/extensions/ahp/internal/pi/clientactions_test.go +273 -0
- package/extensions/ahp/internal/pi/completions.go +230 -0
- package/extensions/ahp/internal/pi/completions_test.go +272 -0
- package/extensions/ahp/internal/pi/config_test.go +269 -0
- package/extensions/ahp/internal/pi/deletesession.go +69 -0
- package/extensions/ahp/internal/pi/deletesession_test.go +153 -0
- package/extensions/ahp/internal/pi/disposal_test.go +362 -0
- package/extensions/ahp/internal/pi/fixture_test.go +308 -0
- package/extensions/ahp/internal/pi/foreign_test.go +122 -0
- package/extensions/ahp/internal/pi/harness_test.go +83 -0
- package/extensions/ahp/internal/pi/history.go +269 -0
- package/extensions/ahp/internal/pi/hydrated_test.go +171 -0
- package/extensions/ahp/internal/pi/hydratedlifecycle_test.go +196 -0
- package/extensions/ahp/internal/pi/hydration_test.go +209 -0
- package/extensions/ahp/internal/pi/hydrator.go +212 -0
- package/extensions/ahp/internal/pi/imageinput.go +224 -0
- package/extensions/ahp/internal/pi/imageinput_test.go +57 -0
- package/extensions/ahp/internal/pi/lifecycle_test.go +381 -0
- package/extensions/ahp/internal/pi/models.go +212 -0
- package/extensions/ahp/internal/pi/models_test.go +128 -0
- package/extensions/ahp/internal/pi/paging.go +95 -0
- package/extensions/ahp/internal/pi/pagingtruncate_test.go +323 -0
- package/extensions/ahp/internal/pi/projecttrust.go +222 -0
- package/extensions/ahp/internal/pi/registry.go +924 -0
- package/extensions/ahp/internal/pi/restart_test.go +185 -0
- package/extensions/ahp/internal/pi/services.go +70 -0
- package/extensions/ahp/internal/pi/sessionconfig.go +64 -0
- package/extensions/ahp/internal/pi/sessionfiles_test.go +74 -0
- package/extensions/ahp/internal/pi/sessionstore.go +83 -0
- package/extensions/ahp/internal/pi/summary_test.go +426 -0
- package/extensions/ahp/internal/pi/workarounds_wire_test.go +208 -0
- package/extensions/ahp/internal/pisession/fromentries_test.go +32 -0
- package/extensions/ahp/internal/pisession/json.go +31 -0
- package/extensions/ahp/internal/pisession/pisession.go +580 -0
- package/extensions/ahp/internal/settings/access.go +63 -0
- package/extensions/ahp/internal/settings/access_test.go +52 -0
- package/extensions/ahp/internal/settings/settings.go +117 -0
- package/extensions/ahp/internal/settings/settings_test.go +104 -0
- package/extensions/ahp/internal/svc/etag_other.go +15 -0
- package/extensions/ahp/internal/svc/etag_unix.go +34 -0
- package/extensions/ahp/internal/svc/glob.go +197 -0
- package/extensions/ahp/internal/svc/mime.go +37 -0
- package/extensions/ahp/internal/svc/paths.go +108 -0
- package/extensions/ahp/internal/svc/pty.go +64 -0
- package/extensions/ahp/internal/svc/pty_darwin.go +42 -0
- package/extensions/ahp/internal/svc/pty_linux.go +36 -0
- package/extensions/ahp/internal/svc/pty_other.go +9 -0
- package/extensions/ahp/internal/svc/pty_test.go +114 -0
- package/extensions/ahp/internal/svc/pty_unix.go +147 -0
- package/extensions/ahp/internal/svc/resource.go +495 -0
- package/extensions/ahp/internal/svc/resource_test.go +501 -0
- package/extensions/ahp/internal/svc/stat_bsd.go +9 -0
- package/extensions/ahp/internal/svc/stat_linux.go +9 -0
- package/extensions/ahp/internal/svc/terminal.go +454 -0
- package/extensions/ahp/internal/svc/terminal_test.go +512 -0
- package/extensions/ahp/internal/svc/watch.go +590 -0
- package/extensions/ahp/internal/svc/watch_test.go +627 -0
- package/extensions/ahp/internal/svc/watchevents_test.go +452 -0
- package/extensions/ahp/internal/svc/watchpolicy.go +81 -0
- package/extensions/ahp/internal/svc/watchpolicy_test.go +151 -0
- package/extensions/ahp/internal/svc/watchracy_test.go +61 -0
- package/extensions/ahp/internal/testkit/schema/LICENSE-agent-host-protocol +21 -0
- package/extensions/ahp/internal/testkit/schema/actions.schema.json +9208 -0
- package/extensions/ahp/internal/testkit/schema/commands.schema.json +10862 -0
- package/extensions/ahp/internal/testkit/schema/errors.schema.json +10928 -0
- package/extensions/ahp/internal/testkit/schema/notifications.schema.json +6667 -0
- package/extensions/ahp/internal/testkit/schema/state.schema.json +6392 -0
- package/extensions/ahp/internal/testkit/schema.go +360 -0
- package/extensions/ahp/internal/testkit/testkit.go +367 -0
- package/extensions/ahp/internal/twin/twin.go +27 -0
- package/extensions/ahp/internal/wire/dispatchable.go +106 -0
- package/extensions/ahp/internal/wire/dispatchable_list.go +15 -0
- package/extensions/ahp/internal/wire/helpers_test.go +5 -0
- package/extensions/ahp/internal/wire/wire.go +218 -0
- package/extensions/ahp/internal/wire/wire_test.go +123 -0
- package/extensions/ahp/internal/ws/helpers_test.go +7 -0
- package/extensions/ahp/internal/ws/vscode_test.go +198 -0
- package/extensions/ahp/internal/ws/ws.go +674 -0
- package/extensions/ahp/internal/ws/ws_test.go +324 -0
- package/extensions/ahp/realpig_test.go +589 -0
- package/extensions/ahp/runtime.go +311 -0
- package/extensions/ahp/runtime_test.go +110 -0
- package/extensions/ahp/testdata/upstream-tests.json +3833 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/LICENSE +21 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/NOTICE-PIGPEN.md +18 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/client.go +1011 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/error.go +111 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/multi_host_state_mirror.go +239 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/reducers.go +1939 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahp/transport.go +176 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/actions.generated.go +2447 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/commands.generated.go +1546 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/common.go +205 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/errors.generated.go +65 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/messages.generated.go +138 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/notifications.generated.go +264 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/state.generated.go +6171 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/ahptypes/version.generated.go +31 -0
- package/extensions/ahp/third_party/agent-host-protocol-go/go.mod +3 -0
- package/extensions/ahp/twins_test.go +91 -0
- package/package.json +40 -0
- package/proof/PORT.md +121 -0
- package/proof/mutations.json +152 -0
- package/proof/oracle/LICENSE +21 -0
- package/proof/oracle/README.md +120 -0
- package/proof/oracle/UPSTREAM.md +7 -0
- package/proof/oracle/package.json +68 -0
- package/proof/oracle/src/bin/cli.ts +66 -0
- package/proof/oracle/src/bin/tunnel.ts +128 -0
- package/proof/oracle/src/channels/chat.ts +134 -0
- package/proof/oracle/src/channels/root.ts +39 -0
- package/proof/oracle/src/channels/session.ts +118 -0
- package/proof/oracle/src/channels/terminal.ts +11 -0
- package/proof/oracle/src/core/channels.ts +108 -0
- package/proof/oracle/src/core/client-workarounds.ts +358 -0
- package/proof/oracle/src/core/connection.ts +41 -0
- package/proof/oracle/src/core/host.ts +882 -0
- package/proof/oracle/src/core/sequencer.ts +75 -0
- package/proof/oracle/src/core/state-store.ts +162 -0
- package/proof/oracle/src/core/uri.ts +25 -0
- package/proof/oracle/src/host/direct-settings.ts +110 -0
- package/proof/oracle/src/host/pi-host.ts +220 -0
- package/proof/oracle/src/host/serve.ts +71 -0
- package/proof/oracle/src/host/terminal-service.ts +346 -0
- package/proof/oracle/src/pi/activity.ts +171 -0
- package/proof/oracle/src/pi/changeset-service.ts +646 -0
- package/proof/oracle/src/pi/changeset-uri.ts +64 -0
- package/proof/oracle/src/pi/chat-driver.ts +529 -0
- package/proof/oracle/src/pi/completions.ts +160 -0
- package/proof/oracle/src/pi/delete-session.ts +53 -0
- package/proof/oracle/src/pi/event-mapper.ts +648 -0
- package/proof/oracle/src/pi/git-changes.ts +605 -0
- package/proof/oracle/src/pi/history.ts +305 -0
- package/proof/oracle/src/pi/image-input.ts +56 -0
- package/proof/oracle/src/pi/image-mime.ts +8 -0
- package/proof/oracle/src/pi/in-process-backend.ts +155 -0
- package/proof/oracle/src/pi/message-input.ts +212 -0
- package/proof/oracle/src/pi/models.ts +124 -0
- package/proof/oracle/src/pi/project-trust.ts +66 -0
- package/proof/oracle/src/pi/provider.ts +2 -0
- package/proof/oracle/src/pi/resource-paths.ts +80 -0
- package/proof/oracle/src/pi/resource-service.ts +356 -0
- package/proof/oracle/src/pi/resource-watch-policy.ts +35 -0
- package/proof/oracle/src/pi/resource-watch.ts +357 -0
- package/proof/oracle/src/pi/session-catalogue.ts +318 -0
- package/proof/oracle/src/pi/session-config.ts +84 -0
- package/proof/oracle/src/pi/session-history.ts +75 -0
- package/proof/oracle/src/pi/session-hydrator.ts +219 -0
- package/proof/oracle/src/pi/session-registry.ts +815 -0
- package/proof/oracle/src/pi/session-storage.ts +29 -0
- package/proof/oracle/src/pi/session-title.ts +18 -0
- package/proof/oracle/src/pi/turn-paging.ts +94 -0
- package/proof/oracle/src/pi/user-message.ts +66 -0
- package/proof/oracle/src/protocol/errors.ts +44 -0
- package/proof/oracle/src/protocol/jsonrpc.ts +89 -0
- package/proof/oracle/src/protocol/version.ts +39 -0
- package/proof/oracle/src/transport/websocket.ts +136 -0
- package/proof/oracle/src/tunnel/devtunnel.ts +293 -0
- package/proof/oracle/src/tunnel/discovery.ts +37 -0
- package/proof/oracle/test/active-turn-reconnect.test.ts +274 -0
- package/proof/oracle/test/activity.test.ts +171 -0
- package/proof/oracle/test/changeset-lifecycle.test.serial.ts +303 -0
- package/proof/oracle/test/changeset-uri.test.ts +37 -0
- package/proof/oracle/test/changeset.test.serial.ts +487 -0
- package/proof/oracle/test/chat-driver.test.ts +691 -0
- package/proof/oracle/test/client-actions.test.ts +409 -0
- package/proof/oracle/test/client-workarounds.test.ts +320 -0
- package/proof/oracle/test/completions.test.ts +323 -0
- package/proof/oracle/test/delete-session.test.ts +142 -0
- package/proof/oracle/test/direct-settings.test.ts +81 -0
- package/proof/oracle/test/event-mapper.test.ts +619 -0
- package/proof/oracle/test/fetch-turns.test.ts +207 -0
- package/proof/oracle/test/fixtures/abort.json +711 -0
- package/proof/oracle/test/fixtures/bash-long-output.json +1312 -0
- package/proof/oracle/test/fixtures/compaction.json +6285 -0
- package/proof/oracle/test/fixtures/parallel-tools.json +2622 -0
- package/proof/oracle/test/fixtures/plain-text.json +1856 -0
- package/proof/oracle/test/fixtures/single-tool.json +1128 -0
- package/proof/oracle/test/fixtures/steering.json +3926 -0
- package/proof/oracle/test/fixtures/tool-bash.json +2372 -0
- package/proof/oracle/test/fixtures/tool-edit.json +3983 -0
- package/proof/oracle/test/fixtures/tool-error.json +2850 -0
- package/proof/oracle/test/fixtures/tool-find.json +1162 -0
- package/proof/oracle/test/fixtures/tool-grep.json +2783 -0
- package/proof/oracle/test/fixtures/tool-loop.json +1693 -0
- package/proof/oracle/test/fixtures/tool-ls.json +2427 -0
- package/proof/oracle/test/fixtures/tool-write.json +1271 -0
- package/proof/oracle/test/handshake.test.ts +275 -0
- package/proof/oracle/test/harness.ts +153 -0
- package/proof/oracle/test/hydrated-session-lifecycle.test.ts +211 -0
- package/proof/oracle/test/image-input.test.ts +28 -0
- package/proof/oracle/test/image-session.test.ts +69 -0
- package/proof/oracle/test/live-turn.test.ts +304 -0
- package/proof/oracle/test/mapper-fixtures.test.ts +293 -0
- package/proof/oracle/test/message-input.test.ts +181 -0
- package/proof/oracle/test/model-discovery.test.ts +170 -0
- package/proof/oracle/test/models.test.ts +109 -0
- package/proof/oracle/test/pi-host.test.ts +91 -0
- package/proof/oracle/test/pi-replay.test.ts +141 -0
- package/proof/oracle/test/project-trust.test.ts +125 -0
- package/proof/oracle/test/protocol-surface.test.ts +121 -0
- package/proof/oracle/test/pty.test.ts +100 -0
- package/proof/oracle/test/reconnect.test.ts +389 -0
- package/proof/oracle/test/resource-watch-policy.test.ts +78 -0
- package/proof/oracle/test/resource-watch.test.serial.ts +536 -0
- package/proof/oracle/test/resource.test.ts +499 -0
- package/proof/oracle/test/schema.test.ts +88 -0
- package/proof/oracle/test/session-catalogue.test.ts +314 -0
- package/proof/oracle/test/session-config.test.ts +224 -0
- package/proof/oracle/test/session-disposal.test.ts +364 -0
- package/proof/oracle/test/session-hydration.test.ts +194 -0
- package/proof/oracle/test/session-lifecycle.test.ts +416 -0
- package/proof/oracle/test/session-storage.test.serial.ts +32 -0
- package/proof/oracle/test/session-summary.test.ts +418 -0
- package/proof/oracle/test/subscriptions.test.ts +198 -0
- package/proof/oracle/test/support/assertions.ts +32 -0
- package/proof/oracle/test/support/async.ts +26 -0
- package/proof/oracle/test/support/hydrated-session.ts +205 -0
- package/proof/oracle/test/support/images.ts +8 -0
- package/proof/oracle/test/support/recorded-fixtures.ts +60 -0
- package/proof/oracle/test/support/recorded-scenarios.ts +145 -0
- package/proof/oracle/test/support/replay.ts +194 -0
- package/proof/oracle/test/support/schema.ts +221 -0
- package/proof/oracle/test/support/session-files.ts +12 -0
- package/proof/oracle/test/support/session-storage.ts +22 -0
- package/proof/oracle/test/support/upstream.ts +25 -0
- package/proof/oracle/test/support/watch-events.ts +108 -0
- package/proof/oracle/test/terminal-service.test.ts +495 -0
- package/proof/oracle/test/truncate.test.ts +230 -0
- package/proof/oracle/test/tunnel.test.ts +258 -0
- package/proof/oracle/test/upstream-workarounds.test.ts +37 -0
- package/proof/oracle/test/uri.test.ts +24 -0
- package/proof/oracle/test/watch-events.test.ts +118 -0
- package/proof/oracle/tsconfig.json +32 -0
- package/proof/proof-piglet/piglet.yaml +18 -0
- package/proof/tools/fakellm/go.mod +3 -0
- package/proof/tools/fakellm/main.go +109 -0
- package/proof/tools/gen-dispatchable.py +30 -0
- package/provenance.json +28 -0
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
// Package ws is a minimal RFC 6455 WebSocket transport for the AHP host: a server (HTTP upgrade
|
|
2
|
+
// with token and Origin checks, text frames, fragmentation, ping/pong, close, size limits, a
|
|
3
|
+
// bounded outbound queue) and a small client used by tests. It replaces pi-ahp's
|
|
4
|
+
// src/transport/websocket.ts, which uses the `ws` package, so the extension needs no third-party
|
|
5
|
+
// module. AHP sends each JSON-RPC message as one text frame.
|
|
6
|
+
package ws
|
|
7
|
+
|
|
8
|
+
import (
|
|
9
|
+
"bufio"
|
|
10
|
+
"context"
|
|
11
|
+
"crypto/sha1"
|
|
12
|
+
"crypto/subtle"
|
|
13
|
+
"encoding/base64"
|
|
14
|
+
"encoding/binary"
|
|
15
|
+
"errors"
|
|
16
|
+
"fmt"
|
|
17
|
+
"io"
|
|
18
|
+
"net"
|
|
19
|
+
"net/http"
|
|
20
|
+
"net/url"
|
|
21
|
+
"strings"
|
|
22
|
+
"sync"
|
|
23
|
+
"time"
|
|
24
|
+
"unicode/utf8"
|
|
25
|
+
|
|
26
|
+
"github.com/MichaelKinsy/pigpen/ahp/internal/host"
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
const guid = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
|
|
30
|
+
|
|
31
|
+
const (
|
|
32
|
+
opContinuation = 0x0
|
|
33
|
+
opText = 0x1
|
|
34
|
+
opBinary = 0x2
|
|
35
|
+
opClose = 0x8
|
|
36
|
+
opPing = 0x9
|
|
37
|
+
opPong = 0xA
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
// Close codes (RFC 6455 section 7.4).
|
|
41
|
+
const (
|
|
42
|
+
closeNormal = 1000
|
|
43
|
+
closeGoingAway = 1001
|
|
44
|
+
closeProtocolError = 1002
|
|
45
|
+
closeInvalidData = 1007
|
|
46
|
+
closeTooBig = 1009
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
// Options configure a Server.
|
|
50
|
+
type Options struct {
|
|
51
|
+
// Addr is the listen address, "127.0.0.1:0" for an ephemeral loopback port.
|
|
52
|
+
Addr string
|
|
53
|
+
// Token, when non-empty, must be presented as ?token= or ?tkn= (VS Code's spelling).
|
|
54
|
+
Token string
|
|
55
|
+
// AllowedOrigins lists browser Origins that may connect. A request with an Origin header that is
|
|
56
|
+
// not listed is refused; a request without one (native clients) is allowed.
|
|
57
|
+
AllowedOrigins []string
|
|
58
|
+
// MaxMessageBytes bounds one message (0: 16 MiB).
|
|
59
|
+
MaxMessageBytes int
|
|
60
|
+
// MaxQueuedFrames bounds the outbound queue of one peer (0: 4096); a peer that falls behind is closed.
|
|
61
|
+
MaxQueuedFrames int
|
|
62
|
+
// PingInterval is the keep-alive interval (0: 30 s); a peer that says nothing for two intervals is closed.
|
|
63
|
+
PingInterval time.Duration
|
|
64
|
+
Log func(string)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
func (o *Options) defaults() {
|
|
68
|
+
if o.MaxMessageBytes <= 0 {
|
|
69
|
+
o.MaxMessageBytes = 16 << 20
|
|
70
|
+
}
|
|
71
|
+
if o.MaxQueuedFrames <= 0 {
|
|
72
|
+
o.MaxQueuedFrames = 4096
|
|
73
|
+
}
|
|
74
|
+
if o.PingInterval <= 0 {
|
|
75
|
+
o.PingInterval = 30 * time.Second
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// AcceptKey computes Sec-WebSocket-Accept for a client key.
|
|
80
|
+
func AcceptKey(key string) string {
|
|
81
|
+
sum := sha1.Sum([]byte(key + guid))
|
|
82
|
+
return base64.StdEncoding.EncodeToString(sum[:])
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Server accepts WebSocket connections and hands each to the host.
|
|
86
|
+
type Server struct {
|
|
87
|
+
h *host.Host
|
|
88
|
+
opts Options
|
|
89
|
+
ln net.Listener
|
|
90
|
+
srv *http.Server
|
|
91
|
+
|
|
92
|
+
mu sync.Mutex
|
|
93
|
+
conns map[*serverConn]struct{}
|
|
94
|
+
closed bool
|
|
95
|
+
wg sync.WaitGroup
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Serve starts listening and wires each connection into the host.
|
|
99
|
+
func Serve(h *host.Host, opts Options) (*Server, error) {
|
|
100
|
+
opts.defaults()
|
|
101
|
+
ln, err := net.Listen("tcp", opts.Addr)
|
|
102
|
+
if err != nil {
|
|
103
|
+
return nil, err
|
|
104
|
+
}
|
|
105
|
+
s := &Server{h: h, opts: opts, ln: ln, conns: map[*serverConn]struct{}{}}
|
|
106
|
+
s.srv = &http.Server{Handler: http.HandlerFunc(s.handle), ReadHeaderTimeout: 10 * time.Second}
|
|
107
|
+
s.wg.Add(1)
|
|
108
|
+
go func() {
|
|
109
|
+
defer s.wg.Done()
|
|
110
|
+
_ = s.srv.Serve(ln)
|
|
111
|
+
}()
|
|
112
|
+
return s, nil
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Addr is the bound address.
|
|
116
|
+
func (s *Server) Addr() net.Addr { return s.ln.Addr() }
|
|
117
|
+
|
|
118
|
+
// Port is the bound port.
|
|
119
|
+
func (s *Server) Port() int { return s.ln.Addr().(*net.TCPAddr).Port }
|
|
120
|
+
|
|
121
|
+
// Close stops listening and closes every connection.
|
|
122
|
+
func (s *Server) Close() error {
|
|
123
|
+
s.mu.Lock()
|
|
124
|
+
if s.closed {
|
|
125
|
+
s.mu.Unlock()
|
|
126
|
+
return nil
|
|
127
|
+
}
|
|
128
|
+
s.closed = true
|
|
129
|
+
conns := make([]*serverConn, 0, len(s.conns))
|
|
130
|
+
for c := range s.conns {
|
|
131
|
+
conns = append(conns, c)
|
|
132
|
+
}
|
|
133
|
+
s.mu.Unlock()
|
|
134
|
+
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
135
|
+
defer cancel()
|
|
136
|
+
_ = s.srv.Shutdown(ctx) // hijacked connections are not tracked by http.Server
|
|
137
|
+
_ = s.ln.Close()
|
|
138
|
+
for _, c := range conns {
|
|
139
|
+
c.shutdown(closeGoingAway, "server closing")
|
|
140
|
+
}
|
|
141
|
+
s.wg.Wait()
|
|
142
|
+
return nil
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
func (s *Server) logf(format string, args ...any) {
|
|
146
|
+
if s.opts.Log != nil {
|
|
147
|
+
s.opts.Log(fmt.Sprintf(format, args...))
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
func headerHasToken(h http.Header, name, token string) bool {
|
|
152
|
+
for _, v := range h.Values(name) {
|
|
153
|
+
for _, part := range strings.Split(v, ",") {
|
|
154
|
+
if strings.EqualFold(strings.TrimSpace(part), token) {
|
|
155
|
+
return true
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return false
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
func (s *Server) originAllowed(origin string) bool {
|
|
163
|
+
for _, allowed := range s.opts.AllowedOrigins {
|
|
164
|
+
if strings.EqualFold(allowed, origin) {
|
|
165
|
+
return true
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return false
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
func (s *Server) handle(w http.ResponseWriter, r *http.Request) {
|
|
172
|
+
if !headerHasToken(r.Header, "Upgrade", "websocket") {
|
|
173
|
+
w.Header().Set("Content-Type", "text/plain")
|
|
174
|
+
w.WriteHeader(http.StatusUpgradeRequired)
|
|
175
|
+
_, _ = io.WriteString(w, "Upgrade required")
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
if s.opts.Token != "" {
|
|
179
|
+
q := r.URL.Query()
|
|
180
|
+
// VS Code calls a manually configured connection token `tkn`; generic AHP examples use
|
|
181
|
+
// `token`, so a protected direct listener accepts both.
|
|
182
|
+
presented, ok := "", false
|
|
183
|
+
if vals, has := q["token"]; has {
|
|
184
|
+
presented, ok = vals[0], true
|
|
185
|
+
} else if vals, has := q["tkn"]; has {
|
|
186
|
+
presented, ok = vals[0], true
|
|
187
|
+
}
|
|
188
|
+
if !ok || subtle.ConstantTimeCompare([]byte(presented), []byte(s.opts.Token)) != 1 {
|
|
189
|
+
// 403, as VS Code's agent host (the AHP reference host) answers a missing or wrong tkn; pi-ahp writes 401.
|
|
190
|
+
// A 401 would also owe a WWW-Authenticate challenge, which a query token has no scheme for.
|
|
191
|
+
s.logf("rejecting upgrade: bad token")
|
|
192
|
+
w.WriteHeader(http.StatusForbidden)
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if origin := r.Header.Get("Origin"); origin != "" && !s.originAllowed(origin) {
|
|
197
|
+
s.logf("rejecting upgrade: origin %q is not allowed", origin)
|
|
198
|
+
w.WriteHeader(http.StatusForbidden)
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
key := r.Header.Get("Sec-WebSocket-Key")
|
|
202
|
+
if r.Method != http.MethodGet || !headerHasToken(r.Header, "Connection", "upgrade") ||
|
|
203
|
+
r.Header.Get("Sec-WebSocket-Version") != "13" || !validKey(key) {
|
|
204
|
+
w.Header().Set("Sec-WebSocket-Version", "13")
|
|
205
|
+
w.WriteHeader(http.StatusBadRequest)
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
hj, ok := w.(http.Hijacker)
|
|
209
|
+
if !ok {
|
|
210
|
+
w.WriteHeader(http.StatusInternalServerError)
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
conn, brw, err := hj.Hijack()
|
|
214
|
+
if err != nil {
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
if _, err := fmt.Fprintf(brw, "HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: %s\r\n\r\n", AcceptKey(key)); err != nil || brw.Flush() != nil {
|
|
218
|
+
conn.Close()
|
|
219
|
+
return
|
|
220
|
+
}
|
|
221
|
+
s.mu.Lock()
|
|
222
|
+
if s.closed {
|
|
223
|
+
s.mu.Unlock()
|
|
224
|
+
conn.Close()
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
sc := newServerConn(s, conn, brw.Reader)
|
|
228
|
+
s.conns[sc] = struct{}{}
|
|
229
|
+
s.wg.Add(1)
|
|
230
|
+
s.mu.Unlock()
|
|
231
|
+
sc.hostConn = s.h.Accept(sc)
|
|
232
|
+
go sc.run()
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
func validKey(key string) bool {
|
|
236
|
+
raw, err := base64.StdEncoding.DecodeString(key)
|
|
237
|
+
return err == nil && len(raw) == 16
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── server connection ───────────────────────────────────────────────────
|
|
241
|
+
|
|
242
|
+
type outFrame struct {
|
|
243
|
+
opcode byte
|
|
244
|
+
payload []byte
|
|
245
|
+
close bool // the connection ends after this frame
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
type serverConn struct {
|
|
249
|
+
s *Server
|
|
250
|
+
conn net.Conn
|
|
251
|
+
br *bufio.Reader
|
|
252
|
+
hostConn *host.Conn
|
|
253
|
+
|
|
254
|
+
out chan outFrame // data frames
|
|
255
|
+
ctrl chan outFrame // control frames, sent ahead of data
|
|
256
|
+
done chan struct{}
|
|
257
|
+
stop chan struct{} // the reader is finished: flush control frames, then end the writer
|
|
258
|
+
doneOnce sync.Once
|
|
259
|
+
closing sync.Once
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
func newServerConn(s *Server, conn net.Conn, br *bufio.Reader) *serverConn {
|
|
263
|
+
return &serverConn{
|
|
264
|
+
s: s, conn: conn, br: br,
|
|
265
|
+
out: make(chan outFrame, s.opts.MaxQueuedFrames),
|
|
266
|
+
ctrl: make(chan outFrame, 16),
|
|
267
|
+
done: make(chan struct{}),
|
|
268
|
+
stop: make(chan struct{}),
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Send implements host.Transport. It never blocks: a peer whose queue is full is closed, which is
|
|
273
|
+
// safe because AHP clients recover through reconnect.
|
|
274
|
+
func (c *serverConn) Send(frame []byte) error {
|
|
275
|
+
select {
|
|
276
|
+
case <-c.done:
|
|
277
|
+
return errors.New("connection closed")
|
|
278
|
+
default:
|
|
279
|
+
}
|
|
280
|
+
select {
|
|
281
|
+
case c.out <- outFrame{opcode: opText, payload: frame}:
|
|
282
|
+
return nil
|
|
283
|
+
default:
|
|
284
|
+
c.s.logf("closing a peer that stopped reading (queue of %d frames full)", cap(c.out))
|
|
285
|
+
go c.shutdown(closeGoingAway, "too slow")
|
|
286
|
+
return errors.New("outbound queue full")
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Close implements host.Transport.
|
|
291
|
+
func (c *serverConn) Close() error {
|
|
292
|
+
go c.shutdown(closeNormal, "")
|
|
293
|
+
return nil
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
func closePayload(code uint16, reason string) []byte {
|
|
297
|
+
p := make([]byte, 2, 2+len(reason))
|
|
298
|
+
binary.BigEndian.PutUint16(p, code)
|
|
299
|
+
if len(reason) > 123 {
|
|
300
|
+
reason = reason[:123]
|
|
301
|
+
}
|
|
302
|
+
return append(p, reason...)
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// shutdown sends a close frame, gives the peer a moment to answer, then drops the connection.
|
|
306
|
+
func (c *serverConn) shutdown(code uint16, reason string) {
|
|
307
|
+
c.closing.Do(func() {
|
|
308
|
+
select {
|
|
309
|
+
case c.ctrl <- outFrame{opcode: opClose, payload: closePayload(code, reason), close: true}:
|
|
310
|
+
case <-c.done:
|
|
311
|
+
return
|
|
312
|
+
}
|
|
313
|
+
select {
|
|
314
|
+
case <-c.done:
|
|
315
|
+
case <-time.After(time.Second):
|
|
316
|
+
c.finish()
|
|
317
|
+
}
|
|
318
|
+
})
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
func (c *serverConn) finish() {
|
|
322
|
+
c.doneOnce.Do(func() {
|
|
323
|
+
close(c.done)
|
|
324
|
+
_ = c.conn.Close()
|
|
325
|
+
})
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
func (c *serverConn) run() {
|
|
329
|
+
defer c.s.wg.Done()
|
|
330
|
+
writerDone := make(chan struct{})
|
|
331
|
+
go func() {
|
|
332
|
+
defer close(writerDone)
|
|
333
|
+
c.writeLoop()
|
|
334
|
+
}()
|
|
335
|
+
c.readLoop()
|
|
336
|
+
close(c.stop)
|
|
337
|
+
select {
|
|
338
|
+
case <-writerDone:
|
|
339
|
+
case <-time.After(2 * time.Second):
|
|
340
|
+
}
|
|
341
|
+
c.finish()
|
|
342
|
+
<-writerDone
|
|
343
|
+
c.s.mu.Lock()
|
|
344
|
+
delete(c.s.conns, c)
|
|
345
|
+
c.s.mu.Unlock()
|
|
346
|
+
c.hostConn.Closed()
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
func (c *serverConn) writeLoop() {
|
|
350
|
+
ping := time.NewTicker(c.s.opts.PingInterval)
|
|
351
|
+
defer ping.Stop()
|
|
352
|
+
write := func(f outFrame) bool {
|
|
353
|
+
_ = c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
|
|
354
|
+
if err := writeFrame(c.conn, f.opcode, f.payload, false); err != nil {
|
|
355
|
+
c.finish()
|
|
356
|
+
return false
|
|
357
|
+
}
|
|
358
|
+
if f.close {
|
|
359
|
+
// Closing handshake initiated by us: leave the peer time to echo; the reader ends on EOF.
|
|
360
|
+
return true
|
|
361
|
+
}
|
|
362
|
+
return true
|
|
363
|
+
}
|
|
364
|
+
for {
|
|
365
|
+
// Control frames first.
|
|
366
|
+
select {
|
|
367
|
+
case f := <-c.ctrl:
|
|
368
|
+
if !write(f) {
|
|
369
|
+
return
|
|
370
|
+
}
|
|
371
|
+
continue
|
|
372
|
+
default:
|
|
373
|
+
}
|
|
374
|
+
select {
|
|
375
|
+
case f := <-c.ctrl:
|
|
376
|
+
if !write(f) {
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
case f := <-c.out:
|
|
380
|
+
if !write(f) {
|
|
381
|
+
return
|
|
382
|
+
}
|
|
383
|
+
case <-ping.C:
|
|
384
|
+
if !write(outFrame{opcode: opPing}) {
|
|
385
|
+
return
|
|
386
|
+
}
|
|
387
|
+
case <-c.stop:
|
|
388
|
+
// Flush what the reader queued (the echo of a close, a protocol-error close), then end.
|
|
389
|
+
for {
|
|
390
|
+
select {
|
|
391
|
+
case f := <-c.ctrl:
|
|
392
|
+
if !write(f) {
|
|
393
|
+
return
|
|
394
|
+
}
|
|
395
|
+
default:
|
|
396
|
+
return
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
case <-c.done:
|
|
400
|
+
return
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
type frameHeader struct {
|
|
406
|
+
fin bool
|
|
407
|
+
rsv byte
|
|
408
|
+
opcode byte
|
|
409
|
+
masked bool
|
|
410
|
+
length uint64
|
|
411
|
+
maskKey [4]byte
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
func readHeader(r io.Reader) (frameHeader, error) {
|
|
415
|
+
var h frameHeader
|
|
416
|
+
var b [2]byte
|
|
417
|
+
if _, err := io.ReadFull(r, b[:]); err != nil {
|
|
418
|
+
return h, err
|
|
419
|
+
}
|
|
420
|
+
h.fin = b[0]&0x80 != 0
|
|
421
|
+
h.rsv = (b[0] >> 4) & 0x7
|
|
422
|
+
h.opcode = b[0] & 0x0f
|
|
423
|
+
h.masked = b[1]&0x80 != 0
|
|
424
|
+
h.length = uint64(b[1] & 0x7f)
|
|
425
|
+
switch h.length {
|
|
426
|
+
case 126:
|
|
427
|
+
var e [2]byte
|
|
428
|
+
if _, err := io.ReadFull(r, e[:]); err != nil {
|
|
429
|
+
return h, err
|
|
430
|
+
}
|
|
431
|
+
h.length = uint64(binary.BigEndian.Uint16(e[:]))
|
|
432
|
+
case 127:
|
|
433
|
+
var e [8]byte
|
|
434
|
+
if _, err := io.ReadFull(r, e[:]); err != nil {
|
|
435
|
+
return h, err
|
|
436
|
+
}
|
|
437
|
+
h.length = binary.BigEndian.Uint64(e[:])
|
|
438
|
+
if h.length>>63 != 0 {
|
|
439
|
+
return h, errors.New("frame length has the high bit set")
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
if h.masked {
|
|
443
|
+
if _, err := io.ReadFull(r, h.maskKey[:]); err != nil {
|
|
444
|
+
return h, err
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
return h, nil
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
func writeFrame(w io.Writer, opcode byte, payload []byte, mask bool) error {
|
|
451
|
+
hdr := []byte{0x80 | opcode}
|
|
452
|
+
maskBit := byte(0)
|
|
453
|
+
if mask {
|
|
454
|
+
maskBit = 0x80
|
|
455
|
+
}
|
|
456
|
+
switch n := len(payload); {
|
|
457
|
+
case n < 126:
|
|
458
|
+
hdr = append(hdr, maskBit|byte(n))
|
|
459
|
+
case n <= 0xffff:
|
|
460
|
+
hdr = append(hdr, maskBit|126, byte(n>>8), byte(n))
|
|
461
|
+
default:
|
|
462
|
+
var l [8]byte
|
|
463
|
+
binary.BigEndian.PutUint64(l[:], uint64(n))
|
|
464
|
+
hdr = append(hdr, maskBit|127)
|
|
465
|
+
hdr = append(hdr, l[:]...)
|
|
466
|
+
}
|
|
467
|
+
body := payload
|
|
468
|
+
if mask {
|
|
469
|
+
key := [4]byte{byte(time.Now().UnixNano()), 0x37, 0xfa, 0x21}
|
|
470
|
+
hdr = append(hdr, key[:]...)
|
|
471
|
+
body = make([]byte, len(payload))
|
|
472
|
+
for i, b := range payload {
|
|
473
|
+
body[i] = b ^ key[i%4]
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
_, err := w.Write(append(hdr, body...))
|
|
477
|
+
return err
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// readMessage reads one full data message (reassembling fragments) and handles control frames
|
|
481
|
+
// through onControl. requireMask is true on the server side.
|
|
482
|
+
func readMessage(r *bufio.Reader, limit int, requireMask bool, onControl func(op byte, payload []byte) error) (opcode byte, msg []byte, code uint16, err error) {
|
|
483
|
+
var buf []byte
|
|
484
|
+
var first byte
|
|
485
|
+
inMessage := false
|
|
486
|
+
for {
|
|
487
|
+
h, err := readHeader(r)
|
|
488
|
+
if err != nil {
|
|
489
|
+
return 0, nil, 0, err
|
|
490
|
+
}
|
|
491
|
+
if h.rsv != 0 {
|
|
492
|
+
return 0, nil, closeProtocolError, errors.New("reserved bits set")
|
|
493
|
+
}
|
|
494
|
+
if requireMask && !h.masked {
|
|
495
|
+
return 0, nil, closeProtocolError, errors.New("client frames must be masked")
|
|
496
|
+
}
|
|
497
|
+
isControl := h.opcode&0x8 != 0
|
|
498
|
+
if isControl && (!h.fin || h.length > 125) {
|
|
499
|
+
return 0, nil, closeProtocolError, errors.New("invalid control frame")
|
|
500
|
+
}
|
|
501
|
+
switch h.opcode {
|
|
502
|
+
case opContinuation:
|
|
503
|
+
if !inMessage {
|
|
504
|
+
return 0, nil, closeProtocolError, errors.New("unexpected continuation frame")
|
|
505
|
+
}
|
|
506
|
+
case opText, opBinary:
|
|
507
|
+
if inMessage {
|
|
508
|
+
return 0, nil, closeProtocolError, errors.New("data frame inside a fragmented message")
|
|
509
|
+
}
|
|
510
|
+
case opClose, opPing, opPong:
|
|
511
|
+
default:
|
|
512
|
+
return 0, nil, closeProtocolError, fmt.Errorf("reserved opcode %d", h.opcode)
|
|
513
|
+
}
|
|
514
|
+
if !isControl && h.length > uint64(limit-len(buf)) {
|
|
515
|
+
return 0, nil, closeTooBig, errors.New("message too big")
|
|
516
|
+
}
|
|
517
|
+
payload := make([]byte, h.length)
|
|
518
|
+
if _, err := io.ReadFull(r, payload); err != nil {
|
|
519
|
+
return 0, nil, 0, err
|
|
520
|
+
}
|
|
521
|
+
if h.masked {
|
|
522
|
+
for i := range payload {
|
|
523
|
+
payload[i] ^= h.maskKey[i%4]
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if isControl {
|
|
527
|
+
if err := onControl(h.opcode, payload); err != nil {
|
|
528
|
+
return h.opcode, nil, 0, err
|
|
529
|
+
}
|
|
530
|
+
continue
|
|
531
|
+
}
|
|
532
|
+
if !inMessage {
|
|
533
|
+
first = h.opcode
|
|
534
|
+
inMessage = true
|
|
535
|
+
}
|
|
536
|
+
buf = append(buf, payload...)
|
|
537
|
+
if h.fin {
|
|
538
|
+
if first == opText && !utf8.Valid(buf) {
|
|
539
|
+
return 0, nil, closeInvalidData, errors.New("invalid UTF-8 in a text message")
|
|
540
|
+
}
|
|
541
|
+
return first, buf, 0, nil
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
var errPeerClosed = errors.New("peer closed the connection")
|
|
547
|
+
|
|
548
|
+
func (c *serverConn) readLoop() {
|
|
549
|
+
idle := 2 * c.s.opts.PingInterval
|
|
550
|
+
for {
|
|
551
|
+
_ = c.conn.SetReadDeadline(time.Now().Add(idle))
|
|
552
|
+
_, msg, code, err := readMessage(c.br, c.s.opts.MaxMessageBytes, true, func(op byte, payload []byte) error {
|
|
553
|
+
_ = c.conn.SetReadDeadline(time.Now().Add(idle))
|
|
554
|
+
switch op {
|
|
555
|
+
case opPing:
|
|
556
|
+
select {
|
|
557
|
+
case c.ctrl <- outFrame{opcode: opPong, payload: payload}:
|
|
558
|
+
default:
|
|
559
|
+
}
|
|
560
|
+
case opClose:
|
|
561
|
+
echo := closePayload(closeNormal, "")
|
|
562
|
+
if len(payload) >= 2 {
|
|
563
|
+
echo = payload[:2]
|
|
564
|
+
}
|
|
565
|
+
c.closing.Do(func() {}) // the peer initiated: do not start our own handshake
|
|
566
|
+
select {
|
|
567
|
+
case c.ctrl <- outFrame{opcode: opClose, payload: echo, close: true}:
|
|
568
|
+
default:
|
|
569
|
+
}
|
|
570
|
+
return errPeerClosed
|
|
571
|
+
}
|
|
572
|
+
return nil
|
|
573
|
+
})
|
|
574
|
+
if err != nil {
|
|
575
|
+
if code != 0 {
|
|
576
|
+
c.closing.Do(func() {})
|
|
577
|
+
select {
|
|
578
|
+
case c.ctrl <- outFrame{opcode: opClose, payload: closePayload(code, ""), close: true}:
|
|
579
|
+
default:
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return
|
|
583
|
+
}
|
|
584
|
+
c.hostConn.Receive(msg)
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// ── client (tests and interop) ──────────────────────────────────────────
|
|
589
|
+
|
|
590
|
+
// Client is a WebSocket client connection.
|
|
591
|
+
type Client struct {
|
|
592
|
+
conn net.Conn
|
|
593
|
+
br *bufio.Reader
|
|
594
|
+
wmu sync.Mutex
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// Dial connects to a ws:// URL. On a refused upgrade it returns the HTTP response.
|
|
598
|
+
func Dial(rawurl string, header http.Header) (*Client, *http.Response, error) {
|
|
599
|
+
u, err := url.Parse(rawurl)
|
|
600
|
+
if err != nil {
|
|
601
|
+
return nil, nil, err
|
|
602
|
+
}
|
|
603
|
+
if u.Scheme != "ws" {
|
|
604
|
+
return nil, nil, fmt.Errorf("ws: unsupported scheme %q", u.Scheme)
|
|
605
|
+
}
|
|
606
|
+
conn, err := net.DialTimeout("tcp", u.Host, 5*time.Second)
|
|
607
|
+
if err != nil {
|
|
608
|
+
return nil, nil, err
|
|
609
|
+
}
|
|
610
|
+
rawKey := make([]byte, 16)
|
|
611
|
+
for i := range rawKey {
|
|
612
|
+
rawKey[i] = byte(time.Now().UnixNano()>>uint(i)) ^ byte(i*31+7)
|
|
613
|
+
}
|
|
614
|
+
key := base64.StdEncoding.EncodeToString(rawKey)
|
|
615
|
+
path := u.RequestURI()
|
|
616
|
+
var sb strings.Builder
|
|
617
|
+
fmt.Fprintf(&sb, "GET %s HTTP/1.1\r\nHost: %s\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: %s\r\nSec-WebSocket-Version: 13\r\n", path, u.Host, key)
|
|
618
|
+
for k, vs := range header {
|
|
619
|
+
for _, v := range vs {
|
|
620
|
+
fmt.Fprintf(&sb, "%s: %s\r\n", k, v)
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
sb.WriteString("\r\n")
|
|
624
|
+
if _, err := io.WriteString(conn, sb.String()); err != nil {
|
|
625
|
+
conn.Close()
|
|
626
|
+
return nil, nil, err
|
|
627
|
+
}
|
|
628
|
+
br := bufio.NewReader(conn)
|
|
629
|
+
resp, err := http.ReadResponse(br, &http.Request{Method: http.MethodGet})
|
|
630
|
+
if err != nil {
|
|
631
|
+
conn.Close()
|
|
632
|
+
return nil, nil, err
|
|
633
|
+
}
|
|
634
|
+
if resp.StatusCode != http.StatusSwitchingProtocols || resp.Header.Get("Sec-WebSocket-Accept") != AcceptKey(key) {
|
|
635
|
+
conn.Close()
|
|
636
|
+
return nil, resp, fmt.Errorf("ws: upgrade refused: %s", resp.Status)
|
|
637
|
+
}
|
|
638
|
+
return &Client{conn: conn, br: br}, resp, nil
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// WriteText sends one text message.
|
|
642
|
+
func (c *Client) WriteText(msg []byte) error {
|
|
643
|
+
c.wmu.Lock()
|
|
644
|
+
defer c.wmu.Unlock()
|
|
645
|
+
return writeFrame(c.conn, opText, msg, true)
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// ReadMessage returns the next data message (text or binary), answering pings.
|
|
649
|
+
func (c *Client) ReadMessage(timeout time.Duration) ([]byte, error) {
|
|
650
|
+
_ = c.conn.SetReadDeadline(time.Now().Add(timeout))
|
|
651
|
+
_, msg, _, err := readMessage(c.br, 64<<20, false, func(op byte, payload []byte) error {
|
|
652
|
+
switch op {
|
|
653
|
+
case opPing:
|
|
654
|
+
c.wmu.Lock()
|
|
655
|
+
defer c.wmu.Unlock()
|
|
656
|
+
return writeFrame(c.conn, opPong, payload, true)
|
|
657
|
+
case opClose:
|
|
658
|
+
c.wmu.Lock()
|
|
659
|
+
_ = writeFrame(c.conn, opClose, payload, true)
|
|
660
|
+
c.wmu.Unlock()
|
|
661
|
+
return io.EOF
|
|
662
|
+
}
|
|
663
|
+
return nil
|
|
664
|
+
})
|
|
665
|
+
return msg, err
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Close sends a close frame and closes the connection.
|
|
669
|
+
func (c *Client) Close() error {
|
|
670
|
+
c.wmu.Lock()
|
|
671
|
+
_ = writeFrame(c.conn, opClose, closePayload(closeNormal, ""), true)
|
|
672
|
+
c.wmu.Unlock()
|
|
673
|
+
return c.conn.Close()
|
|
674
|
+
}
|