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
|
@@ -26,7 +26,6 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import {
|
|
29
|
-
type AccountStore,
|
|
30
29
|
type AwayStore,
|
|
31
30
|
CHANSERV_NICK,
|
|
32
31
|
type ChanName,
|
|
@@ -36,6 +35,7 @@ import {
|
|
|
36
35
|
type ConnectionState,
|
|
37
36
|
DEFAULT_MONITOR_LIMIT,
|
|
38
37
|
DEFAULT_MULTILINE_MAX_BYTES,
|
|
38
|
+
DEFAULT_MULTILINE_MAX_ENTRIES,
|
|
39
39
|
Effect,
|
|
40
40
|
type Effect as EffectType,
|
|
41
41
|
EmptyMotdProvider,
|
|
@@ -55,6 +55,7 @@ import {
|
|
|
55
55
|
NoopLoggerInstance,
|
|
56
56
|
Numerics,
|
|
57
57
|
OPERSERV_NICK,
|
|
58
|
+
type OperFailureStats,
|
|
58
59
|
type RawLine,
|
|
59
60
|
type Reducer,
|
|
60
61
|
SHORTCUT_TO_SERVICE,
|
|
@@ -64,6 +65,7 @@ import {
|
|
|
64
65
|
applyLabelToEffects,
|
|
65
66
|
buildCtx,
|
|
66
67
|
buildMultilineFanout,
|
|
68
|
+
byteLength,
|
|
67
69
|
caseFold,
|
|
68
70
|
chathistoryReducer,
|
|
69
71
|
extractLabel,
|
|
@@ -92,6 +94,7 @@ import {
|
|
|
92
94
|
} from '@serverless-ircd/irc-core';
|
|
93
95
|
import { dispatch } from './dispatch.js';
|
|
94
96
|
import type { DispatchOptions } from './dispatch.js';
|
|
97
|
+
import { parseErrorReason, truncateToken } from './redact.js';
|
|
95
98
|
import { type RoutedReducers, buildRoutedReducers } from './routing.js';
|
|
96
99
|
import type { IrcRuntime } from './runtime.js';
|
|
97
100
|
import { type Transport, WsTextFrameTransport } from './transport.js';
|
|
@@ -150,16 +153,6 @@ export interface ConnectionActorOptions {
|
|
|
150
153
|
* opt in.
|
|
151
154
|
*/
|
|
152
155
|
readonly messages?: MessageStore;
|
|
153
|
-
/**
|
|
154
|
-
* SASL account verification source (absent when no accounts are
|
|
155
|
-
* configured). When bound, `ctx.accounts` is defined so the
|
|
156
|
-
* `AUTHENTICATE` reducer can verify PLAIN credentials end-to-end.
|
|
157
|
-
* Omitted (→ `ctx.accounts` undefined) preserves the existing
|
|
158
|
-
* behaviour for callers/tests that do not opt in: a client completing
|
|
159
|
-
* `AUTHENTICATE PLAIN` receives `904 ERR_SASLFAIL` because no store
|
|
160
|
-
* can vouch for the credentials.
|
|
161
|
-
*/
|
|
162
|
-
readonly accounts?: AccountStore;
|
|
163
156
|
/**
|
|
164
157
|
* mTLS client-cert identity source for SASL EXTERNAL (absent when mTLS
|
|
165
158
|
* is not configured). When bound, `ctx.mtlsIdentity` is defined so the
|
|
@@ -216,6 +209,19 @@ export interface ConnectionActorOptions {
|
|
|
216
209
|
* watched nick later connects or quits.
|
|
217
210
|
*/
|
|
218
211
|
readonly monitor?: MonitorStore;
|
|
212
|
+
/**
|
|
213
|
+
* Per-IP failed-`OPER` counter shared across every connection's actor
|
|
214
|
+
* (absent when the deployment's runtime does not maintain one — e.g.
|
|
215
|
+
* per-connection Durable Object adapters). When bound, `ctx.operFailures`
|
|
216
|
+
* is defined so the OPER reducer throttles brute-force guessing per
|
|
217
|
+
* source host: after `serverConfig.oper.maxFailures` credential
|
|
218
|
+
* failures within `oper.failureWindowSeconds`, further attempts from
|
|
219
|
+
* that host are rejected with `491` before the credential store is
|
|
220
|
+
* consulted and a `BroadcastOperNotice` notifies every oper. The
|
|
221
|
+
* instance MUST share the actor's clock so window arithmetic is
|
|
222
|
+
* coherent.
|
|
223
|
+
*/
|
|
224
|
+
readonly operFailures?: OperFailureStats;
|
|
219
225
|
/**
|
|
220
226
|
* Network-statistics source for the `LUSERS` and `STATS` commands
|
|
221
227
|
* (absent when no stats backend is wired). When bound, the actor
|
|
@@ -271,12 +277,12 @@ export class ConnectionActor {
|
|
|
271
277
|
private readonly ids: IdFactory;
|
|
272
278
|
private readonly motd: MotdProvider;
|
|
273
279
|
private readonly messages: MessageStore | undefined;
|
|
274
|
-
private readonly accounts: AccountStore | undefined;
|
|
275
280
|
private readonly mtlsIdentity: MtlsIdentityProvider | undefined;
|
|
276
281
|
private readonly history: NickHistoryStore | undefined;
|
|
277
282
|
private readonly away: AwayStore | undefined;
|
|
278
283
|
private readonly services: ServicesStore | undefined;
|
|
279
284
|
private readonly monitor: MonitorStore | undefined;
|
|
285
|
+
private readonly operFailures: OperFailureStats | undefined;
|
|
280
286
|
private readonly stats: ServerStats | undefined;
|
|
281
287
|
private readonly reducers: RoutedReducers;
|
|
282
288
|
/**
|
|
@@ -302,9 +308,10 @@ export class ConnectionActor {
|
|
|
302
308
|
* In-progress IRCv3 `draft/multiline` batch, if any. Set when the client
|
|
303
309
|
* sends `BATCH +id draft/multiline :<target>` (and the connection
|
|
304
310
|
* negotiated the cap), populated by each intervening `PRIVMSG`/`NOTICE`,
|
|
305
|
-
* and cleared when the closing `BATCH -id` is finalized
|
|
306
|
-
*
|
|
307
|
-
*
|
|
311
|
+
* and cleared when the closing `BATCH -id` is finalized — or earlier on
|
|
312
|
+
* an incremental-budget abort or connection teardown ({@link dispose}).
|
|
313
|
+
* While set, the actor diverts `PRIVMSG`/`NOTICE` into the accumulator
|
|
314
|
+
* instead of running the normal channel/user reducers.
|
|
308
315
|
*/
|
|
309
316
|
private openMultilineBatch:
|
|
310
317
|
| {
|
|
@@ -325,12 +332,12 @@ export class ConnectionActor {
|
|
|
325
332
|
this.ids = opts.ids;
|
|
326
333
|
this.motd = opts.motd ?? EmptyMotdProvider;
|
|
327
334
|
this.messages = opts.messages;
|
|
328
|
-
this.accounts = opts.accounts;
|
|
329
335
|
this.mtlsIdentity = opts.mtlsIdentity;
|
|
330
336
|
this.history = opts.history;
|
|
331
337
|
this.away = opts.away;
|
|
332
338
|
this.services = opts.services;
|
|
333
339
|
this.monitor = opts.monitor;
|
|
340
|
+
this.operFailures = opts.operFailures;
|
|
334
341
|
this.stats = opts.stats;
|
|
335
342
|
this.logger = opts.logger ?? NoopLoggerInstance;
|
|
336
343
|
this.pinnedTraceId = opts.traceId;
|
|
@@ -387,23 +394,41 @@ export class ConnectionActor {
|
|
|
387
394
|
await this.receive(text);
|
|
388
395
|
}
|
|
389
396
|
|
|
397
|
+
/**
|
|
398
|
+
* Connection teardown hook. Clears any state that must not outlive the
|
|
399
|
+
* connection — currently an abandoned `draft/multiline` batch (a client
|
|
400
|
+
* that disconnects between `BATCH +id` and `BATCH -id` would otherwise
|
|
401
|
+
* leave the accumulator mid-flight). Adapters call this when the
|
|
402
|
+
* underlying socket closes; safe to call at any lifecycle point.
|
|
403
|
+
*/
|
|
404
|
+
dispose(): void {
|
|
405
|
+
this.openMultilineBatch = undefined;
|
|
406
|
+
}
|
|
407
|
+
|
|
390
408
|
private async processLine(line: string, log: Logger, traceId: string): Promise<void> {
|
|
391
409
|
let msg: IrcMessage;
|
|
392
410
|
let parseFailed = false;
|
|
411
|
+
let parseErr: unknown;
|
|
393
412
|
try {
|
|
394
413
|
msg = parse(line);
|
|
395
|
-
} catch {
|
|
414
|
+
} catch (err) {
|
|
396
415
|
// `parse` only throws `IrcParseError` (every throw inside the parser
|
|
397
416
|
// constructs one); the untyped catch is the strict-mode narrowing
|
|
398
417
|
// artifact for `unknown`. Every parse failure becomes a `421` so the
|
|
399
418
|
// actor never crashes on a single bad line (acceptance criteria).
|
|
400
419
|
parseFailed = true;
|
|
420
|
+
parseErr = err;
|
|
401
421
|
msg = { tags: {}, command: '', params: [] };
|
|
402
422
|
}
|
|
403
423
|
if (parseFailed) {
|
|
424
|
+
// The raw line is NEVER logged: a hostile client can smuggle a
|
|
425
|
+
// credential past the parser (e.g. a malformed line whose payload
|
|
426
|
+
// carries `PASS <secret>`). Operators get the leading token
|
|
427
|
+
// (capped), the length, and a stable reason code instead.
|
|
404
428
|
log.warn('frame.parse-error', {
|
|
405
|
-
line,
|
|
406
|
-
|
|
429
|
+
token: truncateToken(firstToken(line)),
|
|
430
|
+
length: line.length,
|
|
431
|
+
reason: parseErrorReason(parseErr),
|
|
407
432
|
});
|
|
408
433
|
const token = firstToken(line);
|
|
409
434
|
await dispatch(
|
|
@@ -511,11 +536,11 @@ export class ConnectionActor {
|
|
|
511
536
|
motd: this.motd,
|
|
512
537
|
connection: this.state,
|
|
513
538
|
...(this.messages !== undefined ? { messages: this.messages } : {}),
|
|
514
|
-
...(this.accounts !== undefined ? { accounts: this.accounts } : {}),
|
|
515
539
|
...(this.mtlsIdentity !== undefined ? { mtlsIdentity: this.mtlsIdentity } : {}),
|
|
516
540
|
...(this.history !== undefined ? { history: this.history } : {}),
|
|
517
541
|
...(this.away !== undefined ? { away: this.away } : {}),
|
|
518
542
|
...(this.services !== undefined ? { services: this.services } : {}),
|
|
543
|
+
...(this.operFailures !== undefined ? { operFailures: this.operFailures } : {}),
|
|
519
544
|
...(label !== undefined ? { label } : {}),
|
|
520
545
|
});
|
|
521
546
|
|
|
@@ -605,6 +630,7 @@ export class ConnectionActor {
|
|
|
605
630
|
return this.routeMultiChannel(
|
|
606
631
|
msg,
|
|
607
632
|
(chan, singleParamMsg) => this.reducers.part(chan, singleParamMsg, ctx).effects,
|
|
633
|
+
ctx,
|
|
608
634
|
);
|
|
609
635
|
case 'TOPIC':
|
|
610
636
|
return this.routeSingleChannel(msg, (chan) => this.reducers.topic(chan, msg, ctx).effects);
|
|
@@ -1123,6 +1149,12 @@ export class ConnectionActor {
|
|
|
1123
1149
|
* `draft/multiline` batch. The inner line's target must match the batch
|
|
1124
1150
|
* target and (after the first line) its command must match too; a
|
|
1125
1151
|
* mismatch aborts the batch with a `FAIL` and clears the accumulator.
|
|
1152
|
+
*
|
|
1153
|
+
* The byte budget (`multilineMaxBytes`) and entry ceiling
|
|
1154
|
+
* (`multilineMaxEntries`) are enforced incrementally on each push: the
|
|
1155
|
+
* line that would push the batch past either limit aborts it immediately
|
|
1156
|
+
* (synthesized `BATCH -id` + `FAIL`) instead of being buffered for a
|
|
1157
|
+
* close the client may never send.
|
|
1126
1158
|
*/
|
|
1127
1159
|
private accumulateMultilineLine(msg: IrcMessage): EffectType[] {
|
|
1128
1160
|
const open = this.openMultilineBatch;
|
|
@@ -1155,10 +1187,46 @@ export class ConnectionActor {
|
|
|
1155
1187
|
]),
|
|
1156
1188
|
];
|
|
1157
1189
|
}
|
|
1190
|
+
// Incremental budget enforcement: the byte budget and entry ceiling are
|
|
1191
|
+
// checked on every push, not just on close, so a client cannot buffer
|
|
1192
|
+
// unbounded content in a batch it never closes.
|
|
1193
|
+
const maxEntries = this.serverConfig.multilineMaxEntries ?? DEFAULT_MULTILINE_MAX_ENTRIES;
|
|
1194
|
+
if (open.entries.length >= maxEntries) {
|
|
1195
|
+
return this.abortMultilineBatch(open, 'MULTILINE_MAX_LINES', maxEntries, 'line');
|
|
1196
|
+
}
|
|
1197
|
+
const maxBytes = this.serverConfig.multilineMaxBytes ?? DEFAULT_MULTILINE_MAX_BYTES;
|
|
1198
|
+
const candidateBytes =
|
|
1199
|
+
multilineByteLength(open.entries) + (open.entries.length === 0 ? 0 : 1) + byteLength(text);
|
|
1200
|
+
if (candidateBytes > maxBytes) {
|
|
1201
|
+
return this.abortMultilineBatch(open, 'MULTILINE_MAX_BYTES', maxBytes, 'byte');
|
|
1202
|
+
}
|
|
1158
1203
|
open.entries.push({ text });
|
|
1159
1204
|
return [];
|
|
1160
1205
|
}
|
|
1161
1206
|
|
|
1207
|
+
/**
|
|
1208
|
+
* Aborts an open `draft/multiline` batch that just violated the
|
|
1209
|
+
* incremental budget: clears the accumulator and emits a synthesized
|
|
1210
|
+
* `BATCH -id` close (so batch-aware clients exit the batch) followed by
|
|
1211
|
+
* the `FAIL` rejection carrying the limit that was exceeded.
|
|
1212
|
+
*/
|
|
1213
|
+
private abortMultilineBatch(
|
|
1214
|
+
open: { readonly id: string; readonly target: string },
|
|
1215
|
+
code: 'MULTILINE_MAX_BYTES' | 'MULTILINE_MAX_LINES',
|
|
1216
|
+
limit: number,
|
|
1217
|
+
unit: 'byte' | 'line',
|
|
1218
|
+
): EffectType[] {
|
|
1219
|
+
this.openMultilineBatch = undefined;
|
|
1220
|
+
return [
|
|
1221
|
+
Effect.send(this.state.id, [
|
|
1222
|
+
{ text: `BATCH -${open.id}` },
|
|
1223
|
+
{
|
|
1224
|
+
text: `:${this.serverConfig.serverName} FAIL BATCH ${code} ${open.target} :Multi-line batch exceeds the ${limit} ${unit} limit`,
|
|
1225
|
+
},
|
|
1226
|
+
]),
|
|
1227
|
+
];
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1162
1230
|
/**
|
|
1163
1231
|
* Finalizes an open `draft/multiline` batch on `BATCH -id`. Channel targets
|
|
1164
1232
|
* delegate to the pure {@link finalizeMultilineBatch} reducer (cap-split
|
|
@@ -1324,6 +1392,7 @@ export class ConnectionActor {
|
|
|
1324
1392
|
private routeMultiChannel(
|
|
1325
1393
|
msg: IrcMessage,
|
|
1326
1394
|
run: (chan: ChannelState, singleParamMsg: IrcMessage) => EffectType[],
|
|
1395
|
+
ctx: ReturnType<typeof buildCtx>,
|
|
1327
1396
|
): EffectType[] {
|
|
1328
1397
|
const effects: EffectType[] = [];
|
|
1329
1398
|
const rawParam = msg.params[0] ?? '';
|
|
@@ -1336,11 +1405,19 @@ export class ConnectionActor {
|
|
|
1336
1405
|
// 461 ERR_NEEDMOREPARAMS rather than the actor silently doing nothing.
|
|
1337
1406
|
return run(this.channels.getOrCreateChannel(''), msg);
|
|
1338
1407
|
}
|
|
1339
|
-
|
|
1408
|
+
// Target cap (e.g. `PART #a,#b,#c,…`): only the first
|
|
1409
|
+
// `maxTargetsPerCommand` channels are processed; every dropped
|
|
1410
|
+
// channel gets a `407 ERR_TOOMANYTARGETS` so the client can see the
|
|
1411
|
+
// truncation instead of the part being silently ignored.
|
|
1412
|
+
const cap = ctx.serverConfig.maxTargetsPerCommand;
|
|
1413
|
+
for (const name of names.slice(0, cap)) {
|
|
1340
1414
|
const chan = this.channels.getOrCreateChannel(name);
|
|
1341
1415
|
const singleParamMsg: IrcMessage = { ...msg, params: [name, ...tail] };
|
|
1342
1416
|
effects.push(...run(chan, singleParamMsg));
|
|
1343
1417
|
}
|
|
1418
|
+
for (const dropped of names.slice(cap)) {
|
|
1419
|
+
effects.push(this.tooManyTargetsEffect(dropped, ctx));
|
|
1420
|
+
}
|
|
1344
1421
|
return effects;
|
|
1345
1422
|
}
|
|
1346
1423
|
|
|
@@ -1370,7 +1447,12 @@ export class ConnectionActor {
|
|
|
1370
1447
|
// proper 411 ERR_NORECIPIENT rather than silently doing nothing.
|
|
1371
1448
|
return userReducer(this.state, msg, ctx).effects;
|
|
1372
1449
|
}
|
|
1373
|
-
|
|
1450
|
+
// Target cap (e.g. `PRIVMSG #a,#b,#c,…`): only the first
|
|
1451
|
+
// `maxTargetsPerCommand` targets are routed; every dropped target
|
|
1452
|
+
// gets a `407 ERR_TOOMANYTARGETS` so the client can see the
|
|
1453
|
+
// truncation instead of the message being silently ignored.
|
|
1454
|
+
const cap = ctx.serverConfig.maxTargetsPerCommand;
|
|
1455
|
+
for (const target of targets.slice(0, cap)) {
|
|
1374
1456
|
const singleParamMsg: IrcMessage = { ...msg, params: [target, ...tail] };
|
|
1375
1457
|
const serviceReducer = this.serviceReducerFor(target, msg.command);
|
|
1376
1458
|
if (isChannelTarget(target)) {
|
|
@@ -1392,9 +1474,28 @@ export class ConnectionActor {
|
|
|
1392
1474
|
effects.push(...userReducer(this.state, singleParamMsg, ctx).effects);
|
|
1393
1475
|
}
|
|
1394
1476
|
}
|
|
1477
|
+
for (const dropped of targets.slice(cap)) {
|
|
1478
|
+
effects.push(this.tooManyTargetsEffect(dropped, ctx));
|
|
1479
|
+
}
|
|
1395
1480
|
return effects;
|
|
1396
1481
|
}
|
|
1397
1482
|
|
|
1483
|
+
/**
|
|
1484
|
+
* Builds the `407 ERR_TOOMANYTARGETS` Send effect for one target dropped
|
|
1485
|
+
* by the `maxTargetsPerCommand` cap in {@link routeMessageTarget} /
|
|
1486
|
+
* {@link routeMultiChannel}. Emitted once per dropped target after the
|
|
1487
|
+
* allowed targets have been routed, so the client learns exactly which
|
|
1488
|
+
* targets were not delivered to.
|
|
1489
|
+
*/
|
|
1490
|
+
private tooManyTargetsEffect(target: string, ctx: ReturnType<typeof buildCtx>): EffectType {
|
|
1491
|
+
const nick = ctx.connection.nick ?? '*';
|
|
1492
|
+
const code = Numerics.ERR_TOOMANYTARGETS.toString().padStart(3, '0');
|
|
1493
|
+
const line: RawLine = {
|
|
1494
|
+
text: `:${this.serverConfig.serverName} ${code} ${nick} ${target} :Too many targets`,
|
|
1495
|
+
};
|
|
1496
|
+
return Effect.send(ctx.connId, [line]);
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1398
1499
|
/**
|
|
1399
1500
|
* Resolves the service pseudo-client reducer for a nick target, or
|
|
1400
1501
|
* `undefined` when the target is not a service nick / the command is not
|
|
@@ -53,6 +53,7 @@ const handlers: { [K in Effect['tag']]: Handler<Extract<Effect, { tag: K }>> } =
|
|
|
53
53
|
ReleaseNick: (e, r, m) => releaseNickEtc(e, r, m),
|
|
54
54
|
ApplyChannelDelta: (e, r) => r.applyChannelDelta(e.chan, e.delta),
|
|
55
55
|
BroadcastWallops: (e, r) => r.broadcastWallops(e.lines, e.except),
|
|
56
|
+
BroadcastOperNotice: (e, r) => r.broadcastOperNotice(e.lines, e.except),
|
|
56
57
|
EnforceNick: (e, r, _m, opts) => enforceNick(e, r, opts),
|
|
57
58
|
};
|
|
58
59
|
|
|
@@ -19,3 +19,10 @@ export {
|
|
|
19
19
|
export type { Transport } from './transport.js';
|
|
20
20
|
export { buildRoutedReducers, resolveFloodControl } from './routing.js';
|
|
21
21
|
export type { RoutedReducers } from './routing.js';
|
|
22
|
+
export {
|
|
23
|
+
MAX_LOG_TOKEN_CHARS,
|
|
24
|
+
parseErrorReason,
|
|
25
|
+
REDACTED,
|
|
26
|
+
redactLine,
|
|
27
|
+
truncateToken,
|
|
28
|
+
} from './redact.js';
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Log-scrubbing helpers — the credential-exfiltration guards for log
|
|
3
|
+
* payloads.
|
|
4
|
+
*
|
|
5
|
+
* Threat model: a hostile client can deliberately smuggle secret
|
|
6
|
+
* material into server logs by sending a line whose payload carries a
|
|
7
|
+
* credential (e.g. `PASS hunter2` inside a frame that trips the parser).
|
|
8
|
+
* Anything derived from inbound client input that reaches a warn/error
|
|
9
|
+
* log record must pass through these helpers first:
|
|
10
|
+
*
|
|
11
|
+
* - A site that must log a line logs `redactLine(line)` — the command
|
|
12
|
+
* verb (and NickServ target + subcommand) survive for debuggability;
|
|
13
|
+
* the credential-bearing parameters are replaced with `[REDACTED]`.
|
|
14
|
+
* - Parse diagnostics never log the line at all: they log
|
|
15
|
+
* `truncateToken(firstToken(line))` + the line length +
|
|
16
|
+
* `parseErrorReason(err)` so operators can correlate without ever
|
|
17
|
+
* storing client payload.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** Placeholder substituted for redacted credential material. */
|
|
21
|
+
export const REDACTED = '[REDACTED]';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Max characters of a leading token included in log records. Caps the
|
|
25
|
+
* volume a hostile client can push into logs with a single over-long
|
|
26
|
+
* garbage token (the wire itself allows 512 bytes per line).
|
|
27
|
+
*/
|
|
28
|
+
export const MAX_LOG_TOKEN_CHARS = 32;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Command verbs whose parameters are credential material. `IDENTIFY`
|
|
32
|
+
* covers the service-shortcut form some clients send as a top-level
|
|
33
|
+
* command; the PRIVMSG/NOTICE-to-NickServ form is handled separately
|
|
34
|
+
* below.
|
|
35
|
+
*/
|
|
36
|
+
const SENSITIVE_COMMANDS: ReadonlySet<string> = new Set([
|
|
37
|
+
'PASS',
|
|
38
|
+
'AUTHENTICATE',
|
|
39
|
+
'OPER',
|
|
40
|
+
'IDENTIFY',
|
|
41
|
+
]);
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* rfc1459-lowercased service nick targets for PRIVMSG/NOTICE. Only
|
|
45
|
+
* messages aimed at these nicks are inspected for credential
|
|
46
|
+
* subcommands, so ordinary chat is never touched.
|
|
47
|
+
*/
|
|
48
|
+
const SERVICE_NICKS: ReadonlySet<string> = new Set(['nickserv', 'ns']);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* NickServ subcommands whose arguments are credentials (`IDENTIFY
|
|
52
|
+
* <password>`, the `ID <password>` alias). Anything else aimed at a
|
|
53
|
+
* service nick (INFO, HELP, …) is not secret and passes through.
|
|
54
|
+
*/
|
|
55
|
+
const SENSITIVE_SUBCOMMANDS: ReadonlySet<string> = new Set(['identify', 'id']);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Redacts the credential-bearing parameters of a raw inbound IRC line
|
|
59
|
+
* while keeping the command verb (and, for service messages, the target
|
|
60
|
+
* and subcommand) for debuggability. Non-sensitive lines are returned
|
|
61
|
+
* unchanged; the input is never lengthened beyond a few placeholder
|
|
62
|
+
* characters.
|
|
63
|
+
*
|
|
64
|
+
* Sensitive shapes:
|
|
65
|
+
* `PASS <pw>` → `PASS [REDACTED]`
|
|
66
|
+
* `AUTHENTICATE <sasl-payload>` → `AUTHENTICATE [REDACTED]`
|
|
67
|
+
* `OPER <name> <pw>` → `OPER [REDACTED]`
|
|
68
|
+
* `IDENTIFY <pw>` → `IDENTIFY [REDACTED]`
|
|
69
|
+
* `PRIVMSG NickServ :IDENTIFY <pw>`→ `PRIVMSG NickServ :IDENTIFY [REDACTED]`
|
|
70
|
+
* (same for NOTICE, the `ID` alias, `ns`, and missing trailing colons)
|
|
71
|
+
*
|
|
72
|
+
* An optional leading `:source` prefix is preserved verbatim.
|
|
73
|
+
*/
|
|
74
|
+
export function redactLine(line: string): string {
|
|
75
|
+
// Split off an optional leading source prefix; the next token is the
|
|
76
|
+
// command position.
|
|
77
|
+
let head = '';
|
|
78
|
+
let rest = line;
|
|
79
|
+
if (line.startsWith(':')) {
|
|
80
|
+
const prefixEnd = line.indexOf(' ');
|
|
81
|
+
if (prefixEnd === -1) return line;
|
|
82
|
+
head = line.slice(0, prefixEnd);
|
|
83
|
+
rest = line.slice(prefixEnd + 1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const commandEnd = rest.indexOf(' ');
|
|
87
|
+
const command = commandEnd === -1 ? rest : rest.slice(0, commandEnd);
|
|
88
|
+
const remainder = commandEnd === -1 ? '' : rest.slice(commandEnd + 1);
|
|
89
|
+
const verb = command.toUpperCase();
|
|
90
|
+
|
|
91
|
+
if (SENSITIVE_COMMANDS.has(verb)) {
|
|
92
|
+
if (remainder.length === 0) return line;
|
|
93
|
+
return withPrefix(head, `${command} ${REDACTED}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (verb === 'PRIVMSG' || verb === 'NOTICE') {
|
|
97
|
+
const targetEnd = remainder.indexOf(' ');
|
|
98
|
+
const target = targetEnd === -1 ? remainder : remainder.slice(0, targetEnd);
|
|
99
|
+
const body = targetEnd === -1 ? '' : remainder.slice(targetEnd + 1);
|
|
100
|
+
if (SERVICE_NICKS.has(target.toLowerCase())) {
|
|
101
|
+
// Tolerate both conforming (`:IDENTIFY pw`) and colon-less bodies.
|
|
102
|
+
const bare = body.startsWith(':') ? body.slice(1) : body;
|
|
103
|
+
const hadColon = bare.length !== body.length;
|
|
104
|
+
const subEnd = bare.indexOf(' ');
|
|
105
|
+
const sub = subEnd === -1 ? bare : bare.slice(0, subEnd);
|
|
106
|
+
const hasArgs = subEnd !== -1;
|
|
107
|
+
if (hasArgs && SENSITIVE_SUBCOMMANDS.has(sub.toLowerCase())) {
|
|
108
|
+
return withPrefix(head, `${command} ${target} ${hadColon ? ':' : ''}${sub} ${REDACTED}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return line;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Caps a leading token at {@link MAX_LOG_TOKEN_CHARS} characters,
|
|
118
|
+
* suffixing an ellipsis marker when truncation occurred. The marker makes
|
|
119
|
+
* truncated tokens visually distinct from tokens that were exactly at the
|
|
120
|
+
* cap.
|
|
121
|
+
*/
|
|
122
|
+
export function truncateToken(token: string): string {
|
|
123
|
+
return token.length <= MAX_LOG_TOKEN_CHARS ? token : `${token.slice(0, MAX_LOG_TOKEN_CHARS)}...`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Parser diagnostics (fixed strings or fixed prefixes) → stable reason
|
|
128
|
+
* codes. Codes are log query keys; they must stay stable even if the
|
|
129
|
+
* human-readable message is reworded.
|
|
130
|
+
*/
|
|
131
|
+
const PARSE_ERROR_REASONS: ReadonlyArray<readonly [prefix: string, reason: string]> = [
|
|
132
|
+
['empty message', 'empty-message'],
|
|
133
|
+
['message exceeds', 'input-cap-exceeded'],
|
|
134
|
+
['tag section exceeds', 'tag-section-too-large'],
|
|
135
|
+
['tag section with no message body', 'tags-without-body'],
|
|
136
|
+
['too many tags', 'too-many-tags'],
|
|
137
|
+
['source prefix with no command', 'prefix-without-command'],
|
|
138
|
+
['missing command', 'missing-command'],
|
|
139
|
+
['invalid command token', 'invalid-command-token'],
|
|
140
|
+
];
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Maps a thrown parse error to a stable reason code for log records.
|
|
144
|
+
* `parse` only throws `IrcParseError` (every throw inside the parser
|
|
145
|
+
* constructs one), but the guard accepts any thrown value so callers can
|
|
146
|
+
* pass an `unknown` catch binding directly.
|
|
147
|
+
*/
|
|
148
|
+
export function parseErrorReason(err: unknown): string {
|
|
149
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
150
|
+
for (const [prefix, reason] of PARSE_ERROR_REASONS) {
|
|
151
|
+
if (message.startsWith(prefix)) return reason;
|
|
152
|
+
}
|
|
153
|
+
return 'parse-failed';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Re-attaches the (possibly absent) source prefix to a redacted tail. */
|
|
157
|
+
function withPrefix(head: string, tail: string): string {
|
|
158
|
+
return head.length === 0 ? tail : `${head} ${tail}`;
|
|
159
|
+
}
|
|
@@ -54,6 +54,20 @@ export interface IrcRuntime {
|
|
|
54
54
|
* oper's WALLOPS rate.
|
|
55
55
|
*/
|
|
56
56
|
broadcastWallops(lines: RawLine[], except?: ConnId): Promise<void>;
|
|
57
|
+
/**
|
|
58
|
+
* Oper-only notice fanout. Delivers `lines` to every live connection
|
|
59
|
+
* whose `+o` (oper) user mode is set, skipping `except`. Emitted by the
|
|
60
|
+
* OPER reducer's per-IP brute-force lockout gate
|
|
61
|
+
* (`:<server> NOTICE * :OPER lockout triggered for <host>`) so the oper
|
|
62
|
+
* corps learns a guessing attack is being throttled. Enumeration
|
|
63
|
+
* strategy mirrors {@link broadcastWallops} per adapter:
|
|
64
|
+
* - in-memory: scans its connection map, filters `+o`.
|
|
65
|
+
* - Cloudflare: walks the sharded registry, fetches each state to
|
|
66
|
+
* check `+o`.
|
|
67
|
+
* - AWS: `Scan`s the `Connections` table filtered to `+o` rows and
|
|
68
|
+
* `PostToConnection`s each.
|
|
69
|
+
*/
|
|
70
|
+
broadcastOperNotice(lines: RawLine[], except?: ConnId): Promise<void>;
|
|
57
71
|
|
|
58
72
|
// nickname registry
|
|
59
73
|
reserveNick(nick: Nick, conn: ConnId): Promise<{ ok: true } | { ok: false }>;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* parser/reducer/dispatch pipeline.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import { splitFrameLines } from '@serverless-ircd/irc-core';
|
|
18
|
+
import { MAX_BUFFER_BYTES, byteLength, splitFrameLines } from '@serverless-ircd/irc-core';
|
|
19
19
|
|
|
20
20
|
export { splitFrameLines };
|
|
21
21
|
|
|
@@ -28,6 +28,22 @@ export interface Transport {
|
|
|
28
28
|
feed(chunk: string): string[];
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/** Options accepted by {@link TcpByteStreamTransport}. */
|
|
32
|
+
export interface TcpByteStreamTransportOptions {
|
|
33
|
+
/**
|
|
34
|
+
* Invoked when an appended chunk grows the retained partial-line buffer
|
|
35
|
+
* past {@link MAX_BUFFER_BYTES} (measured in UTF-8 bytes, once per
|
|
36
|
+
* append). The transport drops the oversized tail and returns no lines;
|
|
37
|
+
* the owner should close the connection — a peer that streams more than
|
|
38
|
+
* 8 KiB without a line terminator can never produce a legal IRC line
|
|
39
|
+
* (the parser caps lines at 512 bytes), so the buffer is pure
|
|
40
|
+
* attacker-controlled memory. Writing `ERROR :Closing link: ...` and
|
|
41
|
+
* tearing down the socket is the caller's job: this seam has no write
|
|
42
|
+
* path.
|
|
43
|
+
*/
|
|
44
|
+
onOverflow?: () => void;
|
|
45
|
+
}
|
|
46
|
+
|
|
31
47
|
/**
|
|
32
48
|
* Splits a WebSocket text frame into IRC lines. Re-exported from
|
|
33
49
|
* `@serverless-ircd/irc-core` (`ws-framing.ts`), which owns the canonical
|
|
@@ -65,6 +81,8 @@ export class WsTextFrameTransport implements Transport {
|
|
|
65
81
|
export class TcpByteStreamTransport implements Transport {
|
|
66
82
|
private buffer = '';
|
|
67
83
|
|
|
84
|
+
constructor(private readonly options?: TcpByteStreamTransportOptions) {}
|
|
85
|
+
|
|
68
86
|
feed(chunk: string): string[] {
|
|
69
87
|
if (chunk.length === 0) return [];
|
|
70
88
|
this.buffer += chunk;
|
|
@@ -80,6 +98,15 @@ export class TcpByteStreamTransport implements Transport {
|
|
|
80
98
|
}
|
|
81
99
|
}
|
|
82
100
|
this.buffer = this.buffer.slice(start);
|
|
101
|
+
// Cap the retained (unterminated) tail — checked once per append, in
|
|
102
|
+
// UTF-8 bytes. Complete lines flushed by this chunk never count against
|
|
103
|
+
// the budget, so legitimate multi-line batches far larger than the cap
|
|
104
|
+
// pass through; only an unterminated tail can grow unboundedly.
|
|
105
|
+
if (byteLength(this.buffer) > MAX_BUFFER_BYTES) {
|
|
106
|
+
this.buffer = '';
|
|
107
|
+
this.options?.onOverflow?.();
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
83
110
|
return lines;
|
|
84
111
|
}
|
|
85
112
|
|