@honeybbq/teamspeak 1.0.0-beta.19 → 1.0.0-beta.21
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/dist/client/api/avatars.d.ts.map +1 -1
- package/dist/client/api/avatars.js +11 -2
- package/dist/client/api/avatars.js.map +1 -1
- package/dist/client/api/files.d.ts +36 -14
- package/dist/client/api/files.d.ts.map +1 -1
- package/dist/client/api/files.js +58 -9
- package/dist/client/api/files.js.map +1 -1
- package/dist/client/api/upload-checks.d.ts +36 -0
- package/dist/client/api/upload-checks.d.ts.map +1 -0
- package/dist/client/api/upload-checks.js +77 -0
- package/dist/client/api/upload-checks.js.map +1 -0
- package/dist/client/api/voice.d.ts +2 -1
- package/dist/client/api/voice.d.ts.map +1 -1
- package/dist/client/api/voice.js +6 -2
- package/dist/client/api/voice.js.map +1 -1
- package/dist/client/client.d.ts +13 -3
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +151 -49
- package/dist/client/client.js.map +1 -1
- package/dist/client/crypto/crypt.d.ts +6 -0
- package/dist/client/crypto/crypt.d.ts.map +1 -1
- package/dist/client/crypto/crypt.js +13 -7
- package/dist/client/crypto/crypt.js.map +1 -1
- package/dist/client/crypto/eax.d.ts +4 -0
- package/dist/client/crypto/eax.d.ts.map +1 -1
- package/dist/client/crypto/eax.js +27 -21
- package/dist/client/crypto/eax.js.map +1 -1
- package/dist/client/errors.d.ts +13 -0
- package/dist/client/errors.d.ts.map +1 -1
- package/dist/client/errors.js +23 -0
- package/dist/client/errors.js.map +1 -1
- package/dist/client/handshake/crypt-handshake.d.ts +2 -1
- package/dist/client/handshake/crypt-handshake.d.ts.map +1 -1
- package/dist/client/handshake/crypt-handshake.js +4 -2
- package/dist/client/handshake/crypt-handshake.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/throttle.d.ts +11 -0
- package/dist/client/throttle.d.ts.map +1 -1
- package/dist/client/throttle.js +38 -3
- package/dist/client/throttle.js.map +1 -1
- package/dist/client/transfer.d.ts +37 -4
- package/dist/client/transfer.d.ts.map +1 -1
- package/dist/client/transfer.js +155 -15
- package/dist/client/transfer.js.map +1 -1
- package/dist/client/transport/handler.d.ts.map +1 -1
- package/dist/client/transport/handler.js +47 -43
- package/dist/client/transport/handler.js.map +1 -1
- package/dist/core/api/clients.d.ts +15 -15
- package/dist/core/api/clients.d.ts.map +1 -1
- package/dist/core/api/clients.js.map +1 -1
- package/dist/core/api/command.d.ts +8 -6
- package/dist/core/api/command.d.ts.map +1 -1
- package/dist/core/api/command.js +1 -1
- package/dist/core/api/command.js.map +1 -1
- package/dist/core/api/files.d.ts +23 -6
- package/dist/core/api/files.d.ts.map +1 -1
- package/dist/core/api/files.js +12 -9
- package/dist/core/api/files.js.map +1 -1
- package/dist/core/api/groups.d.ts +17 -2
- package/dist/core/api/groups.d.ts.map +1 -1
- package/dist/core/api/groups.js +13 -11
- package/dist/core/api/groups.js.map +1 -1
- package/dist/core/api/parse.d.ts +5 -5
- package/dist/core/api/parse.d.ts.map +1 -1
- package/dist/core/api/parse.js +16 -15
- package/dist/core/api/parse.js.map +1 -1
- package/dist/core/command/request.d.ts +6 -0
- package/dist/core/command/request.d.ts.map +1 -1
- package/dist/core/command/request.js +12 -0
- package/dist/core/command/request.js.map +1 -1
- package/dist/core/errors.d.ts +46 -3
- package/dist/core/errors.d.ts.map +1 -1
- package/dist/core/errors.js +73 -20
- package/dist/core/errors.js.map +1 -1
- package/dist/core/events.d.ts +24 -8
- package/dist/core/events.d.ts.map +1 -1
- package/dist/core/internal.d.ts +1 -1
- package/dist/core/internal.d.ts.map +1 -1
- package/dist/core/internal.js +1 -1
- package/dist/core/internal.js.map +1 -1
- package/dist/core/notifications.d.ts +1 -1
- package/dist/core/notifications.js +9 -8
- package/dist/core/notifications.js.map +1 -1
- package/dist/core/reconnect.d.ts +16 -5
- package/dist/core/reconnect.d.ts.map +1 -1
- package/dist/core/reconnect.js +32 -4
- package/dist/core/reconnect.js.map +1 -1
- package/dist/core/types.d.ts +15 -16
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/query/client.d.ts +4 -0
- package/dist/query/client.d.ts.map +1 -1
- package/dist/query/client.js +145 -23
- package/dist/query/client.js.map +1 -1
- package/dist/query/errors.d.ts +20 -0
- package/dist/query/errors.d.ts.map +1 -1
- package/dist/query/errors.js +31 -0
- package/dist/query/errors.js.map +1 -1
- package/dist/query/index.d.ts +1 -1
- package/dist/query/index.d.ts.map +1 -1
- package/dist/query/index.js +1 -1
- package/dist/query/index.js.map +1 -1
- package/dist/query/lines.d.ts +11 -0
- package/dist/query/lines.d.ts.map +1 -1
- package/dist/query/lines.js +36 -6
- package/dist/query/lines.js.map +1 -1
- package/dist/query/options.d.ts +30 -0
- package/dist/query/options.d.ts.map +1 -1
- package/dist/query/options.js +7 -0
- package/dist/query/options.js.map +1 -1
- package/dist/query/reconnect.d.ts +5 -0
- package/dist/query/reconnect.d.ts.map +1 -1
- package/dist/query/reconnect.js +26 -7
- package/dist/query/reconnect.js.map +1 -1
- package/dist/query/ssh.js +9 -4
- package/dist/query/ssh.js.map +1 -1
- package/dist/query/stream.d.ts +2 -0
- package/dist/query/stream.d.ts.map +1 -1
- package/dist/query/stream.js +19 -1
- package/dist/query/stream.js.map +1 -1
- package/dist/query/transport.d.ts +12 -2
- package/dist/query/transport.d.ts.map +1 -1
- package/dist/query/web.d.ts +3 -1
- package/dist/query/web.d.ts.map +1 -1
- package/dist/query/web.js +82 -18
- package/dist/query/web.js.map +1 -1
- package/dist/stream/published.d.ts.map +1 -1
- package/dist/stream/published.js +61 -25
- package/dist/stream/published.js.map +1 -1
- package/dist/stream/streams.d.ts.map +1 -1
- package/dist/stream/streams.js +51 -10
- package/dist/stream/streams.js.map +1 -1
- package/dist/stream/wire.d.ts +2 -2
- package/dist/stream/wire.d.ts.map +1 -1
- package/dist/stream/wire.js +27 -7
- package/dist/stream/wire.js.map +1 -1
- package/package.json +1 -1
- package/src/client/api/avatars.ts +15 -2
- package/src/client/api/files.ts +83 -23
- package/src/client/api/upload-checks.ts +96 -0
- package/src/client/api/voice.ts +5 -2
- package/src/client/client.ts +154 -42
- package/src/client/crypto/crypt.ts +15 -7
- package/src/client/crypto/eax.ts +27 -22
- package/src/client/errors.ts +25 -0
- package/src/client/handshake/crypt-handshake.ts +6 -3
- package/src/client/index.ts +1 -0
- package/src/client/throttle.ts +41 -3
- package/src/client/transfer.ts +172 -28
- package/src/client/transport/handler.ts +54 -46
- package/src/core/api/clients.ts +24 -21
- package/src/core/api/command.ts +9 -7
- package/src/core/api/files.ts +30 -16
- package/src/core/api/groups.ts +32 -13
- package/src/core/api/parse.ts +16 -15
- package/src/core/command/request.ts +18 -0
- package/src/core/errors.ts +74 -20
- package/src/core/events.ts +24 -8
- package/src/core/internal.ts +7 -1
- package/src/core/notifications.ts +9 -8
- package/src/core/reconnect.ts +60 -8
- package/src/core/types.ts +15 -16
- package/src/query/client.ts +144 -20
- package/src/query/errors.ts +34 -0
- package/src/query/index.ts +1 -0
- package/src/query/lines.ts +38 -6
- package/src/query/options.ts +36 -0
- package/src/query/reconnect.ts +27 -7
- package/src/query/ssh.ts +11 -4
- package/src/query/stream.ts +17 -1
- package/src/query/testing/live.ts +4 -4
- package/src/query/transport.ts +10 -2
- package/src/query/web.ts +76 -21
- package/src/stream/published.ts +70 -23
- package/src/stream/streams.ts +47 -13
- package/src/stream/wire.ts +30 -11
package/src/client/transfer.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { connect } from "node:net";
|
|
10
10
|
import { pipeline } from "node:stream/promises";
|
|
11
|
-
import { Transform, type Duplex, type Readable
|
|
11
|
+
import { Transform, Writable, type Duplex, type Readable } from "node:stream";
|
|
12
12
|
import type { FileUploadInfo, FileDownloadInfo } from "@honeybbq/teamspeak";
|
|
13
13
|
import type { FileTransferSocketFactory } from "./types.js";
|
|
14
14
|
import { FileTransferError } from "./errors.js";
|
|
@@ -25,6 +25,25 @@ const DEFAULT_IDLE_TIMEOUT_MS = 30_000;
|
|
|
25
25
|
/** How many bytes move between two calls of the `progress` hook, at least. */
|
|
26
26
|
const PROGRESS_STEP_BYTES = 64 * 1024;
|
|
27
27
|
|
|
28
|
+
/** The file transfer ports a connection can be made to. */
|
|
29
|
+
const MAX_PORT = 65535;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The errors of dials that failed before the connection opened. Nothing has
|
|
33
|
+
* reached the server then: not the key, and no byte of the file.
|
|
34
|
+
*/
|
|
35
|
+
const failedBeforeConnect = new WeakSet();
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Whether `error` is a dial that never got a connection: refused, unreachable,
|
|
39
|
+
* or timed out while connecting. Not one the caller's signal or the idle limit
|
|
40
|
+
* ended, nor one after the connection opened, where the key may have been
|
|
41
|
+
* written and the server may hold or have dropped the transfer.
|
|
42
|
+
*/
|
|
43
|
+
export function failedToConnect(error: unknown): boolean {
|
|
44
|
+
return typeof error === "object" && error !== null && failedBeforeConnect.has(error);
|
|
45
|
+
}
|
|
46
|
+
|
|
28
47
|
/** Where a failed transfer failed. */
|
|
29
48
|
export type FileTransferStage = "dial" | "transfer";
|
|
30
49
|
|
|
@@ -48,7 +67,7 @@ export interface FileTransferDialDoneInfo {
|
|
|
48
67
|
export interface FileTransferProgressInfo {
|
|
49
68
|
/** The bytes moved so far. */
|
|
50
69
|
readonly bytes: number;
|
|
51
|
-
/** The
|
|
70
|
+
/** The bytes the transfer will move, as the negotiation fixed them. */
|
|
52
71
|
readonly total: number;
|
|
53
72
|
}
|
|
54
73
|
|
|
@@ -218,6 +237,9 @@ async function dial(
|
|
|
218
237
|
idle: IdleGuard,
|
|
219
238
|
): Promise<Duplex> {
|
|
220
239
|
const { signal, socketFactory } = options;
|
|
240
|
+
if (!Number.isInteger(port) || port < 1 || port > MAX_PORT) {
|
|
241
|
+
throw new FileTransferError(`the file transfer port is ${String(port)}, not 1 to ${MAX_PORT}`);
|
|
242
|
+
}
|
|
221
243
|
// One budget for the connection and the key write: a peer that accepts the
|
|
222
244
|
// connection and never reads would otherwise hold the write forever.
|
|
223
245
|
const timeout = AbortSignal.timeout(DIAL_TIMEOUT_MS);
|
|
@@ -233,8 +255,10 @@ async function dial(
|
|
|
233
255
|
// report: a stream's unhandled `error` event takes the process down.
|
|
234
256
|
const swallow = (): void => {};
|
|
235
257
|
socket.on("error", swallow);
|
|
258
|
+
let open = false;
|
|
236
259
|
try {
|
|
237
260
|
await abortable(connected(socket), dialSignal);
|
|
261
|
+
open = true;
|
|
238
262
|
idle.touch();
|
|
239
263
|
await abortable(
|
|
240
264
|
new Promise<void>((resolve, reject) => {
|
|
@@ -254,7 +278,11 @@ async function dial(
|
|
|
254
278
|
} catch (error) {
|
|
255
279
|
socket.destroy();
|
|
256
280
|
if (timeout.aborted && error === timeout.reason) {
|
|
257
|
-
|
|
281
|
+
const timedOut = new FileTransferError("file transfer connection timed out", {
|
|
282
|
+
cause: error,
|
|
283
|
+
});
|
|
284
|
+
if (!open) failedBeforeConnect.add(timedOut);
|
|
285
|
+
throw timedOut;
|
|
258
286
|
}
|
|
259
287
|
throw error;
|
|
260
288
|
} finally {
|
|
@@ -293,9 +321,10 @@ function countingStream(t: TransferTrace, total: number, idle: IdleGuard): Trans
|
|
|
293
321
|
|
|
294
322
|
/**
|
|
295
323
|
* Passes exactly `expected` bytes. One more byte fails the transfer, and is
|
|
296
|
-
* not passed on; a stream that ends before `expected` fails it too.
|
|
324
|
+
* not passed on; a stream that ends before `expected` fails it too. `what`
|
|
325
|
+
* says who fixed the size, for the message.
|
|
297
326
|
*/
|
|
298
|
-
function exactSize(expected: number): Transform {
|
|
327
|
+
function exactSize(expected: number, what: "announced" | "declared"): Transform {
|
|
299
328
|
let received = 0;
|
|
300
329
|
return new Transform({
|
|
301
330
|
transform(chunk: Buffer, _encoding, callback) {
|
|
@@ -304,7 +333,7 @@ function exactSize(expected: number): Transform {
|
|
|
304
333
|
this.push(chunk.subarray(0, room));
|
|
305
334
|
received = expected;
|
|
306
335
|
callback(
|
|
307
|
-
new FileTransferError(`file transfer sent more than the
|
|
336
|
+
new FileTransferError(`file transfer sent more than the ${what} ${expected} bytes`),
|
|
308
337
|
);
|
|
309
338
|
return;
|
|
310
339
|
}
|
|
@@ -316,7 +345,7 @@ function exactSize(expected: number): Transform {
|
|
|
316
345
|
received === expected
|
|
317
346
|
? null
|
|
318
347
|
: new FileTransferError(
|
|
319
|
-
`file transfer ended after ${received} of the
|
|
348
|
+
`file transfer ended after ${received} of the ${what} ${expected} bytes`,
|
|
320
349
|
),
|
|
321
350
|
);
|
|
322
351
|
},
|
|
@@ -330,11 +359,7 @@ async function transfer(
|
|
|
330
359
|
key: string,
|
|
331
360
|
total: number,
|
|
332
361
|
options: FileTransferOptions,
|
|
333
|
-
move: (
|
|
334
|
-
socket: Duplex,
|
|
335
|
-
counter: Transform,
|
|
336
|
-
pipeOptions: { signal?: AbortSignal },
|
|
337
|
-
) => Promise<void>,
|
|
362
|
+
move: (socket: Duplex, counter: Transform, pipeOptions: { signal: AbortSignal }) => Promise<void>,
|
|
338
363
|
): Promise<void> {
|
|
339
364
|
const t = newTransferTrace(options);
|
|
340
365
|
const idle = newIdleGuard(options);
|
|
@@ -356,6 +381,10 @@ async function transfer(
|
|
|
356
381
|
const failure = idleOr(idle, error);
|
|
357
382
|
reportFailed(t, failure, "transfer");
|
|
358
383
|
throw failure;
|
|
384
|
+
} finally {
|
|
385
|
+
// The server ends a transfer it has finished, and a failed one is
|
|
386
|
+
// abandoned here; either way nothing is left to keep the socket for.
|
|
387
|
+
socket.destroy();
|
|
359
388
|
}
|
|
360
389
|
} finally {
|
|
361
390
|
idle.stop();
|
|
@@ -386,50 +415,165 @@ function connected(socket: Duplex): Promise<void> {
|
|
|
386
415
|
else reject(error);
|
|
387
416
|
};
|
|
388
417
|
const onConnect = (): void => done();
|
|
389
|
-
const onError = (error: Error): void =>
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
);
|
|
418
|
+
const onError = (error: Error): void => {
|
|
419
|
+
const failure = new FileTransferError(`file transfer connection failed: ${error.message}`, {
|
|
420
|
+
cause: error,
|
|
421
|
+
});
|
|
422
|
+
failedBeforeConnect.add(failure);
|
|
423
|
+
done(failure);
|
|
424
|
+
};
|
|
395
425
|
socket.once("connect", onConnect);
|
|
396
426
|
socket.once("ready", onConnect);
|
|
397
427
|
socket.once("error", onError);
|
|
398
428
|
});
|
|
399
429
|
}
|
|
400
430
|
|
|
401
|
-
/**
|
|
402
|
-
|
|
431
|
+
/**
|
|
432
|
+
* How many bytes a negotiated transfer moves: the file's size less the
|
|
433
|
+
* position it starts from.
|
|
434
|
+
*/
|
|
435
|
+
function bytesToMove(size: bigint, seekPosition: bigint): number {
|
|
436
|
+
if (seekPosition < 0n || seekPosition > size) {
|
|
437
|
+
throw new FileTransferError(
|
|
438
|
+
`the transfer starts at ${seekPosition}, outside a file of ${size} bytes`,
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
return Number(size - seekPosition);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* A sink that hands what it is given to `dest`, one write at a time, without
|
|
446
|
+
* ever ending it: the stream is the caller's, and may take more transfers.
|
|
447
|
+
* `dest`'s errors fail the sink, so the pipeline sees them.
|
|
448
|
+
*/
|
|
449
|
+
function intoStream(dest: Writable): Writable {
|
|
450
|
+
const sink = new Writable({
|
|
451
|
+
write(chunk: Buffer, _encoding, callback) {
|
|
452
|
+
dest.write(chunk, callback);
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
const onError = (error: Error): void => {
|
|
456
|
+
sink.destroy(error);
|
|
457
|
+
};
|
|
458
|
+
dest.on("error", onError);
|
|
459
|
+
// A failed `dest` reports its error a tick after the write that failed, by
|
|
460
|
+
// which time this sink is closed: keep listening until that has been said.
|
|
461
|
+
sink.on("close", () => setImmediate(() => dest.off("error", onError)));
|
|
462
|
+
return sink;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* The same for a socket that is not to be ended: an upload is over when the
|
|
467
|
+
* server says so, not when the last byte has left.
|
|
468
|
+
*/
|
|
469
|
+
function intoSocket(socket: Duplex): Writable {
|
|
470
|
+
// A write after the server closed the connection fails with the socket's own
|
|
471
|
+
// error (EPIPE, or "ended by the other party"): the transfer's failure.
|
|
472
|
+
const failure = (error: Error): FileTransferError =>
|
|
473
|
+
new FileTransferError(`file transfer connection failed: ${error.message}`, {
|
|
474
|
+
cause: error,
|
|
475
|
+
});
|
|
476
|
+
const sink = new Writable({
|
|
477
|
+
write(chunk: Buffer, _encoding, callback) {
|
|
478
|
+
socket.write(chunk, (error) => callback(error ? failure(error) : null));
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
socket.on("error", (error: Error) => {
|
|
482
|
+
sink.destroy(failure(error));
|
|
483
|
+
});
|
|
484
|
+
return sink;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/** Resolves once the server has closed the transfer connection. */
|
|
488
|
+
function serverClosed(socket: Duplex, signal: AbortSignal): Promise<void> {
|
|
489
|
+
if (socket.readableEnded || socket.destroyed) return Promise.resolve();
|
|
490
|
+
const closed = new Promise<void>((resolve, reject) => {
|
|
491
|
+
const done = (error?: Error): void => {
|
|
492
|
+
socket.off("end", onEnd);
|
|
493
|
+
socket.off("close", onEnd);
|
|
494
|
+
socket.off("error", done);
|
|
495
|
+
if (error === undefined) resolve();
|
|
496
|
+
else reject(error);
|
|
497
|
+
};
|
|
498
|
+
const onEnd = (): void => done();
|
|
499
|
+
socket.once("end", onEnd);
|
|
500
|
+
socket.once("close", onEnd);
|
|
501
|
+
socket.once("error", done);
|
|
502
|
+
});
|
|
503
|
+
// Nothing is sent back, but the end of the stream is only seen by reading.
|
|
504
|
+
socket.resume();
|
|
505
|
+
return abortable(closed, signal);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Upload `data` over a negotiated transfer. `data` is the file from
|
|
510
|
+
* `info.seekPosition` on, and must give exactly the bytes left of
|
|
511
|
+
* `info.size`: a stream that gives fewer or more fails the transfer.
|
|
512
|
+
*
|
|
513
|
+
* Resolves once the server has closed the transfer connection, which it does
|
|
514
|
+
* when it has the file. Measured on TeamSpeak 3 3.13.8 and TeamSpeak 6 6.0.0:
|
|
515
|
+
* the server closes about 200 ms after the last byte of a 100 000-byte file,
|
|
516
|
+
* and at once after the declared size when more is sent. A connection that
|
|
517
|
+
* closes before every byte is written, or that stays open for the idle limit,
|
|
518
|
+
* fails the transfer.
|
|
519
|
+
*
|
|
520
|
+
* A close after the last byte is not proof that the file is stored: the server
|
|
521
|
+
* sends nothing on the transfer connection, and over the voice protocol its
|
|
522
|
+
* `notifystatusfiletransfer` comes only for a failure (2066 canceled, 2063
|
|
523
|
+
* lost connection, 2052 more than declared), after the close, never for
|
|
524
|
+
* success; over ServerQuery nothing comes. `uploadFile` therefore asks the
|
|
525
|
+
* server for the file's size with `ftgetfileinfo` before it resolves.
|
|
526
|
+
*/
|
|
527
|
+
export async function uploadFileData(
|
|
403
528
|
host: string,
|
|
404
529
|
info: FileUploadInfo,
|
|
405
530
|
data: Readable,
|
|
406
531
|
options: FileTransferOptions = {},
|
|
407
532
|
): Promise<void> {
|
|
408
|
-
|
|
533
|
+
const expected = bytesToMove(info.size, info.seekPosition);
|
|
534
|
+
await transfer(
|
|
409
535
|
host,
|
|
410
536
|
info.port,
|
|
411
537
|
info.fileTransferKey,
|
|
412
|
-
|
|
538
|
+
expected,
|
|
413
539
|
options,
|
|
414
|
-
(socket, counter, pipeOptions) =>
|
|
540
|
+
async (socket, counter, pipeOptions) => {
|
|
541
|
+
await pipeline(
|
|
542
|
+
data,
|
|
543
|
+
exactSize(expected, "declared"),
|
|
544
|
+
counter,
|
|
545
|
+
intoSocket(socket),
|
|
546
|
+
pipeOptions,
|
|
547
|
+
);
|
|
548
|
+
await serverClosed(socket, pipeOptions.signal);
|
|
549
|
+
},
|
|
415
550
|
);
|
|
416
551
|
}
|
|
417
552
|
|
|
418
|
-
/**
|
|
419
|
-
|
|
553
|
+
/**
|
|
554
|
+
* Download a negotiated transfer into `dest`. Resolves once the file is
|
|
555
|
+
* written; `dest` is not ended, so it may take another download.
|
|
556
|
+
*
|
|
557
|
+
* A download from `info.seekPosition` brings the bytes from there to the end
|
|
558
|
+
* of the file: measured on TeamSpeak 3 3.13.8 and TeamSpeak 6 6.0.0, a
|
|
559
|
+
* `seekpos` of 40 000 on a 100 000-byte file is answered with the whole size
|
|
560
|
+
* and then 60 000 bytes.
|
|
561
|
+
*/
|
|
562
|
+
export async function downloadFileData(
|
|
420
563
|
host: string,
|
|
421
564
|
info: FileDownloadInfo,
|
|
422
565
|
dest: Writable,
|
|
423
566
|
options: FileTransferOptions = {},
|
|
424
567
|
): Promise<void> {
|
|
425
|
-
|
|
568
|
+
const expected = bytesToMove(info.size, info.seekPosition);
|
|
569
|
+
await transfer(
|
|
426
570
|
host,
|
|
427
571
|
info.port,
|
|
428
572
|
info.fileTransferKey,
|
|
429
|
-
|
|
573
|
+
expected,
|
|
430
574
|
options,
|
|
431
575
|
(socket, counter, pipeOptions) =>
|
|
432
|
-
pipeline(socket, exactSize(
|
|
576
|
+
pipeline(socket, exactSize(expected, "announced"), counter, intoStream(dest), pipeOptions),
|
|
433
577
|
);
|
|
434
578
|
}
|
|
435
579
|
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
buildC2SHeader,
|
|
15
15
|
parseS2CHeader,
|
|
16
16
|
} from "./packet.js";
|
|
17
|
-
import { ConnectionTimeoutError } from "../errors.js";
|
|
17
|
+
import { ConnectionStalledError, ConnectionTimeoutError } from "../errors.js";
|
|
18
18
|
import { abortable, abortReason } from "../abort.js";
|
|
19
19
|
import { NotConnectedError, TeamspeakError } from "@honeybbq/teamspeak";
|
|
20
20
|
import { GenerationWindow } from "./generation-window.js";
|
|
@@ -40,6 +40,12 @@ const WHISPER_HEADER_SIZE = 5;
|
|
|
40
40
|
const GROUP_WHISPER_HEADER_SIZE = 13;
|
|
41
41
|
const RESEND_BASE_INTERVAL_MS = 500;
|
|
42
42
|
const RESEND_LOOP_INTERVAL_MS = 100;
|
|
43
|
+
/**
|
|
44
|
+
* How long a command packet may stay missing while later ones wait for it. The
|
|
45
|
+
* server sends a command again until it is acknowledged, so a hole that stays
|
|
46
|
+
* this long is not going to fill.
|
|
47
|
+
*/
|
|
48
|
+
const COMMAND_HOLE_TIMEOUT_MS = 5_000;
|
|
43
49
|
|
|
44
50
|
/**
|
|
45
51
|
* The largest command reassembled from fragments. The receive window already
|
|
@@ -88,6 +94,12 @@ interface ResendPacket {
|
|
|
88
94
|
lastSend: number;
|
|
89
95
|
retryCount: number;
|
|
90
96
|
nextInterval: number;
|
|
97
|
+
/**
|
|
98
|
+
* Whether the packet went out under the dummy key. Fixed at the first send:
|
|
99
|
+
* clientek is sent under it and crypto init completes right after, so a
|
|
100
|
+
* resend that chose again would use a key the server cannot decrypt with.
|
|
101
|
+
*/
|
|
102
|
+
dummy: boolean;
|
|
91
103
|
}
|
|
92
104
|
|
|
93
105
|
export class PacketHandler {
|
|
@@ -542,6 +554,7 @@ export class PacketHandler {
|
|
|
542
554
|
lastSend: performance.now(),
|
|
543
555
|
retryCount: 0,
|
|
544
556
|
nextInterval: RESEND_BASE_INTERVAL_MS,
|
|
557
|
+
dummy,
|
|
545
558
|
};
|
|
546
559
|
this.#trackResend(pType, p, rp);
|
|
547
560
|
}
|
|
@@ -582,7 +595,6 @@ export class PacketHandler {
|
|
|
582
595
|
const ciphertext = raw.slice(TAG_SIZE + 3);
|
|
583
596
|
|
|
584
597
|
const parsed = parseS2CHeader(header);
|
|
585
|
-
this.#traffic.recordReceived(parsed.typeFlagged & 0x0f, raw.length, performance.now());
|
|
586
598
|
// Init1 belongs to the start of the handshake; once crypto init has
|
|
587
599
|
// completed, another one can only restart a session that is already set up.
|
|
588
600
|
const parsedType: PacketType = parsed.typeFlagged & 0x0f;
|
|
@@ -604,6 +616,7 @@ export class PacketHandler {
|
|
|
604
616
|
// Only a packet that decrypted shows the server is there: anyone can send
|
|
605
617
|
// datagrams that do not, and they must not keep a dead session alive.
|
|
606
618
|
this.#lastMessageReceived = performance.now();
|
|
619
|
+
this.#traffic.recordReceived(pType, raw.length, p.receivedAt);
|
|
607
620
|
|
|
608
621
|
if (pType === PacketType.Pong && p.data.length >= 2) {
|
|
609
622
|
const acked = new DataView(p.data.buffer, p.data.byteOffset).getUint16(0, false);
|
|
@@ -747,9 +760,12 @@ export class PacketHandler {
|
|
|
747
760
|
}
|
|
748
761
|
const queue = pType === PacketType.Command ? this.#commandQueue : this.#commandLowQueue;
|
|
749
762
|
const held = queue.get(p.id)?.data.length ?? 0;
|
|
750
|
-
// The packet the queue
|
|
751
|
-
|
|
752
|
-
|
|
763
|
+
// The packet the queue needs next, the first id from the next expected one
|
|
764
|
+
// that it does not hold, is always taken: it is what drains it, and what
|
|
765
|
+
// lets a command larger than the queue reach its last fragment.
|
|
766
|
+
let needed = pType === PacketType.Command ? this.#nextCommandID : this.#nextCommandLowID;
|
|
767
|
+
for (let i = 0; i < queue.size && queue.has(needed); i++) needed = (needed + 1) & 0xffff;
|
|
768
|
+
if (p.id !== needed && this.#queuedBytes - held + p.data.length > MAX_QUEUED_BYTES) {
|
|
753
769
|
// Not acknowledged: the server sends it again.
|
|
754
770
|
this.#logger.debug("dropped a command packet: the receive queues are full", { id: p.id });
|
|
755
771
|
return false;
|
|
@@ -783,12 +799,6 @@ export class PacketHandler {
|
|
|
783
799
|
|
|
784
800
|
this.#hold(queue, p);
|
|
785
801
|
|
|
786
|
-
if (isCommand) {
|
|
787
|
-
this.#fastForwardMissingPackets(queue, win, true);
|
|
788
|
-
} else {
|
|
789
|
-
this.#fastForwardMissingPackets(queue, win, false);
|
|
790
|
-
}
|
|
791
|
-
|
|
792
802
|
while (true) {
|
|
793
803
|
const current = isCommand ? this.#nextCommandID : this.#nextCommandLowID;
|
|
794
804
|
const packet = queue.get(current);
|
|
@@ -827,18 +837,17 @@ export class PacketHandler {
|
|
|
827
837
|
#nextCommandID = 0;
|
|
828
838
|
#nextCommandLowID = 0;
|
|
829
839
|
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
):
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
840
|
+
/**
|
|
841
|
+
* Whether a command packet is missing from `queue` while a later one has
|
|
842
|
+
* waited for it for COMMAND_HOLE_TIMEOUT_MS.
|
|
843
|
+
*/
|
|
844
|
+
#hasStalledHole(queue: Map<number, Packet>, nextID: number, now: number): boolean {
|
|
845
|
+
if (queue.has(nextID)) return false;
|
|
846
|
+
for (const [id, pkg] of queue) {
|
|
847
|
+
const diff = (id - nextID + 0x10000) & 0xffff;
|
|
848
|
+
if (diff < 0x8000 && now - pkg.receivedAt > COMMAND_HOLE_TIMEOUT_MS) return true;
|
|
839
849
|
}
|
|
840
|
-
|
|
841
|
-
else this.#nextCommandLowID = nextID;
|
|
850
|
+
return false;
|
|
842
851
|
}
|
|
843
852
|
|
|
844
853
|
#tryReassemble(
|
|
@@ -859,22 +868,14 @@ export class PacketHandler {
|
|
|
859
868
|
let startSeen = false;
|
|
860
869
|
|
|
861
870
|
// Ends at the fragment that closes the command, or returns while one is
|
|
862
|
-
// still missing.
|
|
871
|
+
// still missing. The size is checked once the whole command is there, so an
|
|
872
|
+
// oversize one is released to its end marker and never leaves the stream
|
|
873
|
+
// part-way through a command.
|
|
863
874
|
while (true) {
|
|
864
875
|
const frag = queue.get(currID);
|
|
865
876
|
if (!frag) return null;
|
|
866
877
|
fragments.push(frag);
|
|
867
878
|
totalSize += frag.data.length;
|
|
868
|
-
if (totalSize > MAX_REASSEMBLED_SIZE) {
|
|
869
|
-
// Dropped whole, so the window moves past it rather than waiting on
|
|
870
|
-
// the rest of a command that will never be accepted.
|
|
871
|
-
for (let i = 0; i < fragments.length; i++) {
|
|
872
|
-
this.#release(queue, nextID);
|
|
873
|
-
win.advance(1);
|
|
874
|
-
nextID = (nextID + 1) & 0xffff;
|
|
875
|
-
}
|
|
876
|
-
return [null, nextID];
|
|
877
|
-
}
|
|
878
879
|
|
|
879
880
|
const fragmented = (packetFlags(frag) & PacketFlags.Fragmented) !== 0;
|
|
880
881
|
if (!startSeen) {
|
|
@@ -886,6 +887,16 @@ export class PacketHandler {
|
|
|
886
887
|
currID = (currID + 1) & 0xffff;
|
|
887
888
|
}
|
|
888
889
|
|
|
890
|
+
if (totalSize > MAX_REASSEMBLED_SIZE) {
|
|
891
|
+
// Dropped whole, so the window moves past it.
|
|
892
|
+
for (let i = 0; i < fragments.length; i++) {
|
|
893
|
+
this.#release(queue, nextID);
|
|
894
|
+
win.advance(1);
|
|
895
|
+
nextID = (nextID + 1) & 0xffff;
|
|
896
|
+
}
|
|
897
|
+
return [null, nextID];
|
|
898
|
+
}
|
|
899
|
+
|
|
889
900
|
const combined = new Uint8Array(totalSize);
|
|
890
901
|
let pos = 0;
|
|
891
902
|
for (const frag of fragments) {
|
|
@@ -971,6 +982,15 @@ export class PacketHandler {
|
|
|
971
982
|
return;
|
|
972
983
|
}
|
|
973
984
|
|
|
985
|
+
if (
|
|
986
|
+
this.#hasStalledHole(this.#commandQueue, this.#nextCommandID, now) ||
|
|
987
|
+
this.#hasStalledHole(this.#commandLowQueue, this.#nextCommandLowID, now)
|
|
988
|
+
) {
|
|
989
|
+
this.#logger.warn("a command from the server never arrived");
|
|
990
|
+
this.#triggerClose(new ConnectionStalledError());
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
|
|
974
994
|
if (this.#initPacketCheck) {
|
|
975
995
|
this.#doResend(this.#initPacketCheck, now);
|
|
976
996
|
}
|
|
@@ -993,7 +1013,6 @@ export class PacketHandler {
|
|
|
993
1013
|
this.#resends++;
|
|
994
1014
|
rp.nextInterval = Math.min(rp.nextInterval * 2, MAX_RETRY_INTERVAL_MS);
|
|
995
1015
|
|
|
996
|
-
const dummy = !this.#crypt.cryptoInitComplete;
|
|
997
1016
|
const unencrypted = (packetFlags(rp.packet) & PacketFlags.Unencrypted) !== 0;
|
|
998
1017
|
const header = buildC2SHeader(rp.packet);
|
|
999
1018
|
const pType = packetType(rp.packet);
|
|
@@ -1004,7 +1023,7 @@ export class PacketHandler {
|
|
|
1004
1023
|
rp.packet.generationID,
|
|
1005
1024
|
header,
|
|
1006
1025
|
rp.packet.data,
|
|
1007
|
-
dummy,
|
|
1026
|
+
rp.dummy,
|
|
1008
1027
|
unencrypted,
|
|
1009
1028
|
);
|
|
1010
1029
|
|
|
@@ -1039,14 +1058,3 @@ function applyProtocolFlags(pType: PacketType, flags: number): number {
|
|
|
1039
1058
|
}
|
|
1040
1059
|
return flags;
|
|
1041
1060
|
}
|
|
1042
|
-
|
|
1043
|
-
function hasOldNewerPacket(queue: Map<number, Packet>, nextID: number): boolean {
|
|
1044
|
-
const now = performance.now();
|
|
1045
|
-
for (const [id, pkg] of queue) {
|
|
1046
|
-
const diff = (id - nextID + 0x10000) & 0xffff;
|
|
1047
|
-
if (diff < 0x8000 && now - pkg.receivedAt > 5_000) {
|
|
1048
|
-
return true;
|
|
1049
|
-
}
|
|
1050
|
-
}
|
|
1051
|
-
return false;
|
|
1052
|
-
}
|
package/src/core/api/clients.ts
CHANGED
|
@@ -58,43 +58,46 @@ export interface ClientEntry {
|
|
|
58
58
|
readonly id: ClientID;
|
|
59
59
|
/** The client's nickname. */
|
|
60
60
|
readonly nickname: string;
|
|
61
|
-
/**
|
|
62
|
-
readonly uid: UID;
|
|
63
|
-
/** The channel the client is in. */
|
|
64
|
-
readonly channelID: ChannelID;
|
|
65
|
-
/** What kind of connection this is, one of {@link ClientType}. */
|
|
61
|
+
/** What kind of connection this is, one of {@link ClientType}; every source carries it. */
|
|
66
62
|
readonly type: number;
|
|
63
|
+
/** The channel the client is in; every source carries it. */
|
|
64
|
+
readonly channelID: ChannelID;
|
|
65
|
+
/** The client's database id (`client_database_id`); every list and every enter notification carries it. */
|
|
66
|
+
readonly databaseID: ClientDBID;
|
|
67
|
+
|
|
68
|
+
// Everything below is `undefined` when the row did not carry its key, never
|
|
69
|
+
// a default: a client that is not away and one whose away state was not asked
|
|
70
|
+
// for are different, and `false` would claim the first. `clientlist` carries
|
|
71
|
+
// only what its flags bring, and a client coming into view carries all but
|
|
72
|
+
// the fields said to be missing there. Which list flag brings a field is said
|
|
73
|
+
// on it; "no flag" means only a client coming into view (or a notification)
|
|
74
|
+
// has it, so a client learned from `listClients` never does.
|
|
75
|
+
|
|
76
|
+
/** The client's unique identifier, stable across connections, with `uid`. Empty is a client that has none. */
|
|
77
|
+
readonly uid?: UID | undefined;
|
|
67
78
|
/**
|
|
68
79
|
* The server groups the client holds, with `groups`. Ids rather than names:
|
|
69
80
|
* the server sends ids, and resolving them costs a separate call.
|
|
70
81
|
*/
|
|
71
|
-
readonly serverGroups
|
|
82
|
+
readonly serverGroups?: readonly ServerGroupID[] | undefined;
|
|
72
83
|
/** Whether the client is away, with `away`. */
|
|
73
|
-
readonly isAway
|
|
84
|
+
readonly isAway?: boolean | undefined;
|
|
74
85
|
/** Whether the client is recording, with `voice`. */
|
|
75
|
-
readonly isRecording
|
|
86
|
+
readonly isRecording?: boolean | undefined;
|
|
76
87
|
/** Whether the client may talk despite the channel's needed talk power, with `voice`. */
|
|
77
|
-
readonly isTalker
|
|
88
|
+
readonly isTalker?: boolean | undefined;
|
|
78
89
|
/** The channel group the client holds in its channel, with `groups`. */
|
|
79
|
-
readonly channelGroupID
|
|
90
|
+
readonly channelGroupID?: ChannelGroupID | undefined;
|
|
80
91
|
/** When the client first connected to this server, with `times`. */
|
|
81
|
-
readonly createdAt
|
|
92
|
+
readonly createdAt?: Date | undefined;
|
|
82
93
|
/** When the client last connected before this session, with `times`. */
|
|
83
|
-
readonly lastConnectedAt
|
|
94
|
+
readonly lastConnectedAt?: Date | undefined;
|
|
84
95
|
/**
|
|
85
96
|
* How long since the client last did anything, in milliseconds
|
|
86
97
|
* (`client_idle_time`), with `times`; undefined without it.
|
|
87
98
|
*/
|
|
88
|
-
readonly idleTimeMs
|
|
89
|
-
|
|
90
|
-
// Everything below is `undefined` when the row did not carry its key, never
|
|
91
|
-
// a default: a muted client and one whose mute state is unknown are
|
|
92
|
-
// different, and `false` would claim the first. Which list flag brings a
|
|
93
|
-
// field is said on it; "no flag" means only a client coming into view (or a
|
|
94
|
-
// notification) has it, so a client learned from `listClients` never does.
|
|
99
|
+
readonly idleTimeMs?: number | undefined;
|
|
95
100
|
|
|
96
|
-
/** The client's database id (`client_database_id`); every list brings it. */
|
|
97
|
-
readonly databaseID?: ClientDBID | undefined;
|
|
98
101
|
/** The away message (`client_away_message`), with `away`. */
|
|
99
102
|
readonly awayMessage?: string | undefined;
|
|
100
103
|
/** Whether the microphone is muted (`client_input_muted`), with `voice`. */
|
package/src/core/api/command.ts
CHANGED
|
@@ -577,12 +577,6 @@ export interface Channel {
|
|
|
577
577
|
readonly name: string;
|
|
578
578
|
/** The topic shown under the name (`channel_topic`). */
|
|
579
579
|
readonly topic: string;
|
|
580
|
-
/**
|
|
581
|
-
* The description (`channel_description`). `channellist` does not carry it,
|
|
582
|
-
* so `listChannels` leaves it empty; `getChannelInfo` or the voice client's
|
|
583
|
-
* `getChannelDescription` has it.
|
|
584
|
-
*/
|
|
585
|
-
readonly description: string;
|
|
586
580
|
/**
|
|
587
581
|
* The channel this one is sorted after among its siblings, or 0 for the
|
|
588
582
|
* first: `channel_order` is a channel id, not a position.
|
|
@@ -645,6 +639,14 @@ export interface Channel {
|
|
|
645
639
|
// undefined; `getChannelInfo` fills it in, and a created or edited
|
|
646
640
|
// notification names the ones that changed.
|
|
647
641
|
|
|
642
|
+
/**
|
|
643
|
+
* The description (`channel_description`). Neither `channellist` nor a
|
|
644
|
+
* channel notification carries it, so `listChannels` and the voice client's
|
|
645
|
+
* view leave it undefined, and a channel's description changing is told by
|
|
646
|
+
* `channelDescriptionChanged` without the text; `getChannelInfo` has it, and
|
|
647
|
+
* so has the voice client's `getChannelDescription`.
|
|
648
|
+
*/
|
|
649
|
+
readonly description?: string | undefined;
|
|
648
650
|
/** How many packets of voice are sent at once, 1 to 10 (`channel_codec_latency_factor`). */
|
|
649
651
|
readonly codecLatencyFactor?: number | undefined;
|
|
650
652
|
/** Whether voice in the channel travels unencrypted (`channel_codec_is_unencrypted`). */
|
|
@@ -722,7 +724,7 @@ export const channelFields: {
|
|
|
722
724
|
},
|
|
723
725
|
name: { keys: ["channel_name"], read: str, always: true },
|
|
724
726
|
topic: { keys: ["channel_topic"], read: str, always: true },
|
|
725
|
-
description: { keys: ["channel_description"], read: str
|
|
727
|
+
description: { keys: ["channel_description"], read: str },
|
|
726
728
|
order: { keys: ["channel_order"], read: (row, key) => asChannelID(big(row, key)), always: true },
|
|
727
729
|
hasPassword: { keys: ["channel_flag_password"], read: bool, always: true },
|
|
728
730
|
isDefault: { keys: ["channel_flag_default"], read: bool, always: true },
|