@blamejs/core 0.8.42 → 0.8.49
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/CHANGELOG.md +93 -0
- package/README.md +10 -10
- package/index.js +52 -0
- package/lib/a2a.js +159 -34
- package/lib/acme.js +762 -0
- package/lib/ai-pref.js +166 -43
- package/lib/api-key.js +108 -47
- package/lib/api-snapshot.js +157 -40
- package/lib/app-shutdown.js +113 -77
- package/lib/archive.js +337 -40
- package/lib/arg-parser.js +697 -0
- package/lib/asyncapi.js +99 -55
- package/lib/atomic-file.js +465 -104
- package/lib/audit-chain.js +123 -34
- package/lib/audit-daily-review.js +389 -0
- package/lib/audit-sign.js +302 -56
- package/lib/audit-tools.js +412 -63
- package/lib/audit.js +656 -35
- package/lib/auth/jwt-external.js +17 -0
- package/lib/auth/oauth.js +7 -0
- package/lib/auth-bot-challenge.js +505 -0
- package/lib/auth-header.js +92 -25
- package/lib/backup/bundle.js +26 -0
- package/lib/backup/index.js +512 -89
- package/lib/backup/manifest.js +168 -7
- package/lib/break-glass.js +415 -39
- package/lib/budr.js +103 -30
- package/lib/bundler.js +86 -66
- package/lib/cache.js +192 -72
- package/lib/chain-writer.js +65 -40
- package/lib/circuit-breaker.js +56 -33
- package/lib/cli-helpers.js +106 -75
- package/lib/cli.js +6 -30
- package/lib/cloud-events.js +99 -32
- package/lib/cluster-storage.js +162 -37
- package/lib/cluster.js +340 -49
- package/lib/codepoint-class.js +66 -0
- package/lib/compliance.js +424 -24
- package/lib/config-drift.js +111 -46
- package/lib/config.js +94 -40
- package/lib/consent.js +165 -18
- package/lib/constants.js +1 -0
- package/lib/content-credentials.js +153 -48
- package/lib/cookies.js +154 -62
- package/lib/credential-hash.js +133 -61
- package/lib/crypto-field.js +702 -18
- package/lib/crypto-hpke.js +256 -0
- package/lib/crypto.js +744 -22
- package/lib/csv.js +178 -35
- package/lib/daemon.js +456 -0
- package/lib/dark-patterns.js +186 -55
- package/lib/db-query.js +79 -2
- package/lib/db.js +1431 -60
- package/lib/ddl-change-control.js +523 -0
- package/lib/deprecate.js +195 -40
- package/lib/dev.js +82 -39
- package/lib/dora.js +67 -48
- package/lib/dr-runbook.js +368 -0
- package/lib/dsr.js +142 -11
- package/lib/dual-control.js +91 -56
- package/lib/events.js +120 -41
- package/lib/external-db-migrate.js +192 -2
- package/lib/external-db.js +795 -50
- package/lib/fapi2.js +122 -1
- package/lib/fda-21cfr11.js +395 -0
- package/lib/fdx.js +132 -2
- package/lib/file-type.js +87 -0
- package/lib/file-upload.js +93 -0
- package/lib/flag.js +82 -20
- package/lib/forms.js +132 -29
- package/lib/framework-error.js +169 -0
- package/lib/framework-schema.js +163 -35
- package/lib/gate-contract.js +849 -175
- package/lib/graphql-federation.js +68 -7
- package/lib/guard-all.js +172 -55
- package/lib/guard-archive.js +286 -124
- package/lib/guard-auth.js +194 -21
- package/lib/guard-cidr.js +190 -28
- package/lib/guard-csv.js +397 -51
- package/lib/guard-domain.js +213 -91
- package/lib/guard-email.js +236 -29
- package/lib/guard-filename.js +307 -75
- package/lib/guard-graphql.js +263 -30
- package/lib/guard-html.js +310 -116
- package/lib/guard-image.js +243 -30
- package/lib/guard-json.js +260 -54
- package/lib/guard-jsonpath.js +235 -23
- package/lib/guard-jwt.js +284 -30
- package/lib/guard-markdown.js +204 -22
- package/lib/guard-mime.js +190 -26
- package/lib/guard-oauth.js +277 -28
- package/lib/guard-pdf.js +251 -27
- package/lib/guard-regex.js +226 -18
- package/lib/guard-shell.js +229 -26
- package/lib/guard-svg.js +177 -10
- package/lib/guard-template.js +232 -21
- package/lib/guard-time.js +195 -29
- package/lib/guard-uuid.js +189 -30
- package/lib/guard-xml.js +259 -36
- package/lib/guard-yaml.js +241 -44
- package/lib/honeytoken.js +63 -27
- package/lib/html-balance.js +83 -0
- package/lib/http-client.js +486 -59
- package/lib/http-message-signature.js +582 -0
- package/lib/i18n.js +102 -49
- package/lib/iab-mspa.js +112 -32
- package/lib/iab-tcf.js +107 -2
- package/lib/inbox.js +90 -52
- package/lib/keychain.js +865 -0
- package/lib/legal-hold.js +374 -0
- package/lib/local-db-thin.js +320 -0
- package/lib/log-stream.js +281 -51
- package/lib/log.js +184 -86
- package/lib/mail-bounce.js +107 -62
- package/lib/mail.js +295 -58
- package/lib/mcp.js +108 -27
- package/lib/metrics.js +98 -89
- package/lib/middleware/age-gate.js +36 -0
- package/lib/middleware/ai-act-disclosure.js +37 -0
- package/lib/middleware/api-encrypt.js +45 -0
- package/lib/middleware/assetlinks.js +40 -0
- package/lib/middleware/asyncapi-serve.js +35 -0
- package/lib/middleware/attach-user.js +40 -0
- package/lib/middleware/bearer-auth.js +40 -0
- package/lib/middleware/body-parser.js +230 -0
- package/lib/middleware/bot-disclose.js +34 -0
- package/lib/middleware/bot-guard.js +39 -0
- package/lib/middleware/compression.js +37 -0
- package/lib/middleware/cookies.js +32 -0
- package/lib/middleware/cors.js +40 -0
- package/lib/middleware/csp-nonce.js +40 -0
- package/lib/middleware/csp-report.js +34 -0
- package/lib/middleware/csrf-protect.js +43 -0
- package/lib/middleware/daily-byte-quota.js +53 -85
- package/lib/middleware/db-role-for.js +40 -0
- package/lib/middleware/dpop.js +40 -0
- package/lib/middleware/error-handler.js +37 -14
- package/lib/middleware/fetch-metadata.js +39 -0
- package/lib/middleware/flag-context.js +34 -0
- package/lib/middleware/gpc.js +33 -0
- package/lib/middleware/headers.js +35 -0
- package/lib/middleware/health.js +46 -0
- package/lib/middleware/host-allowlist.js +30 -0
- package/lib/middleware/network-allowlist.js +38 -0
- package/lib/middleware/openapi-serve.js +34 -0
- package/lib/middleware/rate-limit.js +160 -18
- package/lib/middleware/request-id.js +36 -18
- package/lib/middleware/request-log.js +37 -0
- package/lib/middleware/require-aal.js +29 -0
- package/lib/middleware/require-auth.js +32 -0
- package/lib/middleware/require-bound-key.js +41 -0
- package/lib/middleware/require-content-type.js +32 -0
- package/lib/middleware/require-methods.js +27 -0
- package/lib/middleware/require-mtls.js +33 -0
- package/lib/middleware/require-step-up.js +37 -0
- package/lib/middleware/security-headers.js +44 -0
- package/lib/middleware/security-txt.js +38 -0
- package/lib/middleware/span-http-server.js +37 -0
- package/lib/middleware/sse.js +36 -0
- package/lib/middleware/trace-log-correlation.js +33 -0
- package/lib/middleware/trace-propagate.js +32 -0
- package/lib/middleware/tus-upload.js +90 -0
- package/lib/middleware/web-app-manifest.js +53 -0
- package/lib/mtls-ca.js +100 -70
- package/lib/network-byte-quota.js +308 -0
- package/lib/network-heartbeat.js +135 -0
- package/lib/network-tls.js +534 -4
- package/lib/network.js +103 -0
- package/lib/notify.js +114 -43
- package/lib/ntp-check.js +192 -51
- package/lib/observability.js +145 -47
- package/lib/openapi.js +90 -44
- package/lib/outbox.js +99 -1
- package/lib/pagination.js +168 -86
- package/lib/parsers/index.js +16 -5
- package/lib/permissions.js +93 -40
- package/lib/pqc-agent.js +84 -8
- package/lib/pqc-software.js +94 -60
- package/lib/process-spawn.js +95 -21
- package/lib/pubsub.js +96 -66
- package/lib/queue.js +375 -54
- package/lib/redact.js +793 -21
- package/lib/render.js +139 -47
- package/lib/request-helpers.js +485 -121
- package/lib/restore-bundle.js +142 -39
- package/lib/restore-rollback.js +136 -45
- package/lib/retention.js +178 -50
- package/lib/retry.js +116 -33
- package/lib/router.js +475 -23
- package/lib/safe-async.js +543 -94
- package/lib/safe-buffer.js +337 -41
- package/lib/safe-json.js +467 -62
- package/lib/safe-jsonpath.js +285 -0
- package/lib/safe-schema.js +631 -87
- package/lib/safe-sql.js +221 -59
- package/lib/safe-url.js +278 -46
- package/lib/sandbox-worker.js +135 -0
- package/lib/sandbox.js +358 -0
- package/lib/scheduler.js +135 -70
- package/lib/self-update.js +647 -0
- package/lib/session-device-binding.js +431 -0
- package/lib/session.js +259 -49
- package/lib/slug.js +138 -26
- package/lib/ssrf-guard.js +316 -56
- package/lib/storage.js +433 -70
- package/lib/subject.js +405 -23
- package/lib/template.js +148 -8
- package/lib/tenant-quota.js +545 -0
- package/lib/testing.js +440 -53
- package/lib/time.js +291 -23
- package/lib/tls-exporter.js +239 -0
- package/lib/tracing.js +90 -74
- package/lib/uuid.js +97 -22
- package/lib/vault/index.js +284 -22
- package/lib/vault/seal-pem-file.js +66 -0
- package/lib/watcher.js +368 -0
- package/lib/webhook.js +196 -63
- package/lib/websocket.js +393 -68
- package/lib/wiki-concepts.js +338 -0
- package/lib/worker-pool.js +464 -0
- package/package.json +3 -3
- package/sbom.cyclonedx.json +7 -7
package/lib/websocket.js
CHANGED
|
@@ -1,84 +1,77 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* @module b.websocket
|
|
4
|
+
* @nav HTTP
|
|
5
|
+
* @title Websocket
|
|
4
6
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
7
|
+
* @intro
|
|
8
|
+
* RFC 6455 WebSocket server on top of Node's `'upgrade'` event, plus
|
|
9
|
+
* RFC 8441 Extended CONNECT for HTTP/2. Built on `node:net` +
|
|
10
|
+
* `node:crypto` + `node:zlib` with no npm runtime dep.
|
|
8
11
|
*
|
|
9
|
-
*
|
|
12
|
+
* Three layers exposed to operators:
|
|
10
13
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
14
|
+
* 1. Handshake — `handleUpgrade(req, socket, head, opts)` for h1
|
|
15
|
+
* and `handleExtendedConnect(stream, headers, opts)` for h2.
|
|
16
|
+
* Validate the request, enforce same-origin (default) or an
|
|
17
|
+
* operator-supplied allowlist, negotiate subprotocol +
|
|
18
|
+
* permessage-deflate, return a `WebSocketConnection`. Refuse
|
|
19
|
+
* credential-shaped query parameters (`access_token`, `apikey`,
|
|
20
|
+
* `authorization`, …) — query strings leak via access logs,
|
|
21
|
+
* Referer headers, and browser history.
|
|
16
22
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* conn.send(data) — Buffer or string. Routes to binary
|
|
23
|
-
* or text frame. Throws if not OPEN.
|
|
24
|
-
* conn.ping(payload?) — Send ping frame (no-op if not OPEN).
|
|
25
|
-
* conn.close(code?, reason?) — Send close frame, wait
|
|
26
|
-
* closeGraceMs for peer's echo, end
|
|
27
|
-
* socket.
|
|
28
|
-
* Events:
|
|
29
|
-
* 'message' (data, isBinary)
|
|
30
|
-
* 'ping' (payload)
|
|
31
|
-
* 'pong' (payload)
|
|
32
|
-
* 'close' (code, reason, wasClean) — fires exactly once at
|
|
33
|
-
* lifecycle end. wasClean: true when
|
|
34
|
-
* the close handshake completed in
|
|
35
|
-
* both directions; false on socket
|
|
36
|
-
* errors / abnormal closure (code
|
|
37
|
-
* 1006) / heartbeat timeout / etc.
|
|
38
|
-
* Operators usually only need this
|
|
39
|
-
* listener for full lifecycle tracking.
|
|
40
|
-
* 'error' (err) — diagnosable issue. Always followed
|
|
41
|
-
* by 'close'. Optional listener;
|
|
42
|
-
* missing listener does NOT crash the
|
|
43
|
-
* process (gated by listenerCount).
|
|
23
|
+
* 2. Connection — `WebSocketConnection` is an EventEmitter
|
|
24
|
+
* mirroring the browser API. Read `conn.readyState`
|
|
25
|
+
* (`'open' | 'closing' | 'closed'`); call `conn.send(data)`,
|
|
26
|
+
* `conn.ping(payload?)`, `conn.close(code?, reason?)`. Listen
|
|
27
|
+
* on `'message'`, `'ping'`, `'pong'`, `'close'`, `'error'`.
|
|
44
28
|
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
29
|
+
* 3. Frame layer — `FrameParser` + `serializeFrame` exposed for
|
|
30
|
+
* tests and advanced callers (custom proxy / multiplexer use
|
|
31
|
+
* cases). Operator code rarely touches these directly.
|
|
47
32
|
*
|
|
48
|
-
*
|
|
33
|
+
* Defenses wired in by default:
|
|
49
34
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
35
|
+
* - Same-origin Origin check on browser-initiated upgrades. Cross-
|
|
36
|
+
* site WebSocket hijacking (CSWSH) requires explicit opt-in via
|
|
37
|
+
* `origins: [...allowlist]` or `origins: "*"`.
|
|
38
|
+
* - Control-frame payload cap of 125 bytes (RFC 6455 §5.5).
|
|
39
|
+
* Without it, a 1 MiB PING echoes back as a 1 MiB PONG — a 2x
|
|
40
|
+
* outbound-bandwidth amplification DoS.
|
|
41
|
+
* - Strict UTF-8 validation on TEXT frames + close reasons (§5.6).
|
|
42
|
+
* - Close-code allowlist per §7.4.2 (1000–1011, 3000–4999;
|
|
43
|
+
* reserved 1004/1005/1006/1015 refused on the wire).
|
|
44
|
+
* - Frame + message length capped at `maxMessageBytes`
|
|
45
|
+
* (default 1 MiB).
|
|
46
|
+
* - Heartbeat: ping every 30s, abort after 35s without pong.
|
|
47
|
+
* - Cluster fan-out lives at the router/channel layer above this
|
|
48
|
+
* module; this primitive owns the per-connection protocol.
|
|
55
49
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
50
|
+
* Configurable handshake GUID: closed-ecosystem clients with a
|
|
51
|
+
* custom magic string pass `opts.handshakeGuid` (UUID-shaped). The
|
|
52
|
+
* default is the RFC 6455 §1.3 value so RFC-compliant clients
|
|
53
|
+
* interoperate out of the box. SHA-1 used in `Sec-WebSocket-Accept`
|
|
54
|
+
* is a protocol marker, not a security primitive — its collision
|
|
55
|
+
* resistance is irrelevant to the connection's security.
|
|
62
56
|
*
|
|
63
|
-
*
|
|
64
|
-
* close frame, we MUST echo a close frame back, then close the
|
|
65
|
-
* TCP socket. close() handles this; _handleClose echoes if we
|
|
66
|
-
* haven't already initiated.
|
|
57
|
+
* Spec compliance notes (where naive implementations get it wrong):
|
|
67
58
|
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
59
|
+
* 1. Mask handling (§5.3). All client→server h1 frames MUST be
|
|
60
|
+
* masked; server→client frames MUST NOT be masked. The h2
|
|
61
|
+
* transport (RFC 8441) flips both: frames MUST NOT be masked
|
|
62
|
+
* because h2 already provides the framing guarantees masking
|
|
63
|
+
* defends.
|
|
64
|
+
* 2. Close handshake reciprocity (§5.5.1). Peer-initiated close
|
|
65
|
+
* echoes a close frame back before ending the TCP socket.
|
|
66
|
+
* 3. Subprotocol negotiation. Server picks the FIRST entry from
|
|
67
|
+
* `Sec-WebSocket-Protocol` that's in the operator's allowlist.
|
|
68
|
+
* If none match, response omits the header (§11.3.4).
|
|
69
|
+
* 4. permessage-deflate (RFC 7692). Negotiated when the client
|
|
70
|
+
* offers it; runs in `no_context_takeover` mode in both
|
|
71
|
+
* directions so each message uses a fresh zlib state.
|
|
77
72
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
* allowlist. If none match, the response omits the header (per
|
|
81
|
-
* §11.3.4) and the client decides whether to proceed.
|
|
73
|
+
* @card
|
|
74
|
+
* RFC 6455 WebSocket server on top of Node's `'upgrade'` event, plus RFC 8441 Extended CONNECT for HTTP/2.
|
|
82
75
|
*/
|
|
83
76
|
|
|
84
77
|
var nodeCrypto = require("crypto");
|
|
@@ -208,6 +201,31 @@ var STATE_OPEN = "open";
|
|
|
208
201
|
var STATE_CLOSING = "closing"; // we sent a close frame, awaiting peer's echo
|
|
209
202
|
var STATE_CLOSED = "closed";
|
|
210
203
|
|
|
204
|
+
/**
|
|
205
|
+
* @primitive b.websocket.WebSocketError
|
|
206
|
+
* @signature b.websocket.WebSocketError(code, message, closeCode)
|
|
207
|
+
* @since 0.1.38
|
|
208
|
+
* @status stable
|
|
209
|
+
* @related b.websocket.WebSocketConnection
|
|
210
|
+
*
|
|
211
|
+
* Framework-error subclass thrown for protocol violations + invalid
|
|
212
|
+
* caller input (`send()` on a closed connection, malformed frame
|
|
213
|
+
* payload, frame-too-large detected at parse time). Carries the
|
|
214
|
+
* RFC 6455 §7.4.1 `closeCode` the connection layer uses when
|
|
215
|
+
* aborting (1002 protocol error, 1007 invalid payload, 1009 message
|
|
216
|
+
* too big, 1011 internal error). Operators usually catch via the
|
|
217
|
+
* shared `b.errors` surface and never construct one directly.
|
|
218
|
+
*
|
|
219
|
+
* @example
|
|
220
|
+
* try {
|
|
221
|
+
* conn.send("late message");
|
|
222
|
+
* } catch (err) {
|
|
223
|
+
* if (err.isWebSocketError) {
|
|
224
|
+
* console.log(err.code, err.closeCode);
|
|
225
|
+
* // → "ws/closed" 1002
|
|
226
|
+
* }
|
|
227
|
+
* }
|
|
228
|
+
*/
|
|
211
229
|
class WebSocketError extends FrameworkError {
|
|
212
230
|
constructor(code, message, closeCode) {
|
|
213
231
|
super(message, code);
|
|
@@ -219,6 +237,26 @@ class WebSocketError extends FrameworkError {
|
|
|
219
237
|
|
|
220
238
|
// ---- Handshake helpers ----
|
|
221
239
|
|
|
240
|
+
/**
|
|
241
|
+
* @primitive b.websocket.computeAcceptKey
|
|
242
|
+
* @signature b.websocket.computeAcceptKey(secWebSocketKey, handshakeGuid)
|
|
243
|
+
* @since 0.1.38
|
|
244
|
+
* @status stable
|
|
245
|
+
* @related b.websocket.handleUpgrade, b.websocket.buildUpgradeResponse
|
|
246
|
+
*
|
|
247
|
+
* Compute the `Sec-WebSocket-Accept` value RFC 6455 §1.3 mandates:
|
|
248
|
+
* `base64(SHA1(secWebSocketKey || handshakeGuid))`. The SHA-1 is a
|
|
249
|
+
* protocol marker confirming both ends agree on the upgrade — it is
|
|
250
|
+
* NOT a security primitive, and the framework's other crypto stays
|
|
251
|
+
* SHA3 / SHAKE-based regardless. Pass `handshakeGuid` undefined to
|
|
252
|
+
* use the RFC value (`258EAFA5-E914-47DA-95CA-C5AB0DC85B11`); pass a
|
|
253
|
+
* UUID-shaped override only for closed-ecosystem clients with a
|
|
254
|
+
* matching custom magic string.
|
|
255
|
+
*
|
|
256
|
+
* @example
|
|
257
|
+
* var accept = b.websocket.computeAcceptKey("dGhlIHNhbXBsZSBub25jZQ==");
|
|
258
|
+
* // → "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="
|
|
259
|
+
*/
|
|
222
260
|
function computeAcceptKey(secWebSocketKey, handshakeGuid) {
|
|
223
261
|
// SHA-1 required by RFC 6455 §1.3 — see file-level note 2 above.
|
|
224
262
|
// This is a protocol marker, not a security primitive.
|
|
@@ -229,6 +267,35 @@ function computeAcceptKey(secWebSocketKey, handshakeGuid) {
|
|
|
229
267
|
return hash.digest("base64");
|
|
230
268
|
}
|
|
231
269
|
|
|
270
|
+
/**
|
|
271
|
+
* @primitive b.websocket.validateUpgradeRequest
|
|
272
|
+
* @signature b.websocket.validateUpgradeRequest(req, opts)
|
|
273
|
+
* @since 0.1.38
|
|
274
|
+
* @status stable
|
|
275
|
+
* @related b.websocket.handleUpgrade, b.websocket.isOriginAllowed
|
|
276
|
+
*
|
|
277
|
+
* Strict shape check on the HTTP/1.1 upgrade request. Verifies the
|
|
278
|
+
* method is GET, `Upgrade: websocket` and `Connection: upgrade`
|
|
279
|
+
* tokens are present, `Sec-WebSocket-Version: 13`, and
|
|
280
|
+
* `Sec-WebSocket-Key` is a 24-character base64 of 16 random bytes
|
|
281
|
+
* (RFC 6455 §4.1). Refuses credential-shaped query parameters
|
|
282
|
+
* (`access_token`, `apikey`, `authorization`, …) unless the operator
|
|
283
|
+
* passes `opts.allowQueryAuthParams: true` with an audited reason.
|
|
284
|
+
* Returns `{ ok: true }` on success or
|
|
285
|
+
* `{ ok: false, status, reason }` on refusal — never throws, so the
|
|
286
|
+
* caller can write the refusal response and end the socket cleanly.
|
|
287
|
+
*
|
|
288
|
+
* @opts
|
|
289
|
+
* allowQueryAuthParams: boolean, // opt out of credential-query refusal
|
|
290
|
+
*
|
|
291
|
+
* @example
|
|
292
|
+
* var v = b.websocket.validateUpgradeRequest(req, {});
|
|
293
|
+
* if (!v.ok) {
|
|
294
|
+
* // → { ok: false, status: 400, reason: "..." }
|
|
295
|
+
* socket.write("HTTP/1.1 " + v.status + " Bad Request\r\n\r\n");
|
|
296
|
+
* socket.destroy();
|
|
297
|
+
* }
|
|
298
|
+
*/
|
|
232
299
|
function validateUpgradeRequest(req, opts) {
|
|
233
300
|
if (req.method !== "GET") {
|
|
234
301
|
return { ok: false, status: HTTP.METHOD_NOT_ALLOWED, reason: "method must be GET" };
|
|
@@ -306,6 +373,25 @@ function _findCredentialQueryParam(reqUrl) {
|
|
|
306
373
|
return null;
|
|
307
374
|
}
|
|
308
375
|
|
|
376
|
+
/**
|
|
377
|
+
* @primitive b.websocket.negotiateSubprotocol
|
|
378
|
+
* @signature b.websocket.negotiateSubprotocol(req, supported)
|
|
379
|
+
* @since 0.1.38
|
|
380
|
+
* @status stable
|
|
381
|
+
* @related b.websocket.handleUpgrade, b.websocket.buildUpgradeResponse
|
|
382
|
+
*
|
|
383
|
+
* Pick the first client-offered subprotocol that appears in
|
|
384
|
+
* `supported`. Returns the chosen string, or `null` when there is no
|
|
385
|
+
* intersection (per RFC 6455 §11.3.4 the response then omits the
|
|
386
|
+
* `Sec-WebSocket-Protocol` header and the client decides whether to
|
|
387
|
+
* proceed). `supported` falsy or empty is treated as "no preference"
|
|
388
|
+
* and always returns `null`.
|
|
389
|
+
*
|
|
390
|
+
* @example
|
|
391
|
+
* var req = { headers: { "sec-websocket-protocol": "chat.v2, chat.v1" } };
|
|
392
|
+
* var picked = b.websocket.negotiateSubprotocol(req, ["chat.v1"]);
|
|
393
|
+
* // → "chat.v1"
|
|
394
|
+
*/
|
|
309
395
|
function negotiateSubprotocol(req, supported) {
|
|
310
396
|
if (!supported || supported.length === 0) return null;
|
|
311
397
|
var raw = (req.headers || {})["sec-websocket-protocol"] || "";
|
|
@@ -328,6 +414,33 @@ function negotiateSubprotocol(req, supported) {
|
|
|
328
414
|
// needing cross-origin opt in explicitly via
|
|
329
415
|
// `origins: "*"` (with audited reason) or
|
|
330
416
|
// `origins: [...allowlist]`.
|
|
417
|
+
/**
|
|
418
|
+
* @primitive b.websocket.isOriginAllowed
|
|
419
|
+
* @signature b.websocket.isOriginAllowed(req, origins)
|
|
420
|
+
* @since 0.1.38
|
|
421
|
+
* @status stable
|
|
422
|
+
* @related b.websocket.handleUpgrade, b.websocket.validateUpgradeRequest
|
|
423
|
+
*
|
|
424
|
+
* Browser-Origin policy gate. Behaviour by the `origins` shape:
|
|
425
|
+
*
|
|
426
|
+
* - Array — strict allowlist; the request's `Origin` header must
|
|
427
|
+
* match one entry exactly.
|
|
428
|
+
* - `"*"` — explicit accept-all (operator opt-in to no checking).
|
|
429
|
+
* - `null` / `undefined` — DEFAULT same-origin: the `Origin` host
|
|
430
|
+
* must match the `Host` header. Closes the cross-site WebSocket
|
|
431
|
+
* hijacking (CSWSH) class on browser-targeted routes.
|
|
432
|
+
*
|
|
433
|
+
* Non-browser clients (curl, server-to-server, native apps) don't
|
|
434
|
+
* send `Origin` and bypass the check — gating those callers is the
|
|
435
|
+
* operator's network-ACL / auth-middleware job, not Origin's.
|
|
436
|
+
*
|
|
437
|
+
* @example
|
|
438
|
+
* var req = { headers: { origin: "https://app.example.com",
|
|
439
|
+
* host: "app.example.com" } };
|
|
440
|
+
* b.websocket.isOriginAllowed(req, undefined); // → true
|
|
441
|
+
* b.websocket.isOriginAllowed(req, ["https://other.example"]); // → false
|
|
442
|
+
* b.websocket.isOriginAllowed(req, "*"); // → true
|
|
443
|
+
*/
|
|
331
444
|
function isOriginAllowed(req, origins) {
|
|
332
445
|
if (origins === "*") return true;
|
|
333
446
|
var origin = (req.headers || {}).origin;
|
|
@@ -351,6 +464,28 @@ function isOriginAllowed(req, origins) {
|
|
|
351
464
|
return false;
|
|
352
465
|
}
|
|
353
466
|
|
|
467
|
+
/**
|
|
468
|
+
* @primitive b.websocket.buildUpgradeResponse
|
|
469
|
+
* @signature b.websocket.buildUpgradeResponse(secWebSocketKey, subprotocol, extensionHeader, handshakeGuid)
|
|
470
|
+
* @since 0.1.38
|
|
471
|
+
* @status stable
|
|
472
|
+
* @related b.websocket.handleUpgrade, b.websocket.computeAcceptKey
|
|
473
|
+
*
|
|
474
|
+
* Format the HTTP/1.1 101 Switching Protocols response that completes
|
|
475
|
+
* the WebSocket handshake. Always emits `Upgrade: websocket`,
|
|
476
|
+
* `Connection: Upgrade`, and `Sec-WebSocket-Accept`. Adds
|
|
477
|
+
* `Sec-WebSocket-Protocol` when `subprotocol` is non-null, and
|
|
478
|
+
* `Sec-WebSocket-Extensions` when `extensionHeader` is non-null
|
|
479
|
+
* (e.g. the `permessage-deflate; ...` echo). Pass `handshakeGuid`
|
|
480
|
+
* undefined to use the RFC 6455 default. Returns the raw
|
|
481
|
+
* `\r\n`-delimited response string ready for `socket.write()`.
|
|
482
|
+
*
|
|
483
|
+
* @example
|
|
484
|
+
* var resp = b.websocket.buildUpgradeResponse(
|
|
485
|
+
* "dGhlIHNhbXBsZSBub25jZQ==", "chat.v1", null);
|
|
486
|
+
* // → "HTTP/1.1 101 Switching Protocols\r\n..."
|
|
487
|
+
* socket.write(resp);
|
|
488
|
+
*/
|
|
354
489
|
function buildUpgradeResponse(secWebSocketKey, subprotocol, extensionHeader, handshakeGuid) {
|
|
355
490
|
var lines = [
|
|
356
491
|
"HTTP/1.1 101 Switching Protocols",
|
|
@@ -471,6 +606,33 @@ function _inflateMessage(payload, windowBits) {
|
|
|
471
606
|
// socket and emits zero-or-more complete frames as they arrive. Holds
|
|
472
607
|
// partial frame state across calls.
|
|
473
608
|
|
|
609
|
+
/**
|
|
610
|
+
* @primitive b.websocket.FrameParser
|
|
611
|
+
* @signature b.websocket.FrameParser(opts)
|
|
612
|
+
* @since 0.1.38
|
|
613
|
+
* @status stable
|
|
614
|
+
* @related b.websocket.serializeFrame, b.websocket.WebSocketConnection
|
|
615
|
+
*
|
|
616
|
+
* Incremental RFC 6455 §5.2 frame parser. `push(chunk)` accepts
|
|
617
|
+
* arbitrary buffer slices straight from the socket and returns zero
|
|
618
|
+
* or more complete frames; partial frame state persists across
|
|
619
|
+
* calls. Each emitted frame is
|
|
620
|
+
* `{ fin, rsv1, rsv2, rsv3, opcode, masked, payload }`. Throws a
|
|
621
|
+
* `WebSocketError` (closeCode = 1009 message-too-big) when a single
|
|
622
|
+
* frame's declared payload length exceeds `opts.maxFrameBytes`
|
|
623
|
+
* (default 1 MiB) — the caller catches it and aborts the connection.
|
|
624
|
+
* The parser does NOT enforce control-frame ≤125-byte caps,
|
|
625
|
+
* mask-direction policy, or RSV-bit-vs-extension consistency; those
|
|
626
|
+
* are the connection layer's job.
|
|
627
|
+
*
|
|
628
|
+
* @opts
|
|
629
|
+
* maxFrameBytes: number, // single-frame payload cap (default 1 MiB)
|
|
630
|
+
*
|
|
631
|
+
* @example
|
|
632
|
+
* var parser = new b.websocket.FrameParser({ maxFrameBytes: 65536 });
|
|
633
|
+
* var frames = parser.push(Buffer.from([0x81, 0x05, 0x68, 0x65, 0x6c, 0x6c, 0x6f]));
|
|
634
|
+
* // → [{ fin: true, opcode: 1, payload: <Buffer 68 65 6c 6c 6f>, ... }]
|
|
635
|
+
*/
|
|
474
636
|
function FrameParser(opts) {
|
|
475
637
|
opts = opts || {};
|
|
476
638
|
this.maxFrameBytes = opts.maxFrameBytes || DEFAULT_MAX_MESSAGE_BYTES;
|
|
@@ -569,6 +731,33 @@ FrameParser.prototype._tryParseFrame = function () {
|
|
|
569
731
|
|
|
570
732
|
// ---- Frame serializer ----
|
|
571
733
|
|
|
734
|
+
/**
|
|
735
|
+
* @primitive b.websocket.serializeFrame
|
|
736
|
+
* @signature b.websocket.serializeFrame(opcode, payload, opts)
|
|
737
|
+
* @since 0.1.38
|
|
738
|
+
* @status stable
|
|
739
|
+
* @related b.websocket.FrameParser, b.websocket.WebSocketConnection
|
|
740
|
+
*
|
|
741
|
+
* Build a single RFC 6455 §5.2 frame. `opcode` is one of
|
|
742
|
+
* `b.websocket.OPCODE_TEXT` / `OPCODE_BINARY` / `OPCODE_CLOSE` /
|
|
743
|
+
* `OPCODE_PING` / `OPCODE_PONG` / `OPCODE_CONTINUATION`. `payload`
|
|
744
|
+
* is a `Buffer` or string (string is UTF-8 encoded). Server-side
|
|
745
|
+
* frames default to unmasked; pass `mask: true` only for
|
|
746
|
+
* client-shaped fixtures or test harnesses. `rsv1: true` marks the
|
|
747
|
+
* first frame of a permessage-deflate-compressed message (RFC 7692).
|
|
748
|
+
* Returns the framed `Buffer`.
|
|
749
|
+
*
|
|
750
|
+
* @opts
|
|
751
|
+
* fin: boolean, // FIN bit, default true (single-frame message)
|
|
752
|
+
* mask: boolean, // mask the payload, default false (server side)
|
|
753
|
+
* rsv1: boolean, // RSV1 bit (permessage-deflate), default false
|
|
754
|
+
*
|
|
755
|
+
* @example
|
|
756
|
+
* var frame = b.websocket.serializeFrame(
|
|
757
|
+
* b.websocket.OPCODE_TEXT, "hello");
|
|
758
|
+
* // → <Buffer 81 05 68 65 6c 6c 6f>
|
|
759
|
+
* socket.write(frame);
|
|
760
|
+
*/
|
|
572
761
|
function serializeFrame(opcode, payload, opts) {
|
|
573
762
|
opts = opts || {};
|
|
574
763
|
var fin = opts.fin !== false;
|
|
@@ -623,6 +812,58 @@ function serializeFrame(opcode, payload, opts) {
|
|
|
623
812
|
|
|
624
813
|
// ---- Connection ----
|
|
625
814
|
|
|
815
|
+
/**
|
|
816
|
+
* @primitive b.websocket.WebSocketConnection
|
|
817
|
+
* @signature b.websocket.WebSocketConnection(socket, opts)
|
|
818
|
+
* @since 0.1.38
|
|
819
|
+
* @status stable
|
|
820
|
+
* @related b.websocket.handleUpgrade, b.websocket.handleExtendedConnect, b.websocket.WebSocketError
|
|
821
|
+
*
|
|
822
|
+
* EventEmitter wrapping a post-upgrade socket / h2 stream. State
|
|
823
|
+
* machine mirrors the browser WebSocket API:
|
|
824
|
+
*
|
|
825
|
+
* - `conn.readyState` — `'open' | 'closing' | 'closed'`
|
|
826
|
+
* - `conn.send(data)` — `Buffer` or string. Routes to BINARY or
|
|
827
|
+
* TEXT frame; throws `WebSocketError` if not OPEN.
|
|
828
|
+
* - `conn.ping(payload?)` — send PING (no-op if not OPEN).
|
|
829
|
+
* - `conn.close(code?, reason?)` — send CLOSE, wait `closeGraceMs`
|
|
830
|
+
* for the peer's echo, end the underlying socket.
|
|
831
|
+
*
|
|
832
|
+
* Events: `'message' (data, isBinary)`, `'ping' (payload)`,
|
|
833
|
+
* `'pong' (payload)`, `'close' (code, reason, wasClean)` (fires
|
|
834
|
+
* exactly once at lifecycle end), `'error' (err)`.
|
|
835
|
+
*
|
|
836
|
+
* Cluster fan-out / channel broadcast lives at the router layer that
|
|
837
|
+
* owns the connection registry; this primitive owns the per-
|
|
838
|
+
* connection protocol. To broadcast, iterate the operator-side
|
|
839
|
+
* registry and call `send` on each.
|
|
840
|
+
*
|
|
841
|
+
* @opts
|
|
842
|
+
* subprotocol: string, // negotiated value from handleUpgrade
|
|
843
|
+
* transport: "h1" | "h2", // mask-direction policy, default "h1"
|
|
844
|
+
* maxMessageBytes: number, // total reassembled-message cap, default 1 MiB
|
|
845
|
+
* pingIntervalMs: number, // heartbeat interval, default 30s
|
|
846
|
+
* pongTimeoutMs: number, // abort threshold without pong, default 35s
|
|
847
|
+
* closeGraceMs: number, // peer-echo wait after close(), default 2s
|
|
848
|
+
* permessageDeflate: object | null, // negotiated state, usually from handleUpgrade
|
|
849
|
+
*
|
|
850
|
+
* @example
|
|
851
|
+
* server.on("upgrade", function (req, socket, head) {
|
|
852
|
+
* var conn = b.websocket.handleUpgrade(req, socket, head, {
|
|
853
|
+
* subprotocols: ["chat.v1"],
|
|
854
|
+
* });
|
|
855
|
+
* if (!conn) return;
|
|
856
|
+
* conn.on("message", function (data, isBinary) {
|
|
857
|
+
* conn.send(isBinary ? data : "echo: " + data);
|
|
858
|
+
* });
|
|
859
|
+
* conn.on("ping", function (payload) { void payload; }); // framework auto-pongs
|
|
860
|
+
* conn.on("pong", function (payload) { void payload; }); // heartbeat reply
|
|
861
|
+
|
|
862
|
+
* conn.on("close", function (code, reason, wasClean) {
|
|
863
|
+
* // → 1000, "", true
|
|
864
|
+
* });
|
|
865
|
+
* });
|
|
866
|
+
*/
|
|
626
867
|
class WebSocketConnection extends EventEmitter {
|
|
627
868
|
constructor(socket, opts) {
|
|
628
869
|
super();
|
|
@@ -990,6 +1231,48 @@ class WebSocketConnection extends EventEmitter {
|
|
|
990
1231
|
// this function. Operators usually don't call it directly; they pass
|
|
991
1232
|
// a handler to router.ws(path, opts).
|
|
992
1233
|
|
|
1234
|
+
/**
|
|
1235
|
+
* @primitive b.websocket.handleUpgrade
|
|
1236
|
+
* @signature b.websocket.handleUpgrade(req, socket, head, opts)
|
|
1237
|
+
* @since 0.1.38
|
|
1238
|
+
* @status stable
|
|
1239
|
+
* @related b.websocket.handleExtendedConnect, b.websocket.WebSocketConnection
|
|
1240
|
+
*
|
|
1241
|
+
* RFC 6455 HTTP/1.1 upgrade entry point. Wire it to the HTTP
|
|
1242
|
+
* server's `'upgrade'` event. Validates the handshake, enforces the
|
|
1243
|
+
* Origin policy (same-origin by default), negotiates subprotocol +
|
|
1244
|
+
* permessage-deflate, writes the 101 response, and returns a
|
|
1245
|
+
* `WebSocketConnection`. Returns `null` and writes a refusal HTTP
|
|
1246
|
+
* response on bad handshake / origin mismatch — the caller does not
|
|
1247
|
+
* need a try/catch around the normal refusal paths. Throws
|
|
1248
|
+
* synchronously only when `opts.handshakeGuid` is supplied with a
|
|
1249
|
+
* malformed value (config-time typo).
|
|
1250
|
+
*
|
|
1251
|
+
* @opts
|
|
1252
|
+
* origins: string[] | "*", // allowlist, or "*" accept-all; default same-origin
|
|
1253
|
+
* subprotocols: string[], // negotiation allowlist
|
|
1254
|
+
* handshakeGuid: string, // UUID-shape override of RFC 6455 §1.3 GUID
|
|
1255
|
+
* permessageDeflate: boolean, // RFC 7692 negotiation, default true
|
|
1256
|
+
* maxMessageBytes: number, // total message cap, default 1 MiB
|
|
1257
|
+
* pingIntervalMs: number, // heartbeat interval, default 30s
|
|
1258
|
+
* pongTimeoutMs: number, // abort-after-silence, default 35s
|
|
1259
|
+
* allowQueryAuthParams: boolean, // opt out of credential-query refusal
|
|
1260
|
+
*
|
|
1261
|
+
* @example
|
|
1262
|
+
* var http = require("http");
|
|
1263
|
+
* var server = http.createServer();
|
|
1264
|
+
* server.on("upgrade", function (req, socket, head) {
|
|
1265
|
+
* var conn = b.websocket.handleUpgrade(req, socket, head, {
|
|
1266
|
+
* origins: ["https://app.example.com"],
|
|
1267
|
+
* subprotocols: ["chat.v1"],
|
|
1268
|
+
* });
|
|
1269
|
+
* if (!conn) return; // refusal already written + socket destroyed
|
|
1270
|
+
* conn.on("message", function (data, isBinary) {
|
|
1271
|
+
* // → "hello", false
|
|
1272
|
+
* conn.send("ack: " + data);
|
|
1273
|
+
* });
|
|
1274
|
+
* });
|
|
1275
|
+
*/
|
|
993
1276
|
function handleUpgrade(req, socket, head, opts) {
|
|
994
1277
|
opts = opts || {};
|
|
995
1278
|
|
|
@@ -1082,6 +1365,48 @@ function handleUpgrade(req, socket, head, opts) {
|
|
|
1082
1365
|
// — pass `settings: { enableConnectProtocol: true }` to
|
|
1083
1366
|
// http2.createServer / createSecureServer.
|
|
1084
1367
|
|
|
1368
|
+
/**
|
|
1369
|
+
* @primitive b.websocket.handleExtendedConnect
|
|
1370
|
+
* @signature b.websocket.handleExtendedConnect(stream, requestHeaders, opts)
|
|
1371
|
+
* @since 0.1.39
|
|
1372
|
+
* @status stable
|
|
1373
|
+
* @related b.websocket.handleUpgrade, b.websocket.WebSocketConnection
|
|
1374
|
+
*
|
|
1375
|
+
* RFC 8441 Extended CONNECT entry point for HTTP/2. Wire it to the
|
|
1376
|
+
* h2 server's `'stream'` event when `:method` is `CONNECT` and
|
|
1377
|
+
* `:protocol` is `websocket`. Same Origin / subprotocol policy as
|
|
1378
|
+
* `handleUpgrade`. Responds with `:status 200` (NOT 101 — Extended
|
|
1379
|
+
* CONNECT is a CONNECT, not an Upgrade) and returns a
|
|
1380
|
+
* `WebSocketConnection` wrapping the h2 stream with mask-direction
|
|
1381
|
+
* flipped (h2 frames MUST NOT be masked). The h2 server must
|
|
1382
|
+
* advertise `SETTINGS_ENABLE_CONNECT_PROTOCOL = 1`; pass
|
|
1383
|
+
* `settings: { enableConnectProtocol: true }` to
|
|
1384
|
+
* `http2.createSecureServer`. Returns `null` on refusal.
|
|
1385
|
+
*
|
|
1386
|
+
* @opts
|
|
1387
|
+
* origins: string[] | "*", // allowlist / accept-all; default same-origin
|
|
1388
|
+
* subprotocols: string[], // negotiation allowlist
|
|
1389
|
+
* maxMessageBytes: number, // total message cap, default 1 MiB
|
|
1390
|
+
* pingIntervalMs: number, // heartbeat interval, default 30s
|
|
1391
|
+
* pongTimeoutMs: number, // abort-after-silence, default 35s
|
|
1392
|
+
*
|
|
1393
|
+
* @example
|
|
1394
|
+
* var http2 = require("http2");
|
|
1395
|
+
* var server = http2.createSecureServer({
|
|
1396
|
+
* key: fs.readFileSync("/etc/blamejs/tls.key"),
|
|
1397
|
+
* cert: fs.readFileSync("/etc/blamejs/tls.crt"),
|
|
1398
|
+
* settings: { enableConnectProtocol: true },
|
|
1399
|
+
* });
|
|
1400
|
+
* server.on("stream", function (stream, headers) {
|
|
1401
|
+
* if (headers[":method"] !== "CONNECT") return;
|
|
1402
|
+
* var conn = b.websocket.handleExtendedConnect(stream, headers, {
|
|
1403
|
+
* origins: ["https://app.example.com"],
|
|
1404
|
+
* subprotocols: ["chat.v1"],
|
|
1405
|
+
* });
|
|
1406
|
+
* if (!conn) return;
|
|
1407
|
+
* conn.close(1000, "shutdown"); // → 1000, "shutdown", true on the peer's 'close'
|
|
1408
|
+
* });
|
|
1409
|
+
*/
|
|
1085
1410
|
function handleExtendedConnect(stream, requestHeaders, opts) {
|
|
1086
1411
|
opts = opts || {};
|
|
1087
1412
|
|