serverless-ircd 0.2.0 → 0.4.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 +2 -2
- package/.github/workflows/deploy-aws.yml +1 -3
- package/.github/workflows/deploy-cf-tcp.yml +87 -0
- package/.github/workflows/deploy-cf.yml +1 -1
- package/.node-version +1 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +258 -18
- package/README.md +72 -30
- package/apps/aws-stack/README.md +2 -2
- package/apps/aws-stack/bin/aws.ts +7 -0
- package/apps/aws-stack/package.json +4 -4
- package/apps/aws-stack/src/aws-stack.ts +118 -6
- package/apps/aws-stack/tests/stack.test.ts +98 -3
- package/apps/cf-tcp-container/Dockerfile +69 -0
- package/apps/cf-tcp-container/package.json +34 -0
- package/apps/cf-tcp-container/src/config-loader.ts +145 -0
- package/apps/cf-tcp-container/src/container-do.ts +38 -0
- package/apps/cf-tcp-container/src/container-server.ts +363 -0
- package/apps/cf-tcp-container/src/main.ts +77 -0
- package/apps/cf-tcp-container/src/persistence.ts +144 -0
- package/apps/cf-tcp-container/src/worker.ts +41 -0
- package/apps/cf-tcp-container/terraform/provider.tf +24 -0
- package/apps/cf-tcp-container/terraform/spectrum.tf +81 -0
- package/apps/cf-tcp-container/tests/config-loader.test.ts +217 -0
- package/apps/cf-tcp-container/tests/container-server.test.ts +465 -0
- package/apps/cf-tcp-container/tests/persistence.test.ts +227 -0
- package/apps/cf-tcp-container/tests/tls-e2e.test.ts +275 -0
- package/apps/cf-tcp-container/tsconfig.build.json +17 -0
- package/apps/cf-tcp-container/tsconfig.test.json +15 -0
- package/apps/cf-tcp-container/vitest.config.ts +26 -0
- package/apps/cf-tcp-container/wrangler.toml +63 -0
- package/apps/cf-worker/package.json +1 -1
- package/apps/cf-worker/scripts/smoke.mjs +0 -0
- package/apps/cf-worker/src/worker.ts +8 -2
- package/apps/cf-worker/tests/smoke.test.ts +1 -0
- package/apps/cf-worker/wrangler.test.toml +11 -1
- package/apps/cf-worker/wrangler.toml +69 -19
- package/apps/local-cli/package.json +1 -1
- package/apps/local-cli/src/config-loader.ts +11 -0
- package/apps/local-cli/src/main.ts +1 -1
- package/apps/local-cli/src/server.ts +46 -3
- package/apps/local-cli/tests/e2e.test.ts +52 -0
- package/package.json +19 -16
- package/packages/aws-adapter/package.json +3 -3
- package/packages/aws-adapter/src/account-store.ts +121 -0
- package/packages/aws-adapter/src/admission.ts +74 -0
- package/packages/aws-adapter/src/aws-runtime.ts +124 -34
- package/packages/aws-adapter/src/cdk-table-defs.ts +5 -1
- package/packages/aws-adapter/src/config-loader.ts +62 -0
- package/packages/aws-adapter/src/dynamo-account-store.ts +95 -0
- package/packages/aws-adapter/src/handlers/connect.ts +64 -8
- package/packages/aws-adapter/src/handlers/default.ts +43 -2
- package/packages/aws-adapter/src/handlers/disconnect.ts +27 -8
- package/packages/aws-adapter/src/handlers/index.ts +120 -9
- package/packages/aws-adapter/src/handlers/nlb-stream.ts +481 -0
- package/packages/aws-adapter/src/handlers/ping-checker.ts +1 -1
- package/packages/aws-adapter/src/index.ts +13 -0
- package/packages/aws-adapter/tests/account-store-dynamo.test.ts +182 -0
- package/packages/aws-adapter/tests/account-store.test.ts +279 -0
- package/packages/aws-adapter/tests/admission.test.ts +70 -0
- package/packages/aws-adapter/tests/aws-harness.ts +13 -1
- package/packages/aws-adapter/tests/config-loader.test.ts +75 -0
- package/packages/aws-adapter/tests/connect.test.ts +78 -0
- package/packages/aws-adapter/tests/disconnect-fanout.test.ts +343 -0
- package/packages/aws-adapter/tests/gone-exception.test.ts +31 -26
- package/packages/aws-adapter/tests/handlers.test.ts +194 -47
- package/packages/aws-adapter/tests/nlb-stream.test.ts +478 -0
- package/packages/aws-adapter/tests/ping-checker.test.ts +34 -29
- package/packages/aws-adapter/tests/sweeper.test.ts +25 -18
- package/packages/aws-adapter/tests/transactions.test.ts +25 -20
- package/packages/cf-adapter/package.json +1 -1
- package/packages/cf-adapter/src/cf-runtime.ts +40 -22
- package/packages/cf-adapter/src/channel-do.ts +40 -1
- package/packages/cf-adapter/src/channel-registry-do.ts +58 -0
- package/packages/cf-adapter/src/config-loader.ts +62 -0
- package/packages/cf-adapter/src/connection-do.ts +181 -31
- package/packages/cf-adapter/src/d1-account-store.ts +198 -0
- package/packages/cf-adapter/src/env.ts +58 -8
- package/packages/cf-adapter/src/index.ts +11 -9
- package/packages/cf-adapter/tests/cf-harness.ts +11 -1
- package/packages/cf-adapter/tests/cf-integration.test.ts +2 -1
- package/packages/cf-adapter/tests/cf-runtime.test.ts +91 -0
- package/packages/cf-adapter/tests/config-loader.test.ts +22 -0
- package/packages/cf-adapter/tests/connection-do-channel-registration.test.ts +37 -0
- package/packages/cf-adapter/tests/connection-do-no-batching-reservation.test.ts +52 -0
- package/packages/cf-adapter/tests/connection-do-sasl-d1.test.ts +166 -0
- package/packages/cf-adapter/tests/connection-do.test.ts +40 -0
- package/packages/cf-adapter/tests/d1-account-store.test.ts +226 -0
- package/packages/cf-adapter/tests/raw-modules.d.ts +11 -0
- package/packages/cf-adapter/tests/worker/main.ts +9 -1
- package/packages/cf-adapter/tests/worker/stubs/channel-stub.ts +7 -1
- 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 +14 -28
- package/packages/in-memory-runtime/tests/in-memory-runtime.test.ts +19 -0
- package/packages/irc-core/package.json +8 -2
- package/packages/irc-core/scripts/generate-build-info.mjs +31 -0
- package/packages/irc-core/src/caps/capabilities.ts +23 -2
- package/packages/irc-core/src/case-fold.ts +64 -0
- package/packages/irc-core/src/cloak.ts +1 -1
- package/packages/irc-core/src/commands/cap.ts +8 -1
- package/packages/irc-core/src/commands/index.ts +11 -0
- package/packages/irc-core/src/commands/invite.ts +6 -9
- package/packages/irc-core/src/commands/ison.ts +61 -0
- package/packages/irc-core/src/commands/isupport.ts +1 -1
- package/packages/irc-core/src/commands/join.ts +4 -3
- package/packages/irc-core/src/commands/kick.ts +4 -11
- package/packages/irc-core/src/commands/list.ts +5 -4
- package/packages/irc-core/src/commands/mode.ts +5 -9
- package/packages/irc-core/src/commands/motd-lines.ts +127 -0
- package/packages/irc-core/src/commands/motd.ts +6 -110
- package/packages/irc-core/src/commands/oper.ts +151 -0
- package/packages/irc-core/src/commands/quit.ts +12 -0
- package/packages/irc-core/src/commands/registration.ts +26 -19
- package/packages/irc-core/src/commands/sasl.ts +72 -9
- package/packages/irc-core/src/commands/server-info.ts +129 -0
- package/packages/irc-core/src/commands/tagmsg.ts +205 -0
- package/packages/irc-core/src/commands/userhost.ts +84 -0
- package/packages/irc-core/src/commands/whowas.ts +113 -0
- package/packages/irc-core/src/config.ts +84 -3
- package/packages/irc-core/src/credential-hashing.ts +124 -0
- package/packages/irc-core/src/effects.ts +6 -29
- package/packages/irc-core/src/index.ts +3 -0
- package/packages/irc-core/src/ports.ts +240 -7
- package/packages/irc-core/src/protocol/numerics.ts +14 -8
- package/packages/irc-core/src/types.ts +60 -1
- package/packages/irc-core/tests/account-store.test.ts +131 -0
- package/packages/irc-core/tests/caps/capabilities.test.ts +4 -3
- package/packages/irc-core/tests/case-fold.test.ts +80 -0
- package/packages/irc-core/tests/commands/cap.test.ts +33 -1
- package/packages/irc-core/tests/commands/ison.test.ts +166 -0
- package/packages/irc-core/tests/commands/kick.test.ts +15 -0
- package/packages/irc-core/tests/commands/oper.test.ts +257 -0
- package/packages/irc-core/tests/commands/quit.test.ts +69 -2
- package/packages/irc-core/tests/commands/registration.test.ts +256 -19
- package/packages/irc-core/tests/commands/sasl.test.ts +118 -10
- package/packages/irc-core/tests/commands/server-info.test.ts +274 -0
- package/packages/irc-core/tests/commands/tagmsg.test.ts +662 -0
- package/packages/irc-core/tests/commands/userhost.test.ts +264 -0
- package/packages/irc-core/tests/commands/who.test.ts +22 -4
- package/packages/irc-core/tests/commands/whois.test.ts +8 -1
- package/packages/irc-core/tests/commands/whowas.test.ts +312 -0
- package/packages/irc-core/tests/config.test.ts +170 -1
- package/packages/irc-core/tests/credential-hashing.test.ts +170 -0
- package/packages/irc-core/tests/effects.test.ts +0 -27
- package/packages/irc-core/tests/nick-history-store.test.ts +162 -0
- package/packages/irc-core/tests/numerics.test.ts +12 -0
- package/packages/irc-core/tests/types.test.ts +35 -1
- package/packages/irc-core/tsconfig.build.json +1 -1
- package/packages/irc-core/tsconfig.test.json +1 -1
- package/packages/irc-server/package.json +1 -1
- package/packages/irc-server/src/actor.ts +182 -14
- package/packages/irc-server/src/dispatch.ts +0 -3
- package/packages/irc-server/src/index.ts +10 -2
- package/packages/irc-server/src/routing.ts +21 -0
- package/packages/irc-server/src/transport.ts +101 -0
- package/packages/irc-server/tests/actor.test.ts +617 -1
- package/packages/irc-server/tests/dispatch.test.ts +0 -17
- package/packages/irc-server/tests/routing.test.ts +7 -0
- package/packages/irc-server/tests/transport.test.ts +230 -0
- package/packages/irc-test-support/package.json +1 -1
- package/packages/irc-test-support/src/harness.ts +44 -9
- package/packages/irc-test-support/src/in-memory-harness.ts +78 -13
- package/packages/irc-test-support/src/index.ts +3 -0
- package/packages/irc-test-support/src/scenarios.ts +132 -2
- package/packages/irc-test-support/tests/in-memory-harness.test.ts +2 -1
- package/packages/irc-test-support/tests/in-memory-scenarios.test.ts +23 -9
- package/pnpm-workspace.yaml +9 -0
- package/tools/ci-hardening/package.json +1 -1
- package/tools/package.json +4 -0
- package/tools/seed-aws-accounts.ts +79 -0
- package/tools/seed-cf-accounts.ts +104 -0
- package/tools/tcp-ws-forwarder/package.json +1 -1
- package/AGENTS.md +0 -5
- package/MOTD.txt +0 -3
- package/PLAN-FIXES.md +0 -358
- package/PLAN.md +0 -420
- package/dashboards/cloudwatch-irc.json +0 -139
- package/docs/AWS-Adapter-Architecture.md +0 -494
- package/docs/AWS-Deployment.md +0 -1107
- package/docs/Cloudflare-Deployment-Guide.md +0 -660
- package/docs/Home.md +0 -1
- package/docs/Observability.md +0 -87
- package/docs/PlanIRCv3Websocket.md +0 -489
- package/docs/PlanWebClient.md +0 -451
- package/docs/Release-Process.md +0 -440
- package/progress.md +0 -107
- package/tickets.md +0 -2485
- package/webircgateway/LICENSE +0 -201
- package/webircgateway/Makefile +0 -44
- package/webircgateway/README.md +0 -134
- package/webircgateway/config.conf.example +0 -135
- package/webircgateway/go.mod +0 -16
- package/webircgateway/go.sum +0 -89
- package/webircgateway/main.go +0 -118
- package/webircgateway/pkg/dnsbl/dnsbl.go +0 -121
- package/webircgateway/pkg/identd/identd.go +0 -86
- package/webircgateway/pkg/identd/rpcclient.go +0 -59
- package/webircgateway/pkg/irc/isupport.go +0 -56
- package/webircgateway/pkg/irc/message.go +0 -217
- package/webircgateway/pkg/irc/state.go +0 -79
- package/webircgateway/pkg/proxy/proxy.go +0 -129
- package/webircgateway/pkg/proxy/server.go +0 -237
- package/webircgateway/pkg/recaptcha/recaptcha.go +0 -59
- package/webircgateway/pkg/webircgateway/client.go +0 -741
- package/webircgateway/pkg/webircgateway/client_command_handlers.go +0 -495
- package/webircgateway/pkg/webircgateway/config.go +0 -385
- package/webircgateway/pkg/webircgateway/gateway.go +0 -278
- package/webircgateway/pkg/webircgateway/gateway_utils.go +0 -133
- package/webircgateway/pkg/webircgateway/hooks.go +0 -152
- package/webircgateway/pkg/webircgateway/letsencrypt.go +0 -41
- package/webircgateway/pkg/webircgateway/messagetags.go +0 -103
- package/webircgateway/pkg/webircgateway/transport_kiwiirc.go +0 -206
- package/webircgateway/pkg/webircgateway/transport_sockjs.go +0 -107
- package/webircgateway/pkg/webircgateway/transport_tcp.go +0 -113
- package/webircgateway/pkg/webircgateway/transport_websocket.go +0 -126
- package/webircgateway/pkg/webircgateway/utils.go +0 -147
- package/webircgateway/plugins/example/plugin.go +0 -11
- package/webircgateway/plugins/stats/plugin.go +0 -52
- package/webircgateway/staticcheck.conf +0 -1
- package/webircgateway/webircgateway.svg +0 -3
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { EventEmitter } from 'node:events';
|
|
3
|
+
import { mkdtemp, readFile, rm } from 'node:fs/promises';
|
|
4
|
+
import { type Socket, createConnection } from 'node:net';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import {
|
|
8
|
+
type Server,
|
|
9
|
+
type TLSSocket,
|
|
10
|
+
connect as tlsConnect,
|
|
11
|
+
createServer as tlsCreateServer,
|
|
12
|
+
} from 'node:tls';
|
|
13
|
+
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
14
|
+
import { type ContainerServer, startContainerServer } from '../src/container-server.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generates a self-signed TLS certificate for the test proxy using the
|
|
18
|
+
* system `openssl` binary. This mirrors what Cloudflare Spectrum does in
|
|
19
|
+
* production: terminate TLS at the edge with a certificate, then forward
|
|
20
|
+
* plaintext TCP to the origin.
|
|
21
|
+
*
|
|
22
|
+
* If `openssl` is not available, the calling test is skipped — the TLS
|
|
23
|
+
* e2e requires it to synthesise a trust anchor.
|
|
24
|
+
*/
|
|
25
|
+
async function generateSelfSignedCert(dir: string): Promise<{ key: string; cert: string }> {
|
|
26
|
+
const keyPath = join(dir, 'key.pem');
|
|
27
|
+
const certPath = join(dir, 'cert.pem');
|
|
28
|
+
try {
|
|
29
|
+
execFileSync(
|
|
30
|
+
'openssl',
|
|
31
|
+
[
|
|
32
|
+
'req',
|
|
33
|
+
'-x509',
|
|
34
|
+
'-newkey',
|
|
35
|
+
'rsa:2048',
|
|
36
|
+
'-keyout',
|
|
37
|
+
keyPath,
|
|
38
|
+
'-out',
|
|
39
|
+
certPath,
|
|
40
|
+
'-days',
|
|
41
|
+
'1',
|
|
42
|
+
'-nodes',
|
|
43
|
+
'-subj',
|
|
44
|
+
'/CN=localhost',
|
|
45
|
+
],
|
|
46
|
+
{ stdio: 'pipe' },
|
|
47
|
+
);
|
|
48
|
+
} catch {
|
|
49
|
+
throw new Error('openssl not available — required to generate test TLS cert');
|
|
50
|
+
}
|
|
51
|
+
const key = await readFile(keyPath, 'utf8');
|
|
52
|
+
const cert = await readFile(certPath, 'utf8');
|
|
53
|
+
return { key, cert };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* IRC test client that speaks TLS (mirrors the Spectrum client path).
|
|
58
|
+
* Connects via `tls.connect`, sends lines with CRLF, parses inbound bytes.
|
|
59
|
+
*/
|
|
60
|
+
class TlsIrcClient extends EventEmitter {
|
|
61
|
+
readonly socket: TLSSocket;
|
|
62
|
+
readonly received: string[] = [];
|
|
63
|
+
|
|
64
|
+
constructor(host: string, port: number, opts: { key: string; cert: string }) {
|
|
65
|
+
super();
|
|
66
|
+
this.socket = tlsConnect({
|
|
67
|
+
host,
|
|
68
|
+
port,
|
|
69
|
+
rejectUnauthorized: false,
|
|
70
|
+
ca: opts.cert,
|
|
71
|
+
});
|
|
72
|
+
this.socket.setEncoding('utf8');
|
|
73
|
+
this.socket.on('data', (text: string) => {
|
|
74
|
+
for (const line of text.split(/\r?\n/u)) {
|
|
75
|
+
if (line.length > 0) {
|
|
76
|
+
this.received.push(line);
|
|
77
|
+
this.emit('line', line);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
secured(): Promise<void> {
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
if (this.socket.authorized !== undefined) resolve();
|
|
86
|
+
else {
|
|
87
|
+
this.socket.once('secure', () => resolve());
|
|
88
|
+
this.socket.once('error', reject);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
send(line: string): Promise<void> {
|
|
94
|
+
return new Promise((resolve, reject) => {
|
|
95
|
+
this.socket.write(`${line}\r\n`, (err) => (err ? reject(err) : resolve()));
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
waitFor(predicate: (line: string) => boolean, timeoutMs = 3000): Promise<string> {
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const timeout = setTimeout(() => {
|
|
102
|
+
reject(new Error(`timeout; received: ${JSON.stringify(this.received)}`));
|
|
103
|
+
}, timeoutMs);
|
|
104
|
+
const check = (line: string): void => {
|
|
105
|
+
if (predicate(line)) {
|
|
106
|
+
clearTimeout(timeout);
|
|
107
|
+
this.off('line', check);
|
|
108
|
+
resolve(line);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
for (const line of this.received) {
|
|
112
|
+
if (predicate(line)) {
|
|
113
|
+
clearTimeout(timeout);
|
|
114
|
+
resolve(line);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
this.on('line', check);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
close(): Promise<void> {
|
|
123
|
+
return new Promise((resolve) => {
|
|
124
|
+
this.socket.once('close', () => resolve());
|
|
125
|
+
this.socket.end();
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Starts an in-process TLS-terminating proxy (the "stunnel" replacement).
|
|
132
|
+
* Each TLS connection from a client gets a plaintext TCP connection piped
|
|
133
|
+
* to the container origin. This is the local analogue of Cloudflare
|
|
134
|
+
* Spectrum: TLS at the edge, plaintext to the origin.
|
|
135
|
+
*/
|
|
136
|
+
function startTlsProxy(opts: {
|
|
137
|
+
key: string;
|
|
138
|
+
cert: string;
|
|
139
|
+
originHost: string;
|
|
140
|
+
originPort: number;
|
|
141
|
+
}): Promise<{ server: Server; port: number; close: () => Promise<void> }> {
|
|
142
|
+
return new Promise((resolve, reject) => {
|
|
143
|
+
const originSockets = new Set<Socket>();
|
|
144
|
+
const server = tlsCreateServer({ key: opts.key, cert: opts.cert }, (tlsSocket) => {
|
|
145
|
+
const origin = createConnection({ host: opts.originHost, port: opts.originPort });
|
|
146
|
+
originSockets.add(origin);
|
|
147
|
+
tlsSocket.pipe(origin);
|
|
148
|
+
origin.pipe(tlsSocket);
|
|
149
|
+
const cleanup = (): void => {
|
|
150
|
+
origin.destroy();
|
|
151
|
+
tlsSocket.destroy();
|
|
152
|
+
};
|
|
153
|
+
tlsSocket.on('close', cleanup);
|
|
154
|
+
origin.on('close', () => {
|
|
155
|
+
originSockets.delete(origin);
|
|
156
|
+
tlsSocket.destroy();
|
|
157
|
+
});
|
|
158
|
+
tlsSocket.on('error', cleanup);
|
|
159
|
+
origin.on('error', cleanup);
|
|
160
|
+
});
|
|
161
|
+
server.once('error', reject);
|
|
162
|
+
server.listen(0, '127.0.0.1', () => {
|
|
163
|
+
server.removeListener('error', reject);
|
|
164
|
+
const addr = server.address();
|
|
165
|
+
// c8 ignore next 1
|
|
166
|
+
const port = typeof addr === 'object' && addr !== null ? addr.port : 0;
|
|
167
|
+
resolve({
|
|
168
|
+
server,
|
|
169
|
+
port,
|
|
170
|
+
close: () =>
|
|
171
|
+
new Promise<void>((res) => {
|
|
172
|
+
for (const s of originSockets) s.destroy();
|
|
173
|
+
originSockets.clear();
|
|
174
|
+
server.close(() => res());
|
|
175
|
+
}),
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
describe('container-server — TLS e2e via in-process proxy (Spectrum analogue)', () => {
|
|
182
|
+
let origin: ContainerServer;
|
|
183
|
+
let proxy: { server: Server; port: number; close: () => Promise<void> };
|
|
184
|
+
let dir: string;
|
|
185
|
+
let cert: { key: string; cert: string };
|
|
186
|
+
|
|
187
|
+
beforeAll(async () => {
|
|
188
|
+
dir = await mkdtemp(join(tmpdir(), 'sirc-tls-'));
|
|
189
|
+
try {
|
|
190
|
+
cert = await generateSelfSignedCert(dir);
|
|
191
|
+
} catch (err) {
|
|
192
|
+
// Skip the entire suite if openssl isn't available.
|
|
193
|
+
console.warn(String((err as Error).message));
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
origin = await startContainerServer({
|
|
197
|
+
port: 0,
|
|
198
|
+
host: '127.0.0.1',
|
|
199
|
+
serverName: 'irc.tls.test',
|
|
200
|
+
networkName: 'TlsTestNet',
|
|
201
|
+
});
|
|
202
|
+
proxy = await startTlsProxy({
|
|
203
|
+
key: cert.key,
|
|
204
|
+
cert: cert.cert,
|
|
205
|
+
originHost: '127.0.0.1',
|
|
206
|
+
originPort: origin.port,
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
afterAll(async () => {
|
|
211
|
+
if (proxy !== undefined) await proxy.close();
|
|
212
|
+
if (origin !== undefined) await origin.close();
|
|
213
|
+
await rm(dir, { recursive: true, force: true });
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('a TLS client completes register → join → privmsg → quit through the proxy', async () => {
|
|
217
|
+
if (cert === undefined) return; // openssl not available
|
|
218
|
+
|
|
219
|
+
const alice = new TlsIrcClient('127.0.0.1', proxy.port, cert);
|
|
220
|
+
await alice.secured();
|
|
221
|
+
const bob = new TlsIrcClient('127.0.0.1', proxy.port, cert);
|
|
222
|
+
await bob.secured();
|
|
223
|
+
|
|
224
|
+
await alice.send('NICK alice');
|
|
225
|
+
await alice.send('USER alice 0 * :Alice');
|
|
226
|
+
await alice.waitFor((l) => l.startsWith(':irc.tls.test 001 '));
|
|
227
|
+
|
|
228
|
+
await bob.send('NICK bob');
|
|
229
|
+
await bob.send('USER bob 0 * :Bob');
|
|
230
|
+
await bob.waitFor((l) => l.startsWith(':irc.tls.test 001 '));
|
|
231
|
+
|
|
232
|
+
await alice.send('JOIN #tls-e2e');
|
|
233
|
+
await alice.waitFor((l) => l.includes('JOIN #tls-e2e'));
|
|
234
|
+
await bob.send('JOIN #tls-e2e');
|
|
235
|
+
await bob.waitFor((l) => l.includes('JOIN #tls-e2e'));
|
|
236
|
+
|
|
237
|
+
await alice.send('PRIVMSG #tls-e2e :over tls');
|
|
238
|
+
await bob.waitFor((l) => l.includes('PRIVMSG #tls-e2e :over tls'));
|
|
239
|
+
|
|
240
|
+
await alice.close();
|
|
241
|
+
await bob.close();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('routes PING → PONG over TLS', async () => {
|
|
245
|
+
if (cert === undefined) return;
|
|
246
|
+
|
|
247
|
+
const client = new TlsIrcClient('127.0.0.1', proxy.port, cert);
|
|
248
|
+
await client.secured();
|
|
249
|
+
await client.send('NICK tlsPing');
|
|
250
|
+
await client.send('USER tlsPing 0 * :TlsPing');
|
|
251
|
+
await client.waitFor((l) => l.startsWith(':irc.tls.test 001 '));
|
|
252
|
+
|
|
253
|
+
await client.send('PING :tls-tok');
|
|
254
|
+
await client.waitFor((l) => l === 'PONG :tls-tok');
|
|
255
|
+
await client.close();
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('the origin only sees plaintext (no TLS handshake bytes reach the actor)', async () => {
|
|
259
|
+
if (cert === undefined) return;
|
|
260
|
+
|
|
261
|
+
// If the origin received TLS handshake bytes, the TcpByteStreamTransport
|
|
262
|
+
// would try to parse them as IRC lines and produce garbage 421 replies.
|
|
263
|
+
// A clean registration proves only plaintext IRC lines arrived.
|
|
264
|
+
const client = new TlsIrcClient('127.0.0.1', proxy.port, cert);
|
|
265
|
+
await client.secured();
|
|
266
|
+
await client.send('NICK plaintext');
|
|
267
|
+
await client.send('USER plaintext 0 * :Plaintext');
|
|
268
|
+
const welcome = await client.waitFor((l) => l.startsWith(':irc.tls.test 001 plaintext'));
|
|
269
|
+
expect(welcome).toBeDefined();
|
|
270
|
+
// No 421 (ERR_UNKNOWNCOMMAND) should appear — that would indicate
|
|
271
|
+
// non-IRC bytes (TLS handshake) leaking through to the parser.
|
|
272
|
+
expect(client.received.some((l) => l.includes(' 421 '))).toBe(false);
|
|
273
|
+
await client.close();
|
|
274
|
+
});
|
|
275
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"composite": true,
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"tsBuildInfoFile": "./.tsbuildinfo",
|
|
8
|
+
"types": ["node"]
|
|
9
|
+
},
|
|
10
|
+
"include": ["src/**/*.ts"],
|
|
11
|
+
"exclude": ["dist", "tests", "**/*.test.ts", "src/worker.ts", "src/container-do.ts"],
|
|
12
|
+
"references": [
|
|
13
|
+
{ "path": "../../packages/irc-core/tsconfig.build.json" },
|
|
14
|
+
{ "path": "../../packages/irc-server/tsconfig.build.json" },
|
|
15
|
+
{ "path": "../../packages/in-memory-runtime/tsconfig.build.json" }
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"noEmit": true,
|
|
5
|
+
"types": ["node"],
|
|
6
|
+
"composite": false
|
|
7
|
+
},
|
|
8
|
+
"include": ["src/**/*.ts", "tests/**/*.ts"],
|
|
9
|
+
"exclude": ["dist", "src/worker.ts", "src/container-do.ts"],
|
|
10
|
+
"references": [
|
|
11
|
+
{ "path": "../../packages/irc-core/tsconfig.build.json" },
|
|
12
|
+
{ "path": "../../packages/irc-server/tsconfig.build.json" },
|
|
13
|
+
{ "path": "../../packages/in-memory-runtime/tsconfig.build.json" }
|
|
14
|
+
]
|
|
15
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
environment: 'node',
|
|
6
|
+
testTimeout: 10_000,
|
|
7
|
+
include: ['tests/**/*.test.ts'],
|
|
8
|
+
coverage: {
|
|
9
|
+
provider: 'v8',
|
|
10
|
+
all: false,
|
|
11
|
+
include: ['src/**/*.ts'],
|
|
12
|
+
// `main.ts` is CLI glue (argv parsing, signal handlers); exercised
|
|
13
|
+
// manually by `pnpm start`. `worker.ts` + `container-do.ts` are
|
|
14
|
+
// wrangler-only deploy artifacts (depend on @cloudflare/containers,
|
|
15
|
+
// compiled by wrangler at deploy time). The rest targets ≥90%.
|
|
16
|
+
exclude: ['src/main.ts', 'src/worker.ts', 'src/container-do.ts'],
|
|
17
|
+
reporter: ['text', 'html', 'json-summary'],
|
|
18
|
+
thresholds: {
|
|
19
|
+
lines: 90,
|
|
20
|
+
functions: 90,
|
|
21
|
+
branches: 80,
|
|
22
|
+
statements: 90,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#:vim:set ft=toml:
|
|
2
|
+
##
|
|
3
|
+
## Cloudflare Containers config for the ServerlessIRCd TCP origin.
|
|
4
|
+
##
|
|
5
|
+
## Deploys the IRC TCP container (apps/cf-tcp-container) to the Cloudflare
|
|
6
|
+
## Containers platform. The container listens on port 6667 (plaintext TCP);
|
|
7
|
+
## Cloudflare Spectrum (configured separately in terraform/spectrum.tf)
|
|
8
|
+
## terminates TLS on :6697 and forwards plaintext to this container.
|
|
9
|
+
##
|
|
10
|
+
## Prerequisites:
|
|
11
|
+
## - Cloudflare account with Containers beta access.
|
|
12
|
+
## - wrangler v4.112+ (`pnpm add -g wrangler`).
|
|
13
|
+
## - The container image is built from apps/cf-tcp-container/Dockerfile.
|
|
14
|
+
##
|
|
15
|
+
## Deploy:
|
|
16
|
+
## wrangler deploy --env staging
|
|
17
|
+
##
|
|
18
|
+
|
|
19
|
+
name = "sirc-tcp-origin"
|
|
20
|
+
main = "src/worker.ts"
|
|
21
|
+
compatibility_date = "2024-11-01"
|
|
22
|
+
compatibility_flags = ["nodejs_compat"]
|
|
23
|
+
|
|
24
|
+
[observability]
|
|
25
|
+
enabled = true
|
|
26
|
+
|
|
27
|
+
# --- Container definition ---
|
|
28
|
+
# The IRC TCP origin container. Listens on port 6667; the Worker below is a
|
|
29
|
+
# thin management/health-check wrapper (Spectrum routes TCP directly to the
|
|
30
|
+
# container origin, not through the Worker).
|
|
31
|
+
[[containers]]
|
|
32
|
+
class_name = "IrcTcpOrigin"
|
|
33
|
+
image = "./Dockerfile"
|
|
34
|
+
instance_type = "standard-1"
|
|
35
|
+
max_instances = 3
|
|
36
|
+
sleep_after = "2h"
|
|
37
|
+
|
|
38
|
+
# --- Durable Object binding for the container ---
|
|
39
|
+
[[durable_objects.bindings]]
|
|
40
|
+
name = "IRC_TCP_ORIGIN"
|
|
41
|
+
class_name = "IrcTcpOrigin"
|
|
42
|
+
|
|
43
|
+
# --- Migration ---
|
|
44
|
+
[[migrations]]
|
|
45
|
+
tag = "v1"
|
|
46
|
+
new_sqlite_classes = ["IrcTcpOrigin"]
|
|
47
|
+
|
|
48
|
+
# --- Environment ---
|
|
49
|
+
[vars]
|
|
50
|
+
SERVER_NAME = "irc.example.com"
|
|
51
|
+
NETWORK_NAME = "ExampleNet"
|
|
52
|
+
MOTD_LINES = "Welcome to the ServerlessIRCd TCP origin."
|
|
53
|
+
TCP_PORT = "6667"
|
|
54
|
+
PERSISTENCE_PATH = "/data/state.json"
|
|
55
|
+
|
|
56
|
+
# --- Staging ---
|
|
57
|
+
[env.staging]
|
|
58
|
+
name = "sirc-tcp-origin-staging"
|
|
59
|
+
|
|
60
|
+
[env.staging.vars]
|
|
61
|
+
SERVER_NAME = "irc-staging.example.com"
|
|
62
|
+
NETWORK_NAME = "StagingNet"
|
|
63
|
+
MOTD_LINES = "Welcome to the staging ServerlessIRCd TCP origin."
|
|
File without changes
|
|
@@ -22,11 +22,17 @@
|
|
|
22
22
|
* See `wrangler.toml` and `docs/deployment-cf.md`.
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
|
-
import {
|
|
25
|
+
import {
|
|
26
|
+
ChannelDO,
|
|
27
|
+
ChannelRegistryDO,
|
|
28
|
+
ConnectionDO,
|
|
29
|
+
type Env,
|
|
30
|
+
RegistryDO,
|
|
31
|
+
} from '@serverless-ircd/cf-adapter';
|
|
26
32
|
|
|
27
33
|
// Re-exported for wrangler DO binding resolution. The class names below
|
|
28
34
|
// MUST match `class_name` in `wrangler.toml`.
|
|
29
|
-
export { ChannelDO, ConnectionDO, RegistryDO };
|
|
35
|
+
export { ChannelDO, ChannelRegistryDO, ConnectionDO, RegistryDO };
|
|
30
36
|
export type { Env };
|
|
31
37
|
|
|
32
38
|
/**
|
|
@@ -28,6 +28,16 @@ class_name = "RegistryDO"
|
|
|
28
28
|
name = "CHANNEL_DO"
|
|
29
29
|
class_name = "ChannelDO"
|
|
30
30
|
|
|
31
|
+
[[durable_objects.bindings]]
|
|
32
|
+
name = "CHANNEL_REGISTRY_DO"
|
|
33
|
+
class_name = "ChannelRegistryDO"
|
|
34
|
+
|
|
35
|
+
# D1 backing for persistent SASL accounts
|
|
36
|
+
[[d1_databases]]
|
|
37
|
+
binding = "ACCOUNTS_DB"
|
|
38
|
+
database_name = "cf-worker-test-accounts"
|
|
39
|
+
database_id = "test-d1-worker-accounts"
|
|
40
|
+
|
|
31
41
|
[[migrations]]
|
|
32
42
|
tag = "v1"
|
|
33
|
-
new_classes = ["ConnectionDO", "RegistryDO", "ChannelDO"]
|
|
43
|
+
new_classes = ["ConnectionDO", "RegistryDO", "ChannelDO", "ChannelRegistryDO"]
|
|
@@ -50,7 +50,10 @@ invocation_logs = true
|
|
|
50
50
|
[vars]
|
|
51
51
|
SERVER_NAME = "irc.example.com"
|
|
52
52
|
NETWORK_NAME = "ServerlessIRCd"
|
|
53
|
-
MOTD_LINES = "
|
|
53
|
+
MOTD_LINES = """
|
|
54
|
+
Welcome to ServerlessIRCd.
|
|
55
|
+
This is a deployed Cloudflare Worker.
|
|
56
|
+
"""
|
|
54
57
|
|
|
55
58
|
## ---------------------------------------------------------------------------
|
|
56
59
|
## Staging environment.
|
|
@@ -68,7 +71,30 @@ head_sampling_rate = 1
|
|
|
68
71
|
[env.staging.vars]
|
|
69
72
|
SERVER_NAME = "irc-staging.example.com"
|
|
70
73
|
NETWORK_NAME = "ServerlessIRCd (staging)"
|
|
71
|
-
MOTD_LINES = "
|
|
74
|
+
MOTD_LINES = """
|
|
75
|
+
Welcome to ServerlessIRCd staging.
|
|
76
|
+
This is a deployed Cloudflare Worker.
|
|
77
|
+
This Worker was deployed by CI from the main branch.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
## ---------------------------------------------------------------------------
|
|
81
|
+
## D1 database — SASL account persistence.
|
|
82
|
+
## ---------------------------------------------------------------------------
|
|
83
|
+
## The `accounts` table stores scrypt-hashed SASL PLAIN credentials (never
|
|
84
|
+
## plaintext). Loaded at ConnectionDO construction; when empty or unreachable
|
|
85
|
+
## the resolver falls back to the `SASL_ACCOUNTS` env-var seed. Provision with:
|
|
86
|
+
## wrangler d1 create serverless-ircd-accounts
|
|
87
|
+
## wrangler d1 execute serverless-ircd-accounts --command \
|
|
88
|
+
## "CREATE TABLE IF NOT EXISTS accounts (account TEXT PRIMARY KEY, algorithm TEXT NOT NULL, salt TEXT NOT NULL, hash TEXT NOT NULL)"
|
|
89
|
+
## Seed accounts via `tools/seed-cf-accounts.ts`.
|
|
90
|
+
[[d1_databases]]
|
|
91
|
+
binding = "ACCOUNTS_DB"
|
|
92
|
+
database_name = "serverless-ircd-accounts"
|
|
93
|
+
# database_id is filled in by `wrangler d1 create` and is env-specific.
|
|
94
|
+
|
|
95
|
+
[[env.staging.d1_databases]]
|
|
96
|
+
binding = "ACCOUNTS_DB"
|
|
97
|
+
database_name = "serverless-ircd-accounts-staging"
|
|
72
98
|
|
|
73
99
|
## ---------------------------------------------------------------------------
|
|
74
100
|
## Durable Object bindings.
|
|
@@ -91,6 +117,10 @@ class_name = "RegistryDO"
|
|
|
91
117
|
name = "CHANNEL_DO"
|
|
92
118
|
class_name = "ChannelDO"
|
|
93
119
|
|
|
120
|
+
[[durable_objects.bindings]]
|
|
121
|
+
name = "CHANNEL_REGISTRY_DO"
|
|
122
|
+
class_name = "ChannelRegistryDO"
|
|
123
|
+
|
|
94
124
|
# Staging environment bindings (wrangler requires per-env blocks).
|
|
95
125
|
[[env.staging.durable_objects.bindings]]
|
|
96
126
|
name = "CONNECTION_DO"
|
|
@@ -104,20 +134,40 @@ class_name = "RegistryDO"
|
|
|
104
134
|
name = "CHANNEL_DO"
|
|
105
135
|
class_name = "ChannelDO"
|
|
106
136
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
##
|
|
112
|
-
##
|
|
113
|
-
##
|
|
114
|
-
## `tag`
|
|
115
|
-
##
|
|
116
|
-
##
|
|
117
|
-
|
|
118
|
-
tag
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
[[
|
|
122
|
-
tag = "v1"
|
|
123
|
-
new_sqlite_classes = ["ConnectionDO", "RegistryDO", "ChannelDO"]
|
|
137
|
+
[[env.staging.durable_objects.bindings]]
|
|
138
|
+
name = "CHANNEL_REGISTRY_DO"
|
|
139
|
+
class_name = "ChannelRegistryDO"
|
|
140
|
+
|
|
141
|
+
# ## ---------------------------------------------------------------------------
|
|
142
|
+
# ## Durable Object migrations.
|
|
143
|
+
# ## ---------------------------------------------------------------------------
|
|
144
|
+
# ## `tag` is the user-defined migration id; wrangler refuses to deploy if
|
|
145
|
+
# ## the on-disk migrations don't match the live Workers state. `new_classes`
|
|
146
|
+
# ## declares the three DO classes on first deploy; subsequent schema
|
|
147
|
+
# ## changes (renames, deletions, splits) extend this list with their own
|
|
148
|
+
# ## `tag` so the runtime can migrate persisted state.
|
|
149
|
+
# ##
|
|
150
|
+
# ## See https://developers.cloudflare.com/durable-objects/reference/durable-objects-migrations/
|
|
151
|
+
# [[migrations]]
|
|
152
|
+
# tag = "v1"
|
|
153
|
+
# new_sqlite_classes = ["ConnectionDO", "RegistryDO", "ChannelDO", "ChannelRegistryDO"]
|
|
154
|
+
|
|
155
|
+
# [[env.staging.migrations]]
|
|
156
|
+
# tag = "v1"
|
|
157
|
+
# new_sqlite_classes = ["ConnectionDO", "RegistryDO", "ChannelDO", "ChannelRegistryDO"]
|
|
158
|
+
|
|
159
|
+
[exports.ConnectionDO]
|
|
160
|
+
type = "durable-object"
|
|
161
|
+
storage = "sqlite"
|
|
162
|
+
|
|
163
|
+
[exports.RegistryDO]
|
|
164
|
+
type = "durable-object"
|
|
165
|
+
storage = "sqlite"
|
|
166
|
+
|
|
167
|
+
[exports.ChannelDO]
|
|
168
|
+
type = "durable-object"
|
|
169
|
+
storage = "sqlite"
|
|
170
|
+
|
|
171
|
+
[exports.ChannelRegistryDO]
|
|
172
|
+
type = "durable-object"
|
|
173
|
+
storage = "sqlite"
|
|
@@ -21,6 +21,16 @@ import { type ParsedServerConfig, parseServerConfig } from '@serverless-ircd/irc
|
|
|
21
21
|
export interface CliConfigInput {
|
|
22
22
|
serverName: string;
|
|
23
23
|
networkName: string;
|
|
24
|
+
/**
|
|
25
|
+
* Server version surfaced in `002`/`004`/`351`/`371`. When omitted the
|
|
26
|
+
* schema default ({@link DEFAULT_SERVER_VERSION}) applies.
|
|
27
|
+
*/
|
|
28
|
+
serverVersion?: string;
|
|
29
|
+
/**
|
|
30
|
+
* "Created" text for `003 RPL_CREATED`. A string is used verbatim; a
|
|
31
|
+
* number is treated as epoch-ms and formatted as a UTC timestamp.
|
|
32
|
+
*/
|
|
33
|
+
createdAt?: string | number;
|
|
24
34
|
motdLines?: string[];
|
|
25
35
|
channelPrefixes?: string;
|
|
26
36
|
maxClients?: number;
|
|
@@ -32,6 +42,7 @@ export interface CliConfigInput {
|
|
|
32
42
|
channelLen?: number;
|
|
33
43
|
topicLen?: number;
|
|
34
44
|
quitMessage?: string;
|
|
45
|
+
saslAccounts?: Array<{ username: string; password: string }>;
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
/**
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* Both listeners share one runtime, so a TCP client and a WS client can
|
|
15
15
|
* see each other (cross-transport channel broadcast). Pass `--no-tcp` to
|
|
16
16
|
* run WebSocket-only; the WebSocket port / hostname / MOTD are tunable via
|
|
17
|
-
* flags
|
|
17
|
+
* flags.
|
|
18
18
|
*
|
|
19
19
|
* Usage:
|
|
20
20
|
* pnpm --filter local-cli start -- --port 6667 --tcp-port 6668 --host 127.0.0.1
|