serverless-ircd 0.9.0 → 0.11.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/.github/workflows/ci.yml +28 -0
- package/.github/workflows/deploy-aws.yml +156 -32
- package/.github/workflows/deploy-cf-tcp.yml +35 -9
- package/.github/workflows/deploy-cf.yml +40 -14
- package/CHANGELOG.md +594 -0
- package/README.md +286 -60
- package/apps/aws-stack/README.md +3 -5
- package/apps/aws-stack/bin/aws.ts +118 -9
- package/apps/aws-stack/cdk.json +0 -3
- package/apps/aws-stack/package.json +3 -4
- package/apps/aws-stack/src/aws-stack.ts +398 -67
- package/apps/aws-stack/src/static-site.ts +323 -0
- package/apps/aws-stack/tests/smoke-helpers.test.ts +1 -1
- package/apps/aws-stack/tests/stack.test.ts +714 -105
- package/apps/aws-stack/tests/static-site.test.ts +491 -0
- package/apps/aws-stack/tests/synth-no-bundle.test.ts +0 -1
- package/apps/cf-tcp-container/Dockerfile +37 -5
- package/apps/cf-tcp-container/package.json +7 -3
- package/apps/cf-tcp-container/src/config-loader.ts +113 -2
- package/apps/cf-tcp-container/src/container-server.ts +267 -87
- package/apps/cf-tcp-container/src/main.ts +22 -7
- package/apps/cf-tcp-container/src/proxy-protocol.ts +112 -0
- package/apps/cf-tcp-container/terraform/spectrum.tf +40 -11
- package/apps/cf-tcp-container/tests/config-loader.test.ts +170 -0
- package/apps/cf-tcp-container/tests/container-server-tls.test.ts +382 -0
- package/apps/cf-tcp-container/tests/container-server.test.ts +358 -31
- package/apps/cf-tcp-container/tests/dockerfile.test.ts +110 -0
- package/apps/cf-tcp-container/tests/proxy-protocol.test.ts +187 -0
- package/apps/cf-tcp-container/tests/spectrum-terraform.test.ts +135 -0
- package/apps/cf-tcp-container/tests/tls-e2e.test.ts +5 -1
- package/apps/cf-tcp-container/wrangler.toml +18 -14
- package/apps/cf-worker/package.json +3 -4
- package/apps/cf-worker/src/worker.ts +77 -5
- package/apps/cf-worker/tests/raw-modules.d.ts +11 -0
- package/apps/cf-worker/tests/smoke.test.ts +4 -0
- package/apps/cf-worker/tests/wrangler-config.test.ts +47 -0
- package/apps/cf-worker/tests/ws-admission.test.ts +112 -0
- package/apps/cf-worker/tests/ws-rate-limit.test.ts +133 -0
- package/apps/cf-worker/wrangler.test.toml +15 -1
- package/apps/cf-worker/wrangler.toml +95 -77
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +14 -2
- package/apps/local-cli/src/line-scanner.ts +26 -0
- package/apps/local-cli/src/server.ts +44 -19
- package/apps/local-cli/tests/line-scanner.test.ts +64 -0
- package/apps/local-cli/tests/tcp.test.ts +29 -0
- package/apps/web/landing/favicon.ico +0 -0
- package/apps/web/landing/index.html +1 -0
- package/apps/web/package.json +2 -2
- package/apps/web/scripts/build.mjs +66 -4
- package/apps/web/src/build-env.ts +125 -4
- package/apps/web/src/config-schema.ts +20 -6
- package/apps/web/static/{config.staging.json → config.prod-aws.json} +3 -2
- package/apps/web/tests/build-env.test.ts +210 -9
- package/apps/web/tests/build-smoke.test.ts +2 -2
- package/apps/web/tests/config-schema.test.ts +149 -25
- package/docs/AWS-Deployment.md +793 -118
- package/docs/AWS-TCP-Deployment.md +57 -47
- package/docs/Chat-History.md +55 -0
- package/docs/Cloudflare-Deployment-Guide.md +95 -114
- package/docs/Cloudflare-TCP-Deployment.md +160 -101
- package/docs/Release-Process.md +27 -23
- package/docs/SASL-EXTERNAL.md +175 -0
- package/docs/Services.md +69 -22
- package/docs/WebClientGuide.md +35 -26
- package/package.json +7 -10
- package/packages/aws-adapter/package.json +1 -1
- package/packages/aws-adapter/src/admission.ts +28 -13
- package/packages/aws-adapter/src/aws-runtime.ts +30 -3
- package/packages/aws-adapter/src/cdk-table-defs.ts +39 -16
- package/packages/aws-adapter/src/config-loader.ts +153 -8
- package/packages/aws-adapter/src/dynamo-services-store.ts +19 -0
- package/packages/aws-adapter/src/handlers/connect.ts +73 -1
- package/packages/aws-adapter/src/handlers/default.ts +279 -123
- package/packages/aws-adapter/src/handlers/index.ts +98 -25
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +135 -14
- package/packages/aws-adapter/src/index.ts +5 -7
- package/packages/aws-adapter/src/ip-admission.ts +79 -0
- package/packages/aws-adapter/src/origin-allowlist.ts +94 -0
- package/packages/aws-adapter/src/serialize.ts +23 -0
- package/packages/aws-adapter/src/tables.ts +11 -12
- package/packages/aws-adapter/tests/admission.test.ts +60 -2
- package/packages/aws-adapter/tests/aws-harness.ts +23 -2
- package/packages/aws-adapter/tests/aws-runtime.test.ts +64 -0
- package/packages/aws-adapter/tests/config-loader.test.ts +217 -0
- package/packages/aws-adapter/tests/connect.test.ts +323 -3
- package/packages/aws-adapter/tests/default-frame-limit.test.ts +231 -0
- package/packages/aws-adapter/tests/default-occ.test.ts +226 -0
- package/packages/aws-adapter/tests/dynamo-services-store-unit.test.ts +134 -1
- package/packages/aws-adapter/tests/handlers.test.ts +174 -12
- package/packages/aws-adapter/tests/migrate-accounts-to-services.test.ts +164 -0
- package/packages/aws-adapter/tests/nlb-secure.test.ts +362 -0
- package/packages/aws-adapter/tests/nlb-stream.test.ts +628 -9
- package/packages/aws-adapter/tests/origin-allowlist.test.ts +110 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +0 -1
- package/packages/aws-adapter/tests/stats.test.ts +0 -3
- package/packages/aws-adapter/tests/sweeper.test.ts +0 -1
- package/packages/aws-adapter/tests/tables.test.ts +1 -8
- package/packages/aws-adapter/tests/transactions.test.ts +0 -1
- package/packages/cf-adapter/package.json +1 -5
- package/packages/cf-adapter/src/cf-runtime.ts +100 -10
- package/packages/cf-adapter/src/channel-do.ts +13 -3
- package/packages/cf-adapter/src/config-loader.ts +133 -8
- package/packages/cf-adapter/src/connection-do.ts +406 -116
- package/packages/cf-adapter/src/counter-do.ts +142 -0
- package/packages/cf-adapter/src/d1-services-store.ts +105 -26
- package/packages/cf-adapter/src/env.ts +99 -10
- package/packages/cf-adapter/src/index.ts +17 -7
- package/packages/cf-adapter/src/rate-limit-do.ts +87 -0
- package/packages/cf-adapter/tests/cf-runtime.test.ts +205 -16
- package/packages/cf-adapter/tests/channel-do.test.ts +118 -1
- package/packages/cf-adapter/tests/config-loader.test.ts +159 -0
- package/packages/cf-adapter/tests/connection-do-counter.test.ts +165 -0
- package/packages/cf-adapter/tests/connection-do-coverage.test.ts +460 -0
- package/packages/cf-adapter/tests/connection-do-frame-limit.test.ts +177 -0
- package/packages/cf-adapter/tests/connection-do-pure.test.ts +164 -54
- package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +62 -38
- package/packages/cf-adapter/tests/connection-do-ws-spec-contract.test.ts +7 -4
- package/packages/cf-adapter/tests/counter-do.test.ts +181 -0
- package/packages/cf-adapter/tests/d1-services-store.test.ts +245 -3
- package/packages/cf-adapter/tests/rate-limit-do.test.ts +160 -0
- package/packages/cf-adapter/tests/serialize.test.ts +25 -0
- package/packages/cf-adapter/tests/worker/main.ts +4 -0
- package/packages/cf-adapter/wrangler.test.toml +18 -1
- package/packages/in-memory-runtime/package.json +1 -1
- package/packages/in-memory-runtime/src/in-memory-runtime.ts +25 -0
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +74 -0
- package/packages/irc-core/package.json +1 -1
- package/packages/irc-core/src/account-migration.ts +140 -0
- package/packages/irc-core/src/caps/capabilities.ts +20 -10
- package/packages/irc-core/src/certfp.ts +178 -0
- package/packages/irc-core/src/commands/account-auth.ts +16 -19
- package/packages/irc-core/src/commands/cap.ts +10 -2
- package/packages/irc-core/src/commands/chanserv.ts +117 -14
- package/packages/irc-core/src/commands/chathistory.ts +13 -5
- package/packages/irc-core/src/commands/hostserv.ts +84 -8
- package/packages/irc-core/src/commands/index.ts +2 -1
- package/packages/irc-core/src/commands/invite.ts +1 -7
- package/packages/irc-core/src/commands/join.ts +1 -16
- package/packages/irc-core/src/commands/kick.ts +1 -8
- package/packages/irc-core/src/commands/list.ts +1 -8
- package/packages/irc-core/src/commands/memoserv.ts +1 -1
- package/packages/irc-core/src/commands/mode.ts +1 -8
- package/packages/irc-core/src/commands/multiline.ts +4 -10
- package/packages/irc-core/src/commands/names.ts +53 -13
- package/packages/irc-core/src/commands/nickserv.ts +161 -11
- package/packages/irc-core/src/commands/oper.ts +361 -8
- package/packages/irc-core/src/commands/part.ts +4 -10
- package/packages/irc-core/src/commands/privmsg.ts +8 -4
- package/packages/irc-core/src/commands/registration.ts +148 -4
- package/packages/irc-core/src/commands/sasl.ts +154 -46
- package/packages/irc-core/src/commands/topic.ts +10 -12
- package/packages/irc-core/src/commands/who.ts +1 -8
- package/packages/irc-core/src/config.ts +424 -25
- package/packages/irc-core/src/credential-hashing.ts +11 -54
- package/packages/irc-core/src/effects.ts +24 -0
- package/packages/irc-core/src/flood-control.ts +10 -10
- package/packages/irc-core/src/frame-rate-limit.ts +82 -0
- package/packages/irc-core/src/index.ts +9 -0
- package/packages/irc-core/src/oper-hashing.ts +43 -0
- package/packages/irc-core/src/oper-lockout.ts +87 -0
- package/packages/irc-core/src/ports.ts +529 -190
- package/packages/irc-core/src/protocol/bytes.ts +65 -0
- package/packages/irc-core/src/protocol/channel-name.ts +37 -0
- package/packages/irc-core/src/protocol/index.ts +12 -1
- package/packages/irc-core/src/protocol/outbound.ts +43 -10
- package/packages/irc-core/src/protocol/parser.ts +79 -10
- package/packages/irc-core/src/state/connection.ts +13 -0
- package/packages/irc-core/src/types.ts +266 -23
- package/packages/irc-core/src/ws-framing.ts +5 -4
- package/packages/irc-core/tests/account-migration.test.ts +133 -0
- package/packages/irc-core/tests/bytes.test.ts +89 -0
- package/packages/irc-core/tests/certfp.test.ts +117 -0
- package/packages/irc-core/tests/commands/cap.test.ts +76 -2
- package/packages/irc-core/tests/commands/chanserv.test.ts +166 -0
- package/packages/irc-core/tests/commands/chathistory.test.ts +140 -0
- package/packages/irc-core/tests/commands/hostserv.test.ts +316 -0
- package/packages/irc-core/tests/commands/join.test.ts +78 -1
- package/packages/irc-core/tests/commands/markread.test.ts +54 -0
- package/packages/irc-core/tests/commands/memoserv.test.ts +19 -0
- package/packages/irc-core/tests/commands/names.test.ts +193 -0
- package/packages/irc-core/tests/commands/nickserv.test.ts +419 -3
- package/packages/irc-core/tests/commands/oper.test.ts +574 -1
- package/packages/irc-core/tests/commands/privmsg.test.ts +16 -0
- package/packages/irc-core/tests/commands/registration.test.ts +602 -133
- package/packages/irc-core/tests/commands/sasl.test.ts +742 -172
- package/packages/irc-core/tests/commands/topic.test.ts +137 -2
- package/packages/irc-core/tests/commands/unified-account.test.ts +104 -84
- package/packages/irc-core/tests/config.test.ts +534 -2
- package/packages/irc-core/tests/credential-hashing.test.ts +0 -78
- package/packages/irc-core/tests/effects.test.ts +14 -0
- package/packages/irc-core/tests/flood-control.test.ts +29 -1
- package/packages/irc-core/tests/frame-rate-limit.test.ts +98 -0
- package/packages/irc-core/tests/message-store.test.ts +5 -0
- package/packages/irc-core/tests/oper-hashing.test.ts +60 -0
- package/packages/irc-core/tests/oper-lockout.test.ts +74 -0
- package/packages/irc-core/tests/outbound.test.ts +148 -0
- package/packages/irc-core/tests/parser.test.ts +287 -5
- package/packages/irc-core/tests/persistent-services-store.test.ts +212 -12
- package/packages/irc-core/tests/ports.test.ts +170 -7
- package/packages/irc-core/tests/services-store.test.ts +567 -1
- package/packages/irc-core/tests/ws-framing.test.ts +45 -0
- package/packages/irc-core/vitest.config.ts +6 -1
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +123 -22
- package/packages/irc-server/src/dispatch.ts +1 -0
- package/packages/irc-server/src/index.ts +7 -0
- package/packages/irc-server/src/redact.ts +159 -0
- package/packages/irc-server/src/runtime.ts +14 -0
- package/packages/irc-server/src/transport.ts +28 -1
- package/packages/irc-server/tests/actor.test.ts +563 -54
- package/packages/irc-server/tests/dispatch.test.ts +31 -0
- package/packages/irc-server/tests/redact.test.ts +198 -0
- package/packages/irc-server/tests/runtime.test.ts +2 -0
- package/packages/irc-server/tests/transport.test.ts +66 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/in-memory-harness.ts +4 -0
- package/pnpm-workspace.yaml +1 -0
- package/scripts/__tests__/deploy-web-aws.test.ts +491 -0
- package/scripts/deploy-web-aws.mjs +290 -0
- package/scripts/package.json +23 -0
- package/scripts/tsconfig.test.json +12 -0
- package/scripts/vitest.config.ts +19 -0
- package/tools/ci-hardening/package.json +2 -2
- package/tools/ci-hardening/src/cf-deploy-cli.ts +3 -0
- package/tools/ci-hardening/src/cf-deploy.ts +118 -0
- package/tools/ci-hardening/src/deploy-hostname.ts +118 -0
- package/tools/ci-hardening/src/env-var-drift.ts +192 -0
- package/tools/ci-hardening/src/hostname-guard.ts +11 -0
- package/tools/ci-hardening/src/index.ts +19 -0
- package/tools/ci-hardening/src/validate.ts +57 -0
- package/tools/ci-hardening/tests/__wrangler_missing__.toml +2 -0
- package/tools/ci-hardening/tests/__wrangler_placeholder__.toml +3 -0
- package/tools/ci-hardening/tests/__wrangler_real__.toml +3 -0
- package/tools/ci-hardening/tests/cf-deploy.test.ts +200 -0
- package/tools/ci-hardening/tests/deploy-aws-oidc.test.ts +96 -0
- package/tools/ci-hardening/tests/deploy-hostname.test.ts +348 -0
- package/tools/ci-hardening/tests/env-var-drift.test.ts +284 -0
- package/tools/ci-hardening/tests/validate.test.ts +42 -0
- package/tools/ci-hardening/vitest.config.ts +5 -1
- package/tools/hash-oper-cred.ts +85 -0
- package/tools/load-test/package.json +1 -1
- package/tools/migrate-accounts-to-services.ts +270 -0
- package/tools/package.json +2 -1
- package/tools/seed-aws-accounts.ts +35 -10
- package/tools/seed-cf-accounts.ts +42 -9
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/packages/aws-adapter/src/account-store.ts +0 -121
- package/packages/aws-adapter/src/dynamo-account-store.ts +0 -95
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +0 -223
- package/packages/aws-adapter/tests/account-store.test.ts +0 -276
- package/packages/cf-adapter/src/d1-account-store.ts +0 -198
- package/packages/cf-adapter/tests/d1-account-store.test.ts +0 -274
- package/packages/irc-core/tests/account-store.test.ts +0 -131
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UTF-8 byte accounting for IRC wire budgets.
|
|
3
|
+
*
|
|
4
|
+
* IRC line limits are byte budgets (RFC 1459 §2.3: 512 bytes including the
|
|
5
|
+
* trailing CR-LF; IRCv3 WebSocket framing: 510 content bytes). JavaScript
|
|
6
|
+
* strings expose UTF-16 code units via `.length`, so a 511-character frame
|
|
7
|
+
* of `é` measures 511 units but occupies 1022 bytes on the wire. Every
|
|
8
|
+
* line-budget seam in this package measures through {@link byteLength} so
|
|
9
|
+
* the cap means bytes.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** Shared `TextEncoder` (module-level singleton, reused — never per call). */
|
|
13
|
+
const TEXT_ENCODER = new TextEncoder();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Maximum size, in UTF-8 bytes, of a TCP byte-stream transport's retained
|
|
17
|
+
* partial-line buffer (the `transportBuffer` carried between chunks).
|
|
18
|
+
*
|
|
19
|
+
* The IRC line limit itself is 512 bytes (RFC 1459 §2.3), enforced by the
|
|
20
|
+
* parser AFTER line splitting — but that cap cannot fire on a peer that
|
|
21
|
+
* streams megabytes with no line terminator, which would grow the framing
|
|
22
|
+
* buffer unboundedly before any line exists to measure. This transport-level
|
|
23
|
+
* cap bounds that memory: well above a single legal line, with room for
|
|
24
|
+
* multi-line batching, small enough to make per-peer buffer abuse a
|
|
25
|
+
* non-amplifying nuisance. Shared by every raw TCP transport edge
|
|
26
|
+
* (irc-server's `TcpByteStreamTransport`, local-cli's `LineScanner`, the
|
|
27
|
+
* AWS NLB stream handler, and the Cloudflare TCP container origin) so all
|
|
28
|
+
* four enforce one identical budget.
|
|
29
|
+
*/
|
|
30
|
+
export const MAX_BUFFER_BYTES = 8 * 1024;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Returns the UTF-8 byte length of `s` — the size the wire transport
|
|
34
|
+
* actually sends and the quantity every IRC line budget is defined in.
|
|
35
|
+
*
|
|
36
|
+
* Cost note: this runs on the data-plane hot path for every inbound and
|
|
37
|
+
* outbound line. The module-level singleton `TextEncoder` above avoids
|
|
38
|
+
* per-call allocation, which is the dominant cost. Documented follow-up
|
|
39
|
+
* (profiling-gated, NOT implemented): an ASCII fast path
|
|
40
|
+
* (`s.length === byteLength(s)` style elision) to skip the encode for
|
|
41
|
+
* all-ASCII lines, which is the common case.
|
|
42
|
+
*/
|
|
43
|
+
export function byteLength(s: string): number {
|
|
44
|
+
return TEXT_ENCODER.encode(s).length;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Right-truncates `s` to at most `budget` UTF-8 bytes, never splitting a
|
|
49
|
+
* surrogate pair: code points are appended while they fit, so an astral
|
|
50
|
+
* character near the boundary is dropped whole and the result may land a
|
|
51
|
+
* few bytes under `budget`. For all-ASCII input the result is identical to
|
|
52
|
+
* `s.slice(0, budget)`. Only called on the (rare) over-budget path.
|
|
53
|
+
*/
|
|
54
|
+
export function truncateToByteBudget(s: string, budget: number): string {
|
|
55
|
+
if (budget <= 0) return '';
|
|
56
|
+
let used = 0;
|
|
57
|
+
let out = '';
|
|
58
|
+
for (const ch of s) {
|
|
59
|
+
const size = byteLength(ch);
|
|
60
|
+
if (used + size > budget) break;
|
|
61
|
+
used += size;
|
|
62
|
+
out += ch;
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel-name grammar and validation — the single source of truth shared
|
|
3
|
+
* by every command reducer that validates or routes on a channel target
|
|
4
|
+
* (JOIN, PART, TOPIC, NAMES, MODE, KICK, INVITE, WHO, LIST, ChanServ, and
|
|
5
|
+
* PRIVMSG/NOTICE target routing).
|
|
6
|
+
*
|
|
7
|
+
* Grammar: the first character must be a channel prefix (`#` or `&`); the
|
|
8
|
+
* rest may be any character except:
|
|
9
|
+
* - NUL and the C0 control range `\x00-\x1f` — RFC 1459 §1.3.3 forbids
|
|
10
|
+
* NUL, CR, and LF in channel names; a control byte embedded in a name
|
|
11
|
+
* is concatenated raw into outbound broadcasts and 353 numerics,
|
|
12
|
+
* truncating display in C-based clients and confusing naive parsers,
|
|
13
|
+
* - whitespace (`\s`),
|
|
14
|
+
* - comma (the multi-target list separator),
|
|
15
|
+
* - colon (the trailing-parameter marker).
|
|
16
|
+
*
|
|
17
|
+
* The length cap is measured in UTF-8 bytes — the unit the wire budget is
|
|
18
|
+
* defined in — and includes the `#`/`&` prefix, matching how the name
|
|
19
|
+
* occupies the line. RFC 2811 §1.3 suggests limiting channel names to 50
|
|
20
|
+
* bytes; the operative cap is `serverConfig.channelLen` (advertised as
|
|
21
|
+
* `CHANNELLEN=`, default 50).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { byteLength } from './bytes.js';
|
|
25
|
+
|
|
26
|
+
/** Channel-name grammar; see module docs for the rejected classes. */
|
|
27
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: intentional — this regex exists to reject NUL and C0 control chars (RFC 1459 §1.3.3) in channel names.
|
|
28
|
+
export const CHANNEL_NAME_RE = /^[#&][^\s,\x00-\x1f:]+$/u;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Returns true iff `name` matches the channel-name grammar and its UTF-8
|
|
32
|
+
* byte length (prefix included) is at most `maxLenBytes`.
|
|
33
|
+
*/
|
|
34
|
+
export function isValidChannelName(name: string, maxLenBytes: number): boolean {
|
|
35
|
+
if (name.length === 0 || byteLength(name) > maxLenBytes) return false;
|
|
36
|
+
return CHANNEL_NAME_RE.test(name);
|
|
37
|
+
}
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
export type { IrcMessage, IrcSource } from './messages.js';
|
|
2
|
-
export {
|
|
2
|
+
export { MAX_BUFFER_BYTES, byteLength, truncateToByteBudget } from './bytes.js';
|
|
3
|
+
export { CHANNEL_NAME_RE, isValidChannelName } from './channel-name.js';
|
|
4
|
+
export {
|
|
5
|
+
MAX_INPUT_BYTES,
|
|
6
|
+
MAX_TAG_BYTES,
|
|
7
|
+
MAX_TAGS,
|
|
8
|
+
IrcParseError,
|
|
9
|
+
parse,
|
|
10
|
+
parseTags,
|
|
11
|
+
unescapeTagValue,
|
|
12
|
+
} from './parser.js';
|
|
3
13
|
export { escapeTagValue, serialize, serializeFrame } from './serializer.js';
|
|
4
14
|
export {
|
|
5
15
|
MAX_LINE_BYTES,
|
|
@@ -7,6 +17,7 @@ export {
|
|
|
7
17
|
enforceLineLimit,
|
|
8
18
|
filterClientTags,
|
|
9
19
|
formatServerTime,
|
|
20
|
+
sanitizeLineText,
|
|
10
21
|
} from './outbound.js';
|
|
11
22
|
export { wrapBatch, type BatchFrame } from './batch.js';
|
|
12
23
|
export {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import type { RawLine } from '../effects.js';
|
|
19
19
|
import { MAX_WS_MESSAGE_BYTES, type WsFrameMode } from '../ws-framing.js';
|
|
20
|
+
import { byteLength, truncateToByteBudget } from './bytes.js';
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* RFC 1459 §2.3: maximum IRC line length in bytes, including the trailing
|
|
@@ -65,9 +66,35 @@ export function applyServerTime(line: RawLine, caps: ReadonlySet<string>, now: n
|
|
|
65
66
|
return { text: `@time=${stamp} ${line.text}` };
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Strips every carriage return from `line.text`.
|
|
71
|
+
*
|
|
72
|
+
* Defense-in-depth for paths that construct an outbound line from stored
|
|
73
|
+
* state (TOPIC text, realname in 311) which may predate inbound bare-CR
|
|
74
|
+
* rejection: {@link RawLine} text must never carry CR, because the
|
|
75
|
+
* transport layer appends its own CRLF framing per line, so an embedded CR
|
|
76
|
+
* would corrupt framing on RFC-compliant clients.
|
|
77
|
+
*
|
|
78
|
+
* CR is REMOVED rather than replaced with a space so stripping only ever
|
|
79
|
+
* shrinks the line — every downstream byte budget still holds. LF is left
|
|
80
|
+
* untouched (the hardening is deliberately CR-only).
|
|
81
|
+
*
|
|
82
|
+
* The input {@link RawLine} is never mutated; the original object is
|
|
83
|
+
* returned unchanged when the text contains no CR.
|
|
84
|
+
*/
|
|
85
|
+
export function sanitizeLineText(line: RawLine): RawLine {
|
|
86
|
+
if (!line.text.includes('\r')) return line;
|
|
87
|
+
return { text: line.text.replace(/\r/gu, '') };
|
|
88
|
+
}
|
|
89
|
+
|
|
68
90
|
/**
|
|
69
91
|
* Enforces the IRC line-length budget on `line.text`.
|
|
70
92
|
*
|
|
93
|
+
* CR sanitization ({@link sanitizeLineText}) runs FIRST, so the budget is
|
|
94
|
+
* measured on the sanitized text: a line that fits after CR removal is not
|
|
95
|
+
* truncated, and a line that still exceeds the budget is truncated from
|
|
96
|
+
* already-sanitized text.
|
|
97
|
+
*
|
|
71
98
|
* For recipients that negotiated `message-tags`, the tag section (the
|
|
72
99
|
* leading `@... ` prefix) is part of the wire budget and is preserved
|
|
73
100
|
* intact; truncation happens at the end of the trailing parameter only.
|
|
@@ -83,36 +110,42 @@ export function applyServerTime(line: RawLine, caps: ReadonlySet<string>, now: n
|
|
|
83
110
|
*
|
|
84
111
|
* The returned {@link RawLine} is the original object when no truncation
|
|
85
112
|
* was needed, otherwise a fresh object with the truncated text.
|
|
113
|
+
*
|
|
114
|
+
* The budget is measured in UTF-8 bytes (the wire encoding), not UTF-16
|
|
115
|
+
* code units: a line of multi-byte characters that fits the unit count can
|
|
116
|
+
* still exceed the byte ceiling, and truncation re-encodes to the byte
|
|
117
|
+
* budget so the emitted text always fits it.
|
|
86
118
|
*/
|
|
87
119
|
export function enforceLineLimit(
|
|
88
120
|
line: RawLine,
|
|
89
121
|
caps: ReadonlySet<string>,
|
|
90
122
|
mode?: WsFrameMode,
|
|
91
123
|
): RawLine {
|
|
124
|
+
const sanitized = sanitizeLineText(line);
|
|
92
125
|
const budget = isSpecMode(mode) ? MAX_WS_MESSAGE_BYTES : MAX_LINE_BYTES - TRAILING_CRLF_LEN;
|
|
93
|
-
if (
|
|
126
|
+
if (byteLength(sanitized.text) <= budget) return sanitized;
|
|
94
127
|
|
|
95
|
-
const hasTags = caps.has('message-tags') &&
|
|
128
|
+
const hasTags = caps.has('message-tags') && sanitized.text.startsWith('@');
|
|
96
129
|
if (!hasTags) {
|
|
97
|
-
return { text:
|
|
130
|
+
return { text: truncateToByteBudget(sanitized.text, budget) };
|
|
98
131
|
}
|
|
99
132
|
|
|
100
133
|
// Preserve the tag section verbatim; truncate only the post-tag body.
|
|
101
|
-
const spaceIdx =
|
|
134
|
+
const spaceIdx = sanitized.text.indexOf(' ');
|
|
102
135
|
if (spaceIdx === -1) {
|
|
103
136
|
// Malformed: a `@`-prefixed line with no space. Truncate from the right.
|
|
104
|
-
return { text:
|
|
137
|
+
return { text: truncateToByteBudget(sanitized.text, budget) };
|
|
105
138
|
}
|
|
106
|
-
const tagSection =
|
|
107
|
-
const body =
|
|
108
|
-
const bodyBudget = budget - tagSection
|
|
139
|
+
const tagSection = sanitized.text.slice(0, spaceIdx + 1); // include trailing space
|
|
140
|
+
const body = sanitized.text.slice(spaceIdx + 1);
|
|
141
|
+
const bodyBudget = budget - byteLength(tagSection);
|
|
109
142
|
if (bodyBudget < 0) {
|
|
110
143
|
// Pathological: the tag section alone exceeds the budget. Drop the body
|
|
111
144
|
// entirely and truncate the tag section itself so the wire contract
|
|
112
145
|
// (≤ budget bytes) still holds.
|
|
113
|
-
return { text:
|
|
146
|
+
return { text: truncateToByteBudget(sanitized.text, budget) };
|
|
114
147
|
}
|
|
115
|
-
return { text: `${tagSection}${body
|
|
148
|
+
return { text: `${tagSection}${truncateToByteBudget(body, bodyBudget)}` };
|
|
116
149
|
}
|
|
117
150
|
|
|
118
151
|
/**
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { byteLength } from './bytes.js';
|
|
1
2
|
import type { IrcMessage, IrcSource } from './messages.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -29,6 +30,27 @@ export class IrcParseError extends Error {
|
|
|
29
30
|
*/
|
|
30
31
|
export const MAX_INPUT_BYTES = 512;
|
|
31
32
|
|
|
33
|
+
/**
|
|
34
|
+
* IRCv3 Message Tags spec: recommended maximum number of tags on a single
|
|
35
|
+
* message. Every tag is unescaped and (when `message-tags` is negotiated)
|
|
36
|
+
* re-serialized onto downstream lines, so tag count is an amplification
|
|
37
|
+
* lever; a client packing dozens of short tags per line is rejected as a
|
|
38
|
+
* protocol violation rather than parsed.
|
|
39
|
+
*/
|
|
40
|
+
export const MAX_TAGS = 15;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* IRCv3 Message Tags spec: recommended maximum size, in UTF-8 bytes, of a
|
|
44
|
+
* message's tag section — the tag data between the leading `@` and the
|
|
45
|
+
* space that terminates the section (both excluded from the measurement).
|
|
46
|
+
* Measured in bytes (not UTF-16 code units) via {@link byteLength} so
|
|
47
|
+
* multi-byte tag content cannot double the effective budget. Defense in
|
|
48
|
+
* depth: the RFC 1459 {@link MAX_INPUT_BYTES} line cap already bounds any
|
|
49
|
+
* section that reaches {@link parse}; this guard holds the spec budget for
|
|
50
|
+
* callers of {@link parseTags} independent of the line cap.
|
|
51
|
+
*/
|
|
52
|
+
export const MAX_TAG_BYTES = 8192;
|
|
53
|
+
|
|
32
54
|
const TAG_ESCAPES: Readonly<Record<string, string>> = {
|
|
33
55
|
':': ';',
|
|
34
56
|
s: ' ',
|
|
@@ -58,15 +80,46 @@ export function unescapeTagValue(value: string): string {
|
|
|
58
80
|
return out;
|
|
59
81
|
}
|
|
60
82
|
|
|
61
|
-
|
|
62
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Parses the tag section between the leading `@` and its terminating space
|
|
85
|
+
* into a null-prototype tag map. Exported for direct validation at this
|
|
86
|
+
* seam: the byte budget is unreachable through {@link parse} (the 512-byte
|
|
87
|
+
* line cap fires first), so the tag-section guards are unit-tested here.
|
|
88
|
+
*/
|
|
89
|
+
export function parseTags(section: string, line: string): Record<string, string> {
|
|
90
|
+
// Null prototype: tag keys are attacker-controlled, so a plain object
|
|
91
|
+
// literal would route magic keys (`__proto__`, `constructor`, `toString`,
|
|
92
|
+
// `hasOwnProperty`, …) through `Object.prototype` accessors instead of
|
|
93
|
+
// creating own properties — `__proto__=x` would be silently dropped by
|
|
94
|
+
// the setter, and the others would shadow inherited methods.
|
|
95
|
+
const tags: Record<string, string> = Object.create(null);
|
|
96
|
+
const sectionBytes = byteLength(section);
|
|
97
|
+
if (sectionBytes > MAX_TAG_BYTES) {
|
|
98
|
+
throw new IrcParseError(
|
|
99
|
+
`tag section exceeds ${MAX_TAG_BYTES}-byte budget (${sectionBytes} bytes)`,
|
|
100
|
+
line,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
let tagCount = 0;
|
|
63
104
|
for (const entry of section.split(';')) {
|
|
64
105
|
if (entry === '') continue;
|
|
106
|
+
tagCount++;
|
|
107
|
+
if (tagCount > MAX_TAGS) {
|
|
108
|
+
throw new IrcParseError(`too many tags (${tagCount} > ${MAX_TAGS})`, line);
|
|
109
|
+
}
|
|
65
110
|
const eq = entry.indexOf('=');
|
|
66
111
|
if (eq === -1) {
|
|
67
112
|
tags[entry] = '';
|
|
68
113
|
} else {
|
|
69
|
-
|
|
114
|
+
const value = unescapeTagValue(entry.slice(eq + 1));
|
|
115
|
+
// IRCv3 Message Tags: CR, LF and NUL MUST NOT appear in tag values —
|
|
116
|
+
// not even via their `\r` / `\n` escape sequences. The line-level
|
|
117
|
+
// bare-CR guard cannot see these: the wire bytes are backslash + 'r',
|
|
118
|
+
// not CR, so the check fires on the UNESCAPED value.
|
|
119
|
+
if (/[\r\n\0]/u.test(value)) {
|
|
120
|
+
throw new IrcParseError('tag value contains forbidden CR/LF/NUL', line);
|
|
121
|
+
}
|
|
122
|
+
tags[entry.slice(0, eq)] = value;
|
|
70
123
|
}
|
|
71
124
|
}
|
|
72
125
|
return tags;
|
|
@@ -105,7 +158,9 @@ const COMMAND_RE = /^(?:[A-Za-z]+|[0-9]{3})$/;
|
|
|
105
158
|
*
|
|
106
159
|
* Lenient where real-world clients are: tolerates missing CR/LF and collapses
|
|
107
160
|
* accidental runs of spaces between tokens. Strict where the grammar is: a
|
|
108
|
-
* command token is required and must be all-letters or exactly three digits
|
|
161
|
+
* command token is required and must be all-letters or exactly three digits,
|
|
162
|
+
* and a bare CR (any CR left after the trailing terminator strip) is a
|
|
163
|
+
* protocol violation.
|
|
109
164
|
*/
|
|
110
165
|
export function parse(line: string): IrcMessage {
|
|
111
166
|
// Strip any trailing CR/LF (frame tolerance for ws text frames or TCP lines).
|
|
@@ -114,13 +169,27 @@ export function parse(line: string): IrcMessage {
|
|
|
114
169
|
throw new IrcParseError('empty message', line);
|
|
115
170
|
}
|
|
116
171
|
|
|
172
|
+
// After the trailing terminator strip, any remaining CR is bare by
|
|
173
|
+
// definition: a mid-line CR-LF would have split the frame at the
|
|
174
|
+
// transport layer, so a surviving CR means frame control characters are
|
|
175
|
+
// being smuggled inside message content (middle/trailing params, tag
|
|
176
|
+
// sections, source prefixes). Reject — such content would otherwise be
|
|
177
|
+
// stored and echoed verbatim into outbound lines, corrupting framing on
|
|
178
|
+
// RFC-compliant clients.
|
|
179
|
+
if (stripped.includes('\r')) {
|
|
180
|
+
throw new IrcParseError('bare CR in message', line);
|
|
181
|
+
}
|
|
182
|
+
|
|
117
183
|
// RFC 1459 §2.3 hard cap: reject any single message whose post-CRLF body
|
|
118
|
-
// exceeds {@link MAX_INPUT_BYTES} bytes.
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
184
|
+
// exceeds {@link MAX_INPUT_BYTES} bytes. Measured in UTF-8 bytes (the wire
|
|
185
|
+
// encoding), not UTF-16 code units, so multi-byte content cannot double
|
|
186
|
+
// the effective cap. Prevents memory and processing abuse from over-long
|
|
187
|
+
// inputs and matches the symmetric cap enforced on outbound lines by
|
|
188
|
+
// `outbound.ts`.
|
|
189
|
+
const inputBytes = byteLength(stripped);
|
|
190
|
+
if (inputBytes > MAX_INPUT_BYTES) {
|
|
122
191
|
throw new IrcParseError(
|
|
123
|
-
`message exceeds ${MAX_INPUT_BYTES}-byte input cap (${
|
|
192
|
+
`message exceeds ${MAX_INPUT_BYTES}-byte input cap (${inputBytes} bytes)`,
|
|
124
193
|
line,
|
|
125
194
|
);
|
|
126
195
|
}
|
|
@@ -133,7 +202,7 @@ export function parse(line: string): IrcMessage {
|
|
|
133
202
|
if (space === -1) {
|
|
134
203
|
throw new IrcParseError('tag section with no message body', line);
|
|
135
204
|
}
|
|
136
|
-
tags = parseTags(rest.slice(1, space));
|
|
205
|
+
tags = parseTags(rest.slice(1, space), line);
|
|
137
206
|
rest = rest.slice(space + 1).trimStart();
|
|
138
207
|
}
|
|
139
208
|
|
|
@@ -93,6 +93,19 @@ export interface ConnectionState {
|
|
|
93
93
|
* and consumed once the final short chunk (or `+`) arrives.
|
|
94
94
|
*/
|
|
95
95
|
saslBuffer?: string;
|
|
96
|
+
/**
|
|
97
|
+
* Epoch-ms timestamps of failed SASL attempts on this connection (one
|
|
98
|
+
* entry per `904 ERR_SASLFAIL`), pruned to the configured sliding window
|
|
99
|
+
* on every recording. Once the array reaches the configured
|
|
100
|
+
* `sasl.maxFailures` ceiling within the window, the SASL reducer
|
|
101
|
+
* disconnects the connection (online brute-force hardening).
|
|
102
|
+
*
|
|
103
|
+
* Pre-registration scratch: SASL only runs before registration
|
|
104
|
+
* completes, and adapters do not persist pre-registration scratch fields
|
|
105
|
+
* (`saslMech`/`saslBuffer` are likewise absent from the persisted
|
|
106
|
+
* shape), so a reconnect starts with a clean tracker by construction.
|
|
107
|
+
*/
|
|
108
|
+
saslFailures?: number[];
|
|
96
109
|
registration: RegistrationState;
|
|
97
110
|
/**
|
|
98
111
|
* True once the client has sent `CAP LS` or `CAP REQ` and not yet sent
|