onelobby 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +77 -2
- package/dist/chat.d.ts +60 -0
- package/dist/chat.js +163 -0
- package/dist/chat.js.map +1 -0
- package/dist/chunking.d.ts +17 -0
- package/dist/chunking.js +59 -0
- package/dist/chunking.js.map +1 -0
- package/dist/clock.d.ts +8 -0
- package/dist/clock.js +39 -0
- package/dist/clock.js.map +1 -0
- package/dist/codec.d.ts +141 -0
- package/dist/codec.js +308 -0
- package/dist/codec.js.map +1 -0
- package/dist/crockford.d.ts +14 -0
- package/dist/crockford.js +55 -0
- package/dist/crockford.js.map +1 -0
- package/dist/deflate.d.ts +4 -0
- package/dist/deflate.js +32 -0
- package/dist/deflate.js.map +1 -0
- package/dist/deprecated.d.ts +18 -0
- package/dist/deprecated.js +11 -0
- package/dist/deprecated.js.map +1 -0
- package/dist/direct/hub-host.d.ts +82 -0
- package/dist/direct/hub-host.js +393 -0
- package/dist/direct/hub-host.js.map +1 -0
- package/dist/direct/hub.d.ts +178 -0
- package/dist/direct/hub.js +862 -0
- package/dist/direct/hub.js.map +1 -0
- package/dist/direct/index.d.ts +57 -0
- package/dist/direct/index.js +71 -0
- package/dist/direct/index.js.map +1 -0
- package/dist/direct/portmap.d.ts +84 -0
- package/dist/direct/portmap.js +465 -0
- package/dist/direct/portmap.js.map +1 -0
- package/dist/direct-address.d.ts +20 -0
- package/dist/direct-address.js +61 -0
- package/dist/direct-address.js.map +1 -0
- package/dist/emitter.d.ts +9 -0
- package/dist/emitter.js +36 -0
- package/dist/emitter.js.map +1 -0
- package/dist/friendly.d.ts +20 -0
- package/dist/friendly.js +120 -0
- package/dist/friendly.js.map +1 -0
- package/dist/global.d.ts +8 -0
- package/dist/global.js +9 -0
- package/dist/global.js.map +1 -0
- package/dist/gridpaint.d.ts +27 -0
- package/dist/gridpaint.js +106 -0
- package/dist/gridpaint.js.map +1 -0
- package/dist/hub.d.ts +25 -0
- package/dist/hub.js +2 -0
- package/dist/hub.js.map +1 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/internals.d.ts +28 -0
- package/dist/internals.js +24 -0
- package/dist/internals.js.map +1 -0
- package/dist/joincode.d.ts +21 -0
- package/dist/joincode.js +84 -0
- package/dist/joincode.js.map +1 -0
- package/dist/lobby.d.ts +203 -0
- package/dist/lobby.js +840 -0
- package/dist/lobby.js.map +1 -0
- package/dist/lockstep.d.ts +132 -0
- package/dist/lockstep.js +471 -0
- package/dist/lockstep.js.map +1 -0
- package/dist/migration.d.ts +17 -0
- package/dist/migration.js +34 -0
- package/dist/migration.js.map +1 -0
- package/dist/onelobby.bundle.mjs +140 -0
- package/dist/onelobby.bundle.mjs.map +7 -0
- package/dist/onelobby.d.ts +151 -0
- package/dist/onelobby.global.js +3897 -0
- package/dist/onelobby.global.js.map +7 -0
- package/dist/onelobby.global.min.js +140 -0
- package/dist/onelobby.global.min.js.map +7 -0
- package/dist/onelobby.js +337 -0
- package/dist/onelobby.js.map +1 -0
- package/dist/signaling.d.ts +94 -0
- package/dist/signaling.js +134 -0
- package/dist/signaling.js.map +1 -0
- package/dist/socket.d.ts +63 -0
- package/dist/socket.js +142 -0
- package/dist/socket.js.map +1 -0
- package/dist/testing.d.ts +135 -0
- package/dist/testing.js +438 -0
- package/dist/testing.js.map +1 -0
- package/dist/transport.d.ts +80 -0
- package/dist/transport.js +7 -0
- package/dist/transport.js.map +1 -0
- package/dist/ui/controller.d.ts +137 -0
- package/dist/ui/controller.js +337 -0
- package/dist/ui/controller.js.map +1 -0
- package/dist/ui/element.d.ts +22 -0
- package/dist/ui/element.js +284 -0
- package/dist/ui/element.js.map +1 -0
- package/dist/ui/index.d.ts +5 -0
- package/dist/ui/index.js +9 -0
- package/dist/ui/index.js.map +1 -0
- package/dist/ui/server-url.d.ts +9 -0
- package/dist/ui/server-url.js +39 -0
- package/dist/ui/server-url.js.map +1 -0
- package/dist/ui/styles.d.ts +2 -0
- package/dist/ui/styles.js +70 -0
- package/dist/ui/styles.js.map +1 -0
- package/dist/voice-browser.d.ts +19 -0
- package/dist/voice-browser.js +151 -0
- package/dist/voice-browser.js.map +1 -0
- package/dist/voice-core.d.ts +98 -0
- package/dist/voice-core.js +233 -0
- package/dist/voice-core.js.map +1 -0
- package/dist/voice-types.d.ts +35 -0
- package/dist/voice-types.js +3 -0
- package/dist/voice-types.js.map +1 -0
- package/dist/voice.d.ts +68 -0
- package/dist/voice.js +223 -0
- package/dist/voice.js.map +1 -0
- package/dist/webrtc.d.ts +29 -0
- package/dist/webrtc.js +169 -0
- package/dist/webrtc.js.map +1 -0
- package/package.json +93 -4
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
/// <reference types="node" />
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { createServer } from "node:net";
|
|
4
|
+
import { SignalingHub, directHubOptions } from "./hub.js";
|
|
5
|
+
import { PortMapper } from "./portmap.js";
|
|
6
|
+
const WS_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
|
7
|
+
const MAX_HEADER_BYTES = 8192;
|
|
8
|
+
const HANDSHAKE_TIMEOUT_MS = 5000;
|
|
9
|
+
const STATUS_TEXT = { 200: "OK", 301: "Moved Permanently", 302: "Found", 404: "Not Found", 500: "Internal Server Error" };
|
|
10
|
+
export class PortInUseError extends Error {
|
|
11
|
+
code = "port_in_use";
|
|
12
|
+
constructor(port, detail) {
|
|
13
|
+
super(`port_in_use: cannot listen on port ${port} (${detail}): is another copy of the game hosting?`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
export class DirectHubHost {
|
|
17
|
+
hub;
|
|
18
|
+
/**
|
|
19
|
+
* Plain `GET`s that are not WebSocket upgrades go here, so the host can serve its own join page: a browser opening
|
|
20
|
+
* `http://<host address>:<port>/` gets a page that joins with `kit.joinDirect(location.host)`. Null (default): 426.
|
|
21
|
+
*/
|
|
22
|
+
http = null;
|
|
23
|
+
/** The bound TCP port (the requested one, or the one the OS picked for 0). */
|
|
24
|
+
port;
|
|
25
|
+
portMapper = null;
|
|
26
|
+
server;
|
|
27
|
+
sockets = new Set();
|
|
28
|
+
ticker;
|
|
29
|
+
maxMessageBytes;
|
|
30
|
+
closed = false;
|
|
31
|
+
constructor(server, port, hub, maxMessageBytes) {
|
|
32
|
+
this.server = server;
|
|
33
|
+
this.port = port;
|
|
34
|
+
this.hub = hub;
|
|
35
|
+
this.maxMessageBytes = maxMessageBytes;
|
|
36
|
+
this.ticker = setInterval(() => this.hub.tick(), 1000);
|
|
37
|
+
this.ticker.unref?.();
|
|
38
|
+
server.on("connection", (sock) => this.accept(sock));
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Binds TCP `port` on every interface (IPv6 dual-stack where available). Hub options default to
|
|
42
|
+
* {@link directHubOptions} (one lobby, no codes, no ICE servers). Rejects with {@link PortInUseError}.
|
|
43
|
+
*/
|
|
44
|
+
static async listen(port, options, deps) {
|
|
45
|
+
const opts = { ...directHubOptions(8), ...options };
|
|
46
|
+
const server = createServer({ noDelay: true });
|
|
47
|
+
const tryListen = (host, ipv6Only) => new Promise((resolve, reject) => {
|
|
48
|
+
const onError = (e) => reject(e);
|
|
49
|
+
server.once("error", onError);
|
|
50
|
+
server.listen({ port, host, ipv6Only }, () => {
|
|
51
|
+
server.off("error", onError);
|
|
52
|
+
resolve();
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
try {
|
|
56
|
+
if (deps?.host)
|
|
57
|
+
await tryListen(deps.host);
|
|
58
|
+
else {
|
|
59
|
+
try {
|
|
60
|
+
await tryListen("::", false);
|
|
61
|
+
}
|
|
62
|
+
catch (e) {
|
|
63
|
+
if (e.code === "EADDRINUSE")
|
|
64
|
+
throw e;
|
|
65
|
+
await tryListen("0.0.0.0"); // no IPv6 on this machine
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
throw new PortInUseError(port, e.code ?? String(e));
|
|
71
|
+
}
|
|
72
|
+
const addr = server.address();
|
|
73
|
+
const bound = typeof addr === "object" && addr ? addr.port : port;
|
|
74
|
+
return new DirectHubHost(server, bound, new SignalingHub(opts, deps), opts.maxMessageBytes);
|
|
75
|
+
}
|
|
76
|
+
/** Joiner sockets currently open. */
|
|
77
|
+
get connectionCount() {
|
|
78
|
+
return this.sockets.size;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The host's own signaling socket: a WebSocket look-alike wired straight into the hub (no network). Pass it to the
|
|
82
|
+
* kit as `advanced.socketFactory: () => host.connectLocal()`.
|
|
83
|
+
*/
|
|
84
|
+
connectLocal() {
|
|
85
|
+
return new LocalHubSocket(this.hub);
|
|
86
|
+
}
|
|
87
|
+
/** Asks the home router to forward TCP and UDP `port` here (UPnP, then NAT-PMP; docs/direct-ip.md s.2a). */
|
|
88
|
+
openPort(options) {
|
|
89
|
+
if (this.portMapper)
|
|
90
|
+
throw new Error("the port is already being opened");
|
|
91
|
+
this.portMapper = new PortMapper(this.port, options);
|
|
92
|
+
return this.portMapper.open();
|
|
93
|
+
}
|
|
94
|
+
/** Stops listening, closes every joiner socket and removes the router mapping. */
|
|
95
|
+
async close() {
|
|
96
|
+
if (this.closed)
|
|
97
|
+
return;
|
|
98
|
+
this.closed = true;
|
|
99
|
+
clearInterval(this.ticker);
|
|
100
|
+
const unmap = this.portMapper?.close();
|
|
101
|
+
for (const s of this.sockets)
|
|
102
|
+
s.end(closeFrame(1001, "host left"));
|
|
103
|
+
await new Promise((resolve) => this.server.close(() => resolve()));
|
|
104
|
+
setTimeout(() => { for (const s of this.sockets)
|
|
105
|
+
s.destroy(); }, 300).unref?.();
|
|
106
|
+
await unmap;
|
|
107
|
+
}
|
|
108
|
+
// ---------------------------------------------------------------- one TCP connection
|
|
109
|
+
accept(sock) {
|
|
110
|
+
if (this.closed) {
|
|
111
|
+
sock.destroy();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
this.sockets.add(sock);
|
|
115
|
+
sock.on("close", () => this.sockets.delete(sock));
|
|
116
|
+
sock.on("error", () => { });
|
|
117
|
+
const ip = (sock.remoteAddress ?? "").replace(/^::ffff:/, "");
|
|
118
|
+
let head = Buffer.alloc(0);
|
|
119
|
+
const timer = setTimeout(() => sock.destroy(), HANDSHAKE_TIMEOUT_MS);
|
|
120
|
+
const onData = (chunk) => {
|
|
121
|
+
head = Buffer.concat([head, chunk]);
|
|
122
|
+
const end = head.indexOf("\r\n\r\n");
|
|
123
|
+
if (end < 0) {
|
|
124
|
+
if (head.length > MAX_HEADER_BYTES)
|
|
125
|
+
sock.destroy();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
clearTimeout(timer);
|
|
129
|
+
sock.off("data", onData);
|
|
130
|
+
const rest = head.subarray(end + 4);
|
|
131
|
+
if (!this.handshake(sock, head.subarray(0, end).toString("latin1")))
|
|
132
|
+
return;
|
|
133
|
+
this.run(sock, ip, rest);
|
|
134
|
+
};
|
|
135
|
+
sock.on("data", onData);
|
|
136
|
+
}
|
|
137
|
+
/** RFC 6455 s.4.2: answers 101 with Sec-WebSocket-Accept, or 200 for /healthz, or 400/426. */
|
|
138
|
+
handshake(sock, head) {
|
|
139
|
+
const [requestLine, ...lines] = head.split("\r\n");
|
|
140
|
+
const headers = new Map();
|
|
141
|
+
for (const l of lines) {
|
|
142
|
+
const i = l.indexOf(":");
|
|
143
|
+
if (i > 0)
|
|
144
|
+
headers.set(l.slice(0, i).trim().toLowerCase(), l.slice(i + 1).trim());
|
|
145
|
+
}
|
|
146
|
+
const [method, path] = requestLine.split(" ");
|
|
147
|
+
const reply = (status, body) => sock.end(`HTTP/1.1 ${status}\r\nContent-Type: text/plain\r\nContent-Length: ${Buffer.byteLength(body)}\r\nConnection: close\r\n\r\n${body}`);
|
|
148
|
+
if (method === "GET" && path === "/healthz") {
|
|
149
|
+
reply("200 OK", "ok");
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
const key = headers.get("sec-websocket-key");
|
|
153
|
+
const upgrade = /websocket/i.test(headers.get("upgrade") ?? "");
|
|
154
|
+
if (method === "GET" && !upgrade && this.http && path) {
|
|
155
|
+
let page = null;
|
|
156
|
+
try {
|
|
157
|
+
page = this.http(path.split("?")[0]);
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
page = { status: 500, type: "text/plain", body: "error" };
|
|
161
|
+
}
|
|
162
|
+
const p = page ?? { status: 404, type: "text/plain", body: "not found" };
|
|
163
|
+
const body = typeof p.body === "string" ? Buffer.from(p.body, "utf8") : Buffer.from(p.body);
|
|
164
|
+
const status = p.status ?? 200;
|
|
165
|
+
sock.write(`HTTP/1.1 ${status} ${STATUS_TEXT[status] ?? "OK"}\r\nContent-Type: ${p.type}\r\n` +
|
|
166
|
+
`Content-Length: ${body.length}\r\nCache-Control: no-store\r\nX-Content-Type-Options: nosniff\r\nConnection: close\r\n\r\n`);
|
|
167
|
+
sock.end(body);
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
if (method !== "GET" || !upgrade || !key) {
|
|
171
|
+
reply("426 Upgrade Required", "this port speaks WebSocket (OneLobby direct host)");
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
const accept = createHash("sha1").update(key + WS_GUID).digest("base64");
|
|
175
|
+
sock.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: ${accept}\r\n\r\n`);
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
run(sock, ip, initial) {
|
|
179
|
+
let open = true;
|
|
180
|
+
const sendText = (text) => {
|
|
181
|
+
if (open && !sock.destroyed)
|
|
182
|
+
sock.write(frame(0x1, Buffer.from(text, "utf8")));
|
|
183
|
+
};
|
|
184
|
+
const shut = (code, reason) => {
|
|
185
|
+
if (!open)
|
|
186
|
+
return;
|
|
187
|
+
open = false;
|
|
188
|
+
if (!sock.destroyed)
|
|
189
|
+
sock.end(closeFrame(code, reason));
|
|
190
|
+
};
|
|
191
|
+
let session = null;
|
|
192
|
+
session = this.hub.connect(ip, sendText, () => shut(1000, "closed by the hub"));
|
|
193
|
+
if (!session)
|
|
194
|
+
return;
|
|
195
|
+
const s = session;
|
|
196
|
+
const reader = new FrameReader(this.maxMessageBytes + 1024, {
|
|
197
|
+
text: (t) => this.hub.receive(s, t),
|
|
198
|
+
ping: (p) => !sock.destroyed && sock.write(frame(0xa, p)),
|
|
199
|
+
close: () => {
|
|
200
|
+
shut(1000, "");
|
|
201
|
+
this.hub.disconnect(s);
|
|
202
|
+
},
|
|
203
|
+
fail: (code, why) => {
|
|
204
|
+
shut(code, why);
|
|
205
|
+
this.hub.disconnect(s);
|
|
206
|
+
},
|
|
207
|
+
});
|
|
208
|
+
sock.on("data", (b) => reader.push(b));
|
|
209
|
+
sock.on("close", () => {
|
|
210
|
+
open = false;
|
|
211
|
+
this.hub.disconnect(s);
|
|
212
|
+
});
|
|
213
|
+
if (initial.length)
|
|
214
|
+
reader.push(initial);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// ------------------------------------------------------------------------------------------------ RFC 6455 framing
|
|
218
|
+
/** One unmasked server frame (FIN set). */
|
|
219
|
+
function frame(opcode, payload) {
|
|
220
|
+
const n = payload.length;
|
|
221
|
+
const head = n < 126 ? Buffer.from([0x80 | opcode, n]) : n < 65536 ? Buffer.alloc(4) : Buffer.alloc(10);
|
|
222
|
+
if (n >= 126) {
|
|
223
|
+
head[0] = 0x80 | opcode;
|
|
224
|
+
if (n < 65536) {
|
|
225
|
+
head[1] = 126;
|
|
226
|
+
head.writeUInt16BE(n, 2);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
head[1] = 127;
|
|
230
|
+
head.writeBigUInt64BE(BigInt(n), 2);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return Buffer.concat([head, payload]);
|
|
234
|
+
}
|
|
235
|
+
function closeFrame(code, reason) {
|
|
236
|
+
const r = Buffer.from(reason.slice(0, 100), "utf8");
|
|
237
|
+
const p = Buffer.alloc(2 + r.length);
|
|
238
|
+
p.writeUInt16BE(code, 0);
|
|
239
|
+
r.copy(p, 2);
|
|
240
|
+
return frame(0x8, p);
|
|
241
|
+
}
|
|
242
|
+
/** Incremental client-frame parser: masked frames only, text messages (fragmented or not), control frames between. */
|
|
243
|
+
class FrameReader {
|
|
244
|
+
maxMessage;
|
|
245
|
+
sink;
|
|
246
|
+
buf = Buffer.alloc(0);
|
|
247
|
+
parts = [];
|
|
248
|
+
partBytes = 0;
|
|
249
|
+
inMessage = false;
|
|
250
|
+
binary = false;
|
|
251
|
+
done = false;
|
|
252
|
+
utf8 = new TextDecoder("utf-8", { fatal: true });
|
|
253
|
+
constructor(maxMessage, sink) {
|
|
254
|
+
this.maxMessage = maxMessage;
|
|
255
|
+
this.sink = sink;
|
|
256
|
+
}
|
|
257
|
+
push(chunk) {
|
|
258
|
+
if (this.done)
|
|
259
|
+
return;
|
|
260
|
+
this.buf = this.buf.length ? Buffer.concat([this.buf, chunk]) : chunk;
|
|
261
|
+
while (!this.done) {
|
|
262
|
+
if (this.buf.length < 2)
|
|
263
|
+
return;
|
|
264
|
+
const b0 = this.buf[0];
|
|
265
|
+
const b1 = this.buf[1];
|
|
266
|
+
const fin = (b0 & 0x80) !== 0;
|
|
267
|
+
const opcode = b0 & 0x0f;
|
|
268
|
+
if (b0 & 0x70)
|
|
269
|
+
return this.fail(1002, "reserved bits set");
|
|
270
|
+
if (!(b1 & 0x80))
|
|
271
|
+
return this.fail(1002, "client frames must be masked");
|
|
272
|
+
let len = b1 & 0x7f;
|
|
273
|
+
let off = 2;
|
|
274
|
+
if (len === 126) {
|
|
275
|
+
if (this.buf.length < 4)
|
|
276
|
+
return;
|
|
277
|
+
len = this.buf.readUInt16BE(2);
|
|
278
|
+
off = 4;
|
|
279
|
+
}
|
|
280
|
+
else if (len === 127) {
|
|
281
|
+
if (this.buf.length < 10)
|
|
282
|
+
return;
|
|
283
|
+
const big = this.buf.readBigUInt64BE(2);
|
|
284
|
+
if (big > BigInt(this.maxMessage))
|
|
285
|
+
return this.fail(1009, "message too big");
|
|
286
|
+
len = Number(big);
|
|
287
|
+
off = 10;
|
|
288
|
+
}
|
|
289
|
+
if (len > this.maxMessage)
|
|
290
|
+
return this.fail(1009, "message too big");
|
|
291
|
+
if (this.buf.length < off + 4 + len)
|
|
292
|
+
return;
|
|
293
|
+
const mask = this.buf.subarray(off, off + 4);
|
|
294
|
+
const payload = Buffer.from(this.buf.subarray(off + 4, off + 4 + len));
|
|
295
|
+
for (let i = 0; i < payload.length; i++)
|
|
296
|
+
payload[i] ^= mask[i & 3];
|
|
297
|
+
this.buf = this.buf.subarray(off + 4 + len);
|
|
298
|
+
if (opcode >= 0x8) {
|
|
299
|
+
if (!fin || len > 125)
|
|
300
|
+
return this.fail(1002, "bad control frame");
|
|
301
|
+
if (opcode === 0x8) {
|
|
302
|
+
this.done = true;
|
|
303
|
+
this.sink.close();
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (opcode === 0x9)
|
|
307
|
+
this.sink.ping(payload);
|
|
308
|
+
continue; // pong or unknown control: ignore
|
|
309
|
+
}
|
|
310
|
+
if (opcode === 0x1 || opcode === 0x2) {
|
|
311
|
+
if (this.inMessage)
|
|
312
|
+
return this.fail(1002, "new message inside a fragmented one");
|
|
313
|
+
this.inMessage = true;
|
|
314
|
+
this.binary = opcode === 0x2; // spec/signaling.md: binary frames are ignored
|
|
315
|
+
}
|
|
316
|
+
else if (opcode !== 0x0 || !this.inMessage)
|
|
317
|
+
return this.fail(1002, "unexpected continuation");
|
|
318
|
+
this.partBytes += payload.length;
|
|
319
|
+
if (this.partBytes > this.maxMessage)
|
|
320
|
+
return this.fail(1009, "message too big");
|
|
321
|
+
if (!this.binary)
|
|
322
|
+
this.parts.push(payload);
|
|
323
|
+
if (fin) {
|
|
324
|
+
const all = this.parts.length === 1 ? this.parts[0] : Buffer.concat(this.parts);
|
|
325
|
+
const skip = this.binary;
|
|
326
|
+
this.parts = [];
|
|
327
|
+
this.partBytes = 0;
|
|
328
|
+
this.inMessage = false;
|
|
329
|
+
this.binary = false;
|
|
330
|
+
if (skip)
|
|
331
|
+
continue;
|
|
332
|
+
let text;
|
|
333
|
+
try {
|
|
334
|
+
text = this.utf8.decode(all);
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
return this.fail(1007, "invalid UTF-8");
|
|
338
|
+
}
|
|
339
|
+
this.sink.text(text);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
fail(code, why) {
|
|
344
|
+
this.done = true;
|
|
345
|
+
this.sink.fail(code, why);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// ------------------------------------------------------------------------------------------------ in-memory socket
|
|
349
|
+
/**
|
|
350
|
+
* The direct-IP host's own signaling socket (C#'s `HubSignalingSocket`): sends go straight into the hub; the hub's
|
|
351
|
+
* replies are delivered on a microtask, so callbacks never run inside the hub.
|
|
352
|
+
*/
|
|
353
|
+
export class LocalHubSocket {
|
|
354
|
+
hub;
|
|
355
|
+
/** Address the hub sees for the host itself (rate limits and kicks are per address). */
|
|
356
|
+
static ADDRESS = "local";
|
|
357
|
+
readyState = 0;
|
|
358
|
+
onopen = null;
|
|
359
|
+
onmessage = null;
|
|
360
|
+
onclose = null;
|
|
361
|
+
onerror = null;
|
|
362
|
+
session;
|
|
363
|
+
constructor(hub) {
|
|
364
|
+
this.hub = hub;
|
|
365
|
+
this.session = hub.connect(LocalHubSocket.ADDRESS, (text) => queueMicrotask(() => this.readyState === 1 && this.onmessage?.({ data: text })), () => this.finish());
|
|
366
|
+
queueMicrotask(() => {
|
|
367
|
+
if (!this.session) {
|
|
368
|
+
this.finish();
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
this.readyState = 1;
|
|
372
|
+
this.onopen?.();
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
send(data) {
|
|
376
|
+
if (this.readyState === 1 && this.session)
|
|
377
|
+
this.hub.receive(this.session, data);
|
|
378
|
+
}
|
|
379
|
+
close() {
|
|
380
|
+
const s = this.session;
|
|
381
|
+
const wasOpen = this.readyState === 1;
|
|
382
|
+
this.finish();
|
|
383
|
+
if (wasOpen && s)
|
|
384
|
+
this.hub.disconnect(s);
|
|
385
|
+
}
|
|
386
|
+
finish() {
|
|
387
|
+
if (this.readyState === 3)
|
|
388
|
+
return;
|
|
389
|
+
this.readyState = 3;
|
|
390
|
+
queueMicrotask(() => this.onclose?.());
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
//# sourceMappingURL=hub-host.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hub-host.js","sourceRoot":"","sources":["../../src/direct/hub-host.ts"],"names":[],"mappings":"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","sourcesContent":["/// <reference types=\"node\" />\nimport { createHash } from \"node:crypto\";\nimport { createServer, type Server, type Socket } from \"node:net\";\nimport type { SocketLike } from \"../transport.js\";\nimport { SignalingHub, directHubOptions, type HubDeps, type HubOptions, type Session } from \"./hub.js\";\nimport { PortMapper, type PortMapperOptions, type PortMapResult } from \"./portmap.js\";\n\nconst WS_GUID = \"258EAFA5-E914-47DA-95CA-C5AB0DC85B11\";\nconst MAX_HEADER_BYTES = 8192;\nconst HANDSHAKE_TIMEOUT_MS = 5000;\nconst STATUS_TEXT: Record<number, string> = { 200: \"OK\", 301: \"Moved Permanently\", 302: \"Found\", 404: \"Not Found\", 500: \"Internal Server Error\" };\n\nexport class PortInUseError extends Error {\n readonly code = \"port_in_use\";\n constructor(port: number, detail: string) {\n super(`port_in_use: cannot listen on port ${port} (${detail}): is another copy of the game hosting?`);\n }\n}\n\n/**\n * Runs a {@link SignalingHub} inside a Node process and listens for joiners' signaling WebSockets on TCP `port`: the\n * Node twin of C#'s `DirectHubHost` (docs/direct-ip.md s.2, s.5 step 3). A plain `net` server with a minimal HTTP/1.1\n * upgrade and RFC 6455 framing (text, close, ping/pong, fragments; client frames must be masked), so the package keeps\n * zero runtime dependencies. Any path upgrades; `GET /healthz` answers `ok`. The host's own client talks to the hub in\n * memory through {@link connectLocal}. `hub.tick()` runs once a second on an unref'd timer.\n */\n/** One plain-HTTP answer from the host's port, e.g. a browser join page. */\nexport interface HttpReply {\n /** Default 200. */\n status?: number;\n /** Content-Type, e.g. \"text/html; charset=utf-8\". */\n type: string;\n body: string | Uint8Array;\n}\n\n/** Serves plain `GET` requests on the direct port (path without the query); null answers 404. */\nexport type HttpHandler = (path: string) => HttpReply | null;\n\nexport class DirectHubHost {\n readonly hub: SignalingHub;\n /**\n * Plain `GET`s that are not WebSocket upgrades go here, so the host can serve its own join page: a browser opening\n * `http://<host address>:<port>/` gets a page that joins with `kit.joinDirect(location.host)`. Null (default): 426.\n */\n http: HttpHandler | null = null;\n /** The bound TCP port (the requested one, or the one the OS picked for 0). */\n readonly port: number;\n portMapper: PortMapper | null = null;\n private readonly server: Server;\n private readonly sockets = new Set<Socket>();\n private readonly ticker: ReturnType<typeof setInterval>;\n private readonly maxMessageBytes: number;\n private closed = false;\n\n private constructor(server: Server, port: number, hub: SignalingHub, maxMessageBytes: number) {\n this.server = server;\n this.port = port;\n this.hub = hub;\n this.maxMessageBytes = maxMessageBytes;\n this.ticker = setInterval(() => this.hub.tick(), 1000);\n this.ticker.unref?.();\n server.on(\"connection\", (sock) => this.accept(sock));\n }\n\n /**\n * Binds TCP `port` on every interface (IPv6 dual-stack where available). Hub options default to\n * {@link directHubOptions} (one lobby, no codes, no ICE servers). Rejects with {@link PortInUseError}.\n */\n static async listen(port: number, options?: Partial<HubOptions>, deps?: HubDeps & { host?: string }): Promise<DirectHubHost> {\n const opts = { ...directHubOptions(8), ...options };\n const server = createServer({ noDelay: true });\n const tryListen = (host: string, ipv6Only?: boolean) =>\n new Promise<void>((resolve, reject) => {\n const onError = (e: NodeJS.ErrnoException) => reject(e);\n server.once(\"error\", onError);\n server.listen({ port, host, ipv6Only }, () => {\n server.off(\"error\", onError);\n resolve();\n });\n });\n try {\n if (deps?.host) await tryListen(deps.host);\n else {\n try {\n await tryListen(\"::\", false);\n } catch (e) {\n if ((e as NodeJS.ErrnoException).code === \"EADDRINUSE\") throw e;\n await tryListen(\"0.0.0.0\"); // no IPv6 on this machine\n }\n }\n } catch (e) {\n throw new PortInUseError(port, (e as NodeJS.ErrnoException).code ?? String(e));\n }\n const addr = server.address();\n const bound = typeof addr === \"object\" && addr ? addr.port : port;\n return new DirectHubHost(server, bound, new SignalingHub(opts, deps), opts.maxMessageBytes);\n }\n\n /** Joiner sockets currently open. */\n get connectionCount(): number {\n return this.sockets.size;\n }\n\n /**\n * The host's own signaling socket: a WebSocket look-alike wired straight into the hub (no network). Pass it to the\n * kit as `advanced.socketFactory: () => host.connectLocal()`.\n */\n connectLocal(): SocketLike {\n return new LocalHubSocket(this.hub);\n }\n\n /** Asks the home router to forward TCP and UDP `port` here (UPnP, then NAT-PMP; docs/direct-ip.md s.2a). */\n openPort(options?: PortMapperOptions): Promise<PortMapResult> {\n if (this.portMapper) throw new Error(\"the port is already being opened\");\n this.portMapper = new PortMapper(this.port, options);\n return this.portMapper.open();\n }\n\n /** Stops listening, closes every joiner socket and removes the router mapping. */\n async close(): Promise<void> {\n if (this.closed) return;\n this.closed = true;\n clearInterval(this.ticker);\n const unmap = this.portMapper?.close();\n for (const s of this.sockets) s.end(closeFrame(1001, \"host left\"));\n await new Promise<void>((resolve) => this.server.close(() => resolve()));\n setTimeout(() => { for (const s of this.sockets) s.destroy(); }, 300).unref?.();\n await unmap;\n }\n\n // ---------------------------------------------------------------- one TCP connection\n\n private accept(sock: Socket): void {\n if (this.closed) {\n sock.destroy();\n return;\n }\n this.sockets.add(sock);\n sock.on(\"close\", () => this.sockets.delete(sock));\n sock.on(\"error\", () => {});\n const ip = (sock.remoteAddress ?? \"\").replace(/^::ffff:/, \"\");\n let head = Buffer.alloc(0);\n const timer = setTimeout(() => sock.destroy(), HANDSHAKE_TIMEOUT_MS);\n const onData = (chunk: Buffer) => {\n head = Buffer.concat([head, chunk]);\n const end = head.indexOf(\"\\r\\n\\r\\n\");\n if (end < 0) {\n if (head.length > MAX_HEADER_BYTES) sock.destroy();\n return;\n }\n clearTimeout(timer);\n sock.off(\"data\", onData);\n const rest = head.subarray(end + 4);\n if (!this.handshake(sock, head.subarray(0, end).toString(\"latin1\"))) return;\n this.run(sock, ip, rest);\n };\n sock.on(\"data\", onData);\n }\n\n /** RFC 6455 s.4.2: answers 101 with Sec-WebSocket-Accept, or 200 for /healthz, or 400/426. */\n private handshake(sock: Socket, head: string): boolean {\n const [requestLine, ...lines] = head.split(\"\\r\\n\");\n const headers = new Map<string, string>();\n for (const l of lines) {\n const i = l.indexOf(\":\");\n if (i > 0) headers.set(l.slice(0, i).trim().toLowerCase(), l.slice(i + 1).trim());\n }\n const [method, path] = requestLine.split(\" \");\n const reply = (status: string, body: string) =>\n sock.end(`HTTP/1.1 ${status}\\r\\nContent-Type: text/plain\\r\\nContent-Length: ${Buffer.byteLength(body)}\\r\\nConnection: close\\r\\n\\r\\n${body}`);\n if (method === \"GET\" && path === \"/healthz\") {\n reply(\"200 OK\", \"ok\");\n return false;\n }\n const key = headers.get(\"sec-websocket-key\");\n const upgrade = /websocket/i.test(headers.get(\"upgrade\") ?? \"\");\n if (method === \"GET\" && !upgrade && this.http && path) {\n let page: HttpReply | null = null;\n try {\n page = this.http(path.split(\"?\")[0]);\n } catch {\n page = { status: 500, type: \"text/plain\", body: \"error\" };\n }\n const p = page ?? { status: 404, type: \"text/plain\", body: \"not found\" };\n const body = typeof p.body === \"string\" ? Buffer.from(p.body, \"utf8\") : Buffer.from(p.body);\n const status = p.status ?? 200;\n sock.write(`HTTP/1.1 ${status} ${STATUS_TEXT[status] ?? \"OK\"}\\r\\nContent-Type: ${p.type}\\r\\n` +\n `Content-Length: ${body.length}\\r\\nCache-Control: no-store\\r\\nX-Content-Type-Options: nosniff\\r\\nConnection: close\\r\\n\\r\\n`);\n sock.end(body);\n return false;\n }\n if (method !== \"GET\" || !upgrade || !key) {\n reply(\"426 Upgrade Required\", \"this port speaks WebSocket (OneLobby direct host)\");\n return false;\n }\n const accept = createHash(\"sha1\").update(key + WS_GUID).digest(\"base64\");\n sock.write(`HTTP/1.1 101 Switching Protocols\\r\\nUpgrade: websocket\\r\\nConnection: Upgrade\\r\\nSec-WebSocket-Accept: ${accept}\\r\\n\\r\\n`);\n return true;\n }\n\n private run(sock: Socket, ip: string, initial: Buffer): void {\n let open = true;\n const sendText = (text: string) => {\n if (open && !sock.destroyed) sock.write(frame(0x1, Buffer.from(text, \"utf8\")));\n };\n const shut = (code: number, reason: string) => {\n if (!open) return;\n open = false;\n if (!sock.destroyed) sock.end(closeFrame(code, reason));\n };\n let session: Session | null = null;\n session = this.hub.connect(ip, sendText, () => shut(1000, \"closed by the hub\"));\n if (!session) return;\n const s = session;\n const reader = new FrameReader(this.maxMessageBytes + 1024, {\n text: (t) => this.hub.receive(s, t),\n ping: (p) => !sock.destroyed && sock.write(frame(0xa, p)),\n close: () => {\n shut(1000, \"\");\n this.hub.disconnect(s);\n },\n fail: (code, why) => {\n shut(code, why);\n this.hub.disconnect(s);\n },\n });\n sock.on(\"data\", (b: Buffer) => reader.push(b));\n sock.on(\"close\", () => {\n open = false;\n this.hub.disconnect(s);\n });\n if (initial.length) reader.push(initial);\n }\n}\n\n// ------------------------------------------------------------------------------------------------ RFC 6455 framing\n\n/** One unmasked server frame (FIN set). */\nfunction frame(opcode: number, payload: Buffer): Buffer {\n const n = payload.length;\n const head = n < 126 ? Buffer.from([0x80 | opcode, n]) : n < 65536 ? Buffer.alloc(4) : Buffer.alloc(10);\n if (n >= 126) {\n head[0] = 0x80 | opcode;\n if (n < 65536) {\n head[1] = 126;\n head.writeUInt16BE(n, 2);\n } else {\n head[1] = 127;\n head.writeBigUInt64BE(BigInt(n), 2);\n }\n }\n return Buffer.concat([head, payload]);\n}\n\nfunction closeFrame(code: number, reason: string): Buffer {\n const r = Buffer.from(reason.slice(0, 100), \"utf8\");\n const p = Buffer.alloc(2 + r.length);\n p.writeUInt16BE(code, 0);\n r.copy(p, 2);\n return frame(0x8, p);\n}\n\ninterface FrameSink {\n text(t: string): void;\n ping(payload: Buffer): void;\n close(): void;\n fail(code: number, why: string): void;\n}\n\n/** Incremental client-frame parser: masked frames only, text messages (fragmented or not), control frames between. */\nclass FrameReader {\n private buf: Buffer = Buffer.alloc(0);\n private parts: Buffer[] = [];\n private partBytes = 0;\n private inMessage = false;\n private binary = false;\n private done = false;\n private readonly utf8 = new TextDecoder(\"utf-8\", { fatal: true });\n\n constructor(private readonly maxMessage: number, private readonly sink: FrameSink) {}\n\n push(chunk: Buffer): void {\n if (this.done) return;\n this.buf = this.buf.length ? Buffer.concat([this.buf, chunk]) : chunk;\n while (!this.done) {\n if (this.buf.length < 2) return;\n const b0 = this.buf[0];\n const b1 = this.buf[1];\n const fin = (b0 & 0x80) !== 0;\n const opcode = b0 & 0x0f;\n if (b0 & 0x70) return this.fail(1002, \"reserved bits set\");\n if (!(b1 & 0x80)) return this.fail(1002, \"client frames must be masked\");\n let len = b1 & 0x7f;\n let off = 2;\n if (len === 126) {\n if (this.buf.length < 4) return;\n len = this.buf.readUInt16BE(2);\n off = 4;\n } else if (len === 127) {\n if (this.buf.length < 10) return;\n const big = this.buf.readBigUInt64BE(2);\n if (big > BigInt(this.maxMessage)) return this.fail(1009, \"message too big\");\n len = Number(big);\n off = 10;\n }\n if (len > this.maxMessage) return this.fail(1009, \"message too big\");\n if (this.buf.length < off + 4 + len) return;\n const mask = this.buf.subarray(off, off + 4);\n const payload = Buffer.from(this.buf.subarray(off + 4, off + 4 + len));\n for (let i = 0; i < payload.length; i++) payload[i] ^= mask[i & 3];\n this.buf = this.buf.subarray(off + 4 + len);\n if (opcode >= 0x8) {\n if (!fin || len > 125) return this.fail(1002, \"bad control frame\");\n if (opcode === 0x8) {\n this.done = true;\n this.sink.close();\n return;\n }\n if (opcode === 0x9) this.sink.ping(payload);\n continue; // pong or unknown control: ignore\n }\n if (opcode === 0x1 || opcode === 0x2) {\n if (this.inMessage) return this.fail(1002, \"new message inside a fragmented one\");\n this.inMessage = true;\n this.binary = opcode === 0x2; // spec/signaling.md: binary frames are ignored\n } else if (opcode !== 0x0 || !this.inMessage) return this.fail(1002, \"unexpected continuation\");\n this.partBytes += payload.length;\n if (this.partBytes > this.maxMessage) return this.fail(1009, \"message too big\");\n if (!this.binary) this.parts.push(payload);\n if (fin) {\n const all = this.parts.length === 1 ? this.parts[0] : Buffer.concat(this.parts);\n const skip = this.binary;\n this.parts = [];\n this.partBytes = 0;\n this.inMessage = false;\n this.binary = false;\n if (skip) continue;\n let text: string;\n try {\n text = this.utf8.decode(all);\n } catch {\n return this.fail(1007, \"invalid UTF-8\");\n }\n this.sink.text(text);\n }\n }\n }\n\n private fail(code: number, why: string): void {\n this.done = true;\n this.sink.fail(code, why);\n }\n}\n\n// ------------------------------------------------------------------------------------------------ in-memory socket\n\n/**\n * The direct-IP host's own signaling socket (C#'s `HubSignalingSocket`): sends go straight into the hub; the hub's\n * replies are delivered on a microtask, so callbacks never run inside the hub.\n */\nexport class LocalHubSocket implements SocketLike {\n /** Address the hub sees for the host itself (rate limits and kicks are per address). */\n static readonly ADDRESS = \"local\";\n readyState = 0;\n onopen: SocketLike[\"onopen\"] = null;\n onmessage: SocketLike[\"onmessage\"] = null;\n onclose: SocketLike[\"onclose\"] = null;\n onerror: SocketLike[\"onerror\"] = null;\n private session: Session | null;\n\n constructor(private readonly hub: SignalingHub) {\n this.session = hub.connect(LocalHubSocket.ADDRESS, (text) => queueMicrotask(() => this.readyState === 1 && this.onmessage?.({ data: text })), () =>\n this.finish(),\n );\n queueMicrotask(() => {\n if (!this.session) {\n this.finish();\n return;\n }\n this.readyState = 1;\n this.onopen?.();\n });\n }\n\n send(data: string): void {\n if (this.readyState === 1 && this.session) this.hub.receive(this.session, data);\n }\n\n close(): void {\n const s = this.session;\n const wasOpen = this.readyState === 1;\n this.finish();\n if (wasOpen && s) this.hub.disconnect(s);\n }\n\n private finish(): void {\n if (this.readyState === 3) return;\n this.readyState = 3;\n queueMicrotask(() => this.onclose?.());\n }\n}\n"]}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The signaling hub (protocol lk1) in TypeScript, for a Node process that hosts direct-IP lobbies
|
|
3
|
+
* (docs/direct-ip.md s.2): a line-by-line port of `server/OneLobby.Hub/Hub.cs`, held to the same rules by
|
|
4
|
+
* `conformance/server`. Transport-agnostic: feed it text frames with `receive`, call `tick` about once a second.
|
|
5
|
+
* Metrics are left out (that is onelobby-server's job); everything else (codes, list, resume, private lobbies,
|
|
6
|
+
* kicks, migration claims, TURN credentials, rate limits) behaves as in C#.
|
|
7
|
+
*/
|
|
8
|
+
export declare const PROTO = "lk1";
|
|
9
|
+
/** The settings the hub reads; same names and defaults as C#'s `HubOptions`. */
|
|
10
|
+
export interface HubOptions {
|
|
11
|
+
turnUrls: string[];
|
|
12
|
+
turnSecret: string;
|
|
13
|
+
stunUrls: string[];
|
|
14
|
+
region: string;
|
|
15
|
+
/** Empty = every gameId allowed. */
|
|
16
|
+
allowedGames: string[];
|
|
17
|
+
gameQuotas: Record<string, number>;
|
|
18
|
+
maxLobbies: number;
|
|
19
|
+
maxPeers: number;
|
|
20
|
+
/** Default and advertised maxPlayers limit; create accepts 2..this. */
|
|
21
|
+
hardMaxPlayers: number;
|
|
22
|
+
rateScale: number;
|
|
23
|
+
turnTtlSeconds: number;
|
|
24
|
+
maxMessageBytes: number;
|
|
25
|
+
disableIpLimits: boolean;
|
|
26
|
+
createPerMin: number;
|
|
27
|
+
joinPerMin: number;
|
|
28
|
+
signalPerSec: number;
|
|
29
|
+
openLobbiesPerIp: number;
|
|
30
|
+
idleTimeoutMs: number;
|
|
31
|
+
resumeWindowMs: number;
|
|
32
|
+
codeReuseMs: number;
|
|
33
|
+
listLimitMax: number;
|
|
34
|
+
/** One lobby per hub (direct IP): later creates are refused, a join without a code joins it. */
|
|
35
|
+
singleLobby: boolean;
|
|
36
|
+
/** When set, join must carry the same password or gets bad_password. */
|
|
37
|
+
password: string | null;
|
|
38
|
+
/** False: lobby messages carry code "" and list is empty. */
|
|
39
|
+
issueCodes: boolean;
|
|
40
|
+
}
|
|
41
|
+
export declare function defaultHubOptions(): HubOptions;
|
|
42
|
+
/** Settings for a hub inside the host's game (docs/direct-ip.md s.2): no codes, no list, no ICE servers. */
|
|
43
|
+
export declare function directHubOptions(maxPlayers: number, password?: string | null): HubOptions;
|
|
44
|
+
/** Sliding-window counter: at most `limit` events per `windowMs`. */
|
|
45
|
+
export declare class RateWindow {
|
|
46
|
+
readonly limit: number;
|
|
47
|
+
readonly windowMs: number;
|
|
48
|
+
private hits;
|
|
49
|
+
constructor(limit: number, windowMs: number);
|
|
50
|
+
tryHit(now: number): boolean;
|
|
51
|
+
retryAfterMs(now: number): number;
|
|
52
|
+
isIdle(now: number): boolean;
|
|
53
|
+
private trim;
|
|
54
|
+
}
|
|
55
|
+
/** Control characters and bidi overrides out, then at most `max` UTF-16 units without splitting a surrogate pair. */
|
|
56
|
+
export declare function sanitize(s: string | null | undefined, max: number, fallback?: string): string;
|
|
57
|
+
/** One signaling connection. The hub only calls `send` and `close`. */
|
|
58
|
+
export declare class Session {
|
|
59
|
+
readonly id: number;
|
|
60
|
+
readonly ip: string;
|
|
61
|
+
readonly connId: string;
|
|
62
|
+
readonly send: (text: string) => void;
|
|
63
|
+
readonly close: () => void;
|
|
64
|
+
readonly signalRate: RateWindow;
|
|
65
|
+
closed: boolean;
|
|
66
|
+
lastSeen: number;
|
|
67
|
+
helloDone: boolean;
|
|
68
|
+
gameId: string;
|
|
69
|
+
netVersion: string;
|
|
70
|
+
name: string;
|
|
71
|
+
lobby: HubLobby | null;
|
|
72
|
+
member: Member | null;
|
|
73
|
+
pendingIn: HubLobby | null;
|
|
74
|
+
pendingMember: Member | null;
|
|
75
|
+
constructor(id: number, ip: string, connId: string, send: (text: string) => void, close: () => void, signalRate: RateWindow);
|
|
76
|
+
}
|
|
77
|
+
declare class Member {
|
|
78
|
+
peerId: number;
|
|
79
|
+
name: string;
|
|
80
|
+
joinedAt: number;
|
|
81
|
+
session: Session | null;
|
|
82
|
+
detachedAt: number;
|
|
83
|
+
token: string;
|
|
84
|
+
ip: string;
|
|
85
|
+
}
|
|
86
|
+
declare class HubLobby {
|
|
87
|
+
code: string;
|
|
88
|
+
gameId: string;
|
|
89
|
+
netVersion: string;
|
|
90
|
+
name: string;
|
|
91
|
+
visibility: string;
|
|
92
|
+
mode: string;
|
|
93
|
+
tickRate: number;
|
|
94
|
+
lateJoin: boolean;
|
|
95
|
+
relayOnly: boolean;
|
|
96
|
+
phase: string;
|
|
97
|
+
maxPlayers: number;
|
|
98
|
+
createdAt: number;
|
|
99
|
+
ownerIp: string;
|
|
100
|
+
meta: Record<string, string>;
|
|
101
|
+
members: Member[];
|
|
102
|
+
pending: Member[];
|
|
103
|
+
kicked: Set<string>;
|
|
104
|
+
hostId: number;
|
|
105
|
+
epoch: number;
|
|
106
|
+
nextPeerId: number;
|
|
107
|
+
find(peerId: number): Member | undefined;
|
|
108
|
+
}
|
|
109
|
+
export interface HubDeps {
|
|
110
|
+
/** Unix milliseconds; injected so tests drive time. */
|
|
111
|
+
now?: () => number;
|
|
112
|
+
/** Uniform integer in [0, n). */
|
|
113
|
+
randomInt?: (n: number) => number;
|
|
114
|
+
log?: (line: string) => void;
|
|
115
|
+
}
|
|
116
|
+
/** All signaling state and rules (DESIGN s.6, s.7, s.9, s.10, s.12; spec/signaling.md). */
|
|
117
|
+
export declare class SignalingHub {
|
|
118
|
+
private readonly o;
|
|
119
|
+
private readonly now;
|
|
120
|
+
private readonly rand;
|
|
121
|
+
private readonly log;
|
|
122
|
+
private readonly lobbies;
|
|
123
|
+
private readonly retired;
|
|
124
|
+
private readonly tokens;
|
|
125
|
+
private readonly sessions;
|
|
126
|
+
private readonly gameLobbies;
|
|
127
|
+
private readonly ips;
|
|
128
|
+
private nextSession;
|
|
129
|
+
private createdOnce;
|
|
130
|
+
private members;
|
|
131
|
+
private readonly logSalt;
|
|
132
|
+
constructor(options?: Partial<HubOptions>, deps?: HubDeps);
|
|
133
|
+
get lobbyCount(): number;
|
|
134
|
+
get socketCount(): number;
|
|
135
|
+
private get scale();
|
|
136
|
+
/** Registers a socket. Returns null (after sending server_full and closing) when over maxPeers. */
|
|
137
|
+
connect(ip: string, send: (text: string) => void, close: () => void): Session | null;
|
|
138
|
+
disconnect(s: Session): void;
|
|
139
|
+
private closeSession;
|
|
140
|
+
/** Call about once a second: idle timeouts, resume-window expiry, code retirement. */
|
|
141
|
+
tick(): void;
|
|
142
|
+
receive(s: Session, text: string): void;
|
|
143
|
+
private hello;
|
|
144
|
+
private iceList;
|
|
145
|
+
private ipOf;
|
|
146
|
+
private create;
|
|
147
|
+
private join;
|
|
148
|
+
private admit;
|
|
149
|
+
private addMember;
|
|
150
|
+
private list;
|
|
151
|
+
private leave;
|
|
152
|
+
private signal;
|
|
153
|
+
private requireHost;
|
|
154
|
+
private hostMeta;
|
|
155
|
+
private hostPhase;
|
|
156
|
+
private kick;
|
|
157
|
+
private approve;
|
|
158
|
+
/** Peer-side migration (s.9) tells the server: the new host announces itself with a higher epoch. */
|
|
159
|
+
private claimHost;
|
|
160
|
+
private pathReport;
|
|
161
|
+
/** Removes a member for good. A member that is already detached was announced as left, so nothing is sent for it. */
|
|
162
|
+
private removeMember;
|
|
163
|
+
/** Server mirror of the peer-side rule (s.9): new host is the lowest peerId still connected; epoch +1. */
|
|
164
|
+
private promoteHost;
|
|
165
|
+
private closeLobby;
|
|
166
|
+
private newCode;
|
|
167
|
+
private lobbyMsg;
|
|
168
|
+
private broadcastLobby;
|
|
169
|
+
private broadcast;
|
|
170
|
+
private peerObj;
|
|
171
|
+
private send;
|
|
172
|
+
private sendTo;
|
|
173
|
+
private error;
|
|
174
|
+
private rateLimited;
|
|
175
|
+
private logEvent;
|
|
176
|
+
private hashIp;
|
|
177
|
+
}
|
|
178
|
+
export {};
|