@torrent-tv/proxy 2.100.11 → 2.100.13
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/bin/cli.js +2 -0
- package/package.json +1 -1
- package/services/transport/webrtc-manager.js +50 -23
package/bin/cli.js
CHANGED
|
@@ -18,6 +18,7 @@ import os from "node:os";
|
|
|
18
18
|
import { spawnSync } from "node:child_process";
|
|
19
19
|
import { createRequire } from "node:module";
|
|
20
20
|
import ffmpegStatic from "ffmpeg-static";
|
|
21
|
+
import nodeDataChannel from "node-datachannel";
|
|
21
22
|
import { startProxyServer } from "../server.js";
|
|
22
23
|
import { registerClient } from "../services/transport/registry-api.js";
|
|
23
24
|
import { createTunnelClient } from "../services/transport/tunnel-client.js";
|
|
@@ -689,6 +690,7 @@ try {
|
|
|
689
690
|
});
|
|
690
691
|
|
|
691
692
|
webRtcManager = createWebRtcManager({
|
|
693
|
+
rtc: nodeDataChannel,
|
|
692
694
|
// Single UDP port (UPnP-mapped above) shared by all sessions via a
|
|
693
695
|
// persistent ICE UDP mux listener, so the WebRTC path is reachable from the
|
|
694
696
|
// internet on one fixed port.
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @file WebRTC connection manager for the proxy (Node.js side).
|
|
3
3
|
*
|
|
4
|
-
* Maintains
|
|
4
|
+
* Maintains a media connection and an optional independent control connection
|
|
5
|
+
* per browser session. Control signals carry a generation's `connectionId`.
|
|
5
6
|
* Receives SDP offers and ICE candidates through the signalling tunnel
|
|
6
7
|
* and responds with SDP answers and its own candidates through the same channel.
|
|
7
8
|
*
|
|
@@ -9,8 +10,6 @@
|
|
|
9
10
|
* via the `onDataChannel` callback — this module only owns the signalling phase.
|
|
10
11
|
*/
|
|
11
12
|
|
|
12
|
-
import nodeDataChannel from "node-datachannel";
|
|
13
|
-
|
|
14
13
|
/** @import { WebRtcSignal } from './tunnel-client.js' */
|
|
15
14
|
|
|
16
15
|
// Two STUN servers from different operators, both with IPv6 (AAAA) records, so
|
|
@@ -148,6 +147,8 @@ function candidateAddrKind(candidate) {
|
|
|
148
147
|
* Configuration for the WebRTC manager.
|
|
149
148
|
*
|
|
150
149
|
* @typedef {Object} WebRtcManagerOptions
|
|
150
|
+
* @property {typeof import("node-datachannel").default} rtc
|
|
151
|
+
* The peer connection implementation, supplied by the composition root.
|
|
151
152
|
* @property {(sessionId: string, signal: WebRtcSignal) => void} sendSignal
|
|
152
153
|
* Send a WebRTC signal (answer or ICE candidate) back to the browser via
|
|
153
154
|
* the tunnel. Typically wired to `tunnelClient.sendSignal`.
|
|
@@ -196,9 +197,10 @@ function candidateAddrKind(candidate) {
|
|
|
196
197
|
* @param {WebRtcManagerOptions} options
|
|
197
198
|
* @returns {WebRtcManager}
|
|
198
199
|
*/
|
|
199
|
-
export function createWebRtcManager({ sendSignal, onDataChannel, onLog, udpPort, getNatInfo }) {
|
|
200
|
+
export function createWebRtcManager({ sendSignal, onDataChannel, onLog, udpPort, getNatInfo, rtc }) {
|
|
200
201
|
/** @type {Map<string, import("node-datachannel").PeerConnection>} */
|
|
201
202
|
const peers = new Map();
|
|
203
|
+
const controlPeers = new Map();
|
|
202
204
|
|
|
203
205
|
/**
|
|
204
206
|
* @param {string} message
|
|
@@ -222,7 +224,7 @@ export function createWebRtcManager({ sendSignal, onDataChannel, onLog, udpPort,
|
|
|
222
224
|
let udpMuxListener = null;
|
|
223
225
|
if (Number.isInteger(udpPort) && udpPort > 0 && udpPort <= 65535) {
|
|
224
226
|
try {
|
|
225
|
-
udpMuxListener = new
|
|
227
|
+
udpMuxListener = new rtc.IceUdpMuxListener(udpPort);
|
|
226
228
|
// STUN that doesn't match an existing session (our PeerConnections are
|
|
227
229
|
// created from the SDP offer before the browser's STUN arrives, so normal
|
|
228
230
|
// traffic is "handled"). Stray/unhandled requests are simply dropped.
|
|
@@ -249,13 +251,23 @@ export function createWebRtcManager({ sendSignal, onDataChannel, onLog, udpPort,
|
|
|
249
251
|
* @param {string} sessionId
|
|
250
252
|
* @returns {import("node-datachannel").PeerConnection}
|
|
251
253
|
*/
|
|
252
|
-
function getOrCreatePeer(sessionId) {
|
|
253
|
-
const
|
|
254
|
+
function getOrCreatePeer(sessionId, connectionId = null) {
|
|
255
|
+
const peerId = connectionId === null ? sessionId : `${sessionId}/${connectionId}`;
|
|
256
|
+
const existing = peers.get(peerId);
|
|
254
257
|
if (existing) {
|
|
255
258
|
return existing;
|
|
256
259
|
}
|
|
257
260
|
|
|
258
|
-
|
|
261
|
+
if (connectionId !== null) {
|
|
262
|
+
const previous = controlPeers.get(sessionId);
|
|
263
|
+
if (previous) closePeer(previous);
|
|
264
|
+
}
|
|
265
|
+
const pc = new rtc.PeerConnection(`proxy-${peerId}`, pcConfig);
|
|
266
|
+
if (connectionId !== null) controlPeers.set(sessionId, peerId);
|
|
267
|
+
const signalBrowser = (signal) => sendSignal(sessionId, {
|
|
268
|
+
...signal,
|
|
269
|
+
...(connectionId === null ? {} : { connectionId })
|
|
270
|
+
});
|
|
259
271
|
|
|
260
272
|
// Forward all ICE candidates to the browser through the tunnel.
|
|
261
273
|
//
|
|
@@ -272,7 +284,7 @@ export function createWebRtcManager({ sendSignal, onDataChannel, onLog, udpPort,
|
|
|
272
284
|
log(
|
|
273
285
|
`[webrtc] Session ${sessionId.slice(0, 8)}: sending ${kind} candidate: ${candidate.replace(/^a=/, "")}`
|
|
274
286
|
);
|
|
275
|
-
|
|
287
|
+
signalBrowser({ type: "candidate", candidate, mid });
|
|
276
288
|
|
|
277
289
|
// Symmetric-NAT port prediction: offer extra srflx candidates at the
|
|
278
290
|
// predicted external ports so the browser probes them too. No-op unless
|
|
@@ -287,7 +299,7 @@ export function createWebRtcManager({ sendSignal, onDataChannel, onLog, udpPort,
|
|
|
287
299
|
`[webrtc] Session ${sessionId.slice(0, 8)}: symmetric NAT (delta=${nat.portDelta}) — injecting ${predicted.length} predicted srflx candidates: ${predicted.map((p) => p.port).join(",")}`
|
|
288
300
|
);
|
|
289
301
|
for (const p of predicted) {
|
|
290
|
-
|
|
302
|
+
signalBrowser({ type: "candidate", candidate: p.candidate, mid });
|
|
291
303
|
}
|
|
292
304
|
}
|
|
293
305
|
}
|
|
@@ -304,7 +316,7 @@ export function createWebRtcManager({ sendSignal, onDataChannel, onLog, udpPort,
|
|
|
304
316
|
// Forward our SDP answer to the browser through the tunnel.
|
|
305
317
|
pc.onLocalDescription((sdp, type) => {
|
|
306
318
|
log(`[webrtc] Session ${sessionId.slice(0, 8)}: sending ${type}`);
|
|
307
|
-
|
|
319
|
+
signalBrowser({ type, sdp, ...(connectionId === null ? { separateControl: 2 } : {}) });
|
|
308
320
|
});
|
|
309
321
|
|
|
310
322
|
pc.onStateChange((state) => {
|
|
@@ -328,7 +340,7 @@ export function createWebRtcManager({ sendSignal, onDataChannel, onLog, udpPort,
|
|
|
328
340
|
// "disconnected" is a transient state — ICE may recover on its own.
|
|
329
341
|
// Only tear down on terminal states: "failed" and "closed".
|
|
330
342
|
if (state === "failed" || state === "closed") {
|
|
331
|
-
|
|
343
|
+
if (peers.get(peerId) === pc) closePeer(peerId);
|
|
332
344
|
}
|
|
333
345
|
});
|
|
334
346
|
|
|
@@ -340,11 +352,15 @@ export function createWebRtcManager({ sendSignal, onDataChannel, onLog, udpPort,
|
|
|
340
352
|
|
|
341
353
|
// Browser creates the data channel — we receive it here.
|
|
342
354
|
pc.onDataChannel((channel) => {
|
|
343
|
-
|
|
344
|
-
|
|
355
|
+
if (peers.get(peerId) !== pc || (connectionId !== null && channel.getLabel() !== "proxy-control")) {
|
|
356
|
+
channel.close();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
log(`[webrtc] Session ${sessionId.slice(0, 8)}: ${connectionId === null ? "media" : `control ${connectionId}`} channel "${channel.getLabel()}" opened`);
|
|
360
|
+
onDataChannel(peerId, channel);
|
|
345
361
|
});
|
|
346
362
|
|
|
347
|
-
peers.set(
|
|
363
|
+
peers.set(peerId, pc);
|
|
348
364
|
return pc;
|
|
349
365
|
}
|
|
350
366
|
|
|
@@ -364,6 +380,9 @@ export function createWebRtcManager({ sendSignal, onDataChannel, onLog, udpPort,
|
|
|
364
380
|
if (!signal || typeof signal.type !== "string") {
|
|
365
381
|
return;
|
|
366
382
|
}
|
|
383
|
+
const connectionId = signal.connectionId ?? null;
|
|
384
|
+
if (connectionId !== null && (typeof connectionId !== "string" || !/^[a-zA-Z0-9-]{1,64}$/.test(connectionId))) return;
|
|
385
|
+
const peerId = connectionId === null ? sessionId : `${sessionId}/${connectionId}`;
|
|
367
386
|
|
|
368
387
|
if (signal.type === "offer") {
|
|
369
388
|
if (typeof signal.sdp !== "string") {
|
|
@@ -375,19 +394,19 @@ export function createWebRtcManager({ sendSignal, onDataChannel, onLog, udpPort,
|
|
|
375
394
|
// single bad session must never crash the whole proxy — that would drop
|
|
376
395
|
// every other viewer and the tunnel. Contain it: fail this session only.
|
|
377
396
|
try {
|
|
378
|
-
const pc = getOrCreatePeer(sessionId);
|
|
397
|
+
const pc = getOrCreatePeer(sessionId, connectionId);
|
|
379
398
|
pc.setRemoteDescription(signal.sdp, "offer");
|
|
380
399
|
// `onLocalDescription` fires automatically with the SDP answer.
|
|
381
400
|
} catch (error) {
|
|
382
401
|
const message = error instanceof Error ? error.message : String(error);
|
|
383
402
|
log(`[webrtc] Session ${sessionId.slice(0, 8)}: failed to handle offer: ${message}`);
|
|
384
|
-
|
|
403
|
+
closePeer(peerId);
|
|
385
404
|
}
|
|
386
405
|
return;
|
|
387
406
|
}
|
|
388
407
|
|
|
389
408
|
if (signal.type === "candidate") {
|
|
390
|
-
const pc = peers.get(
|
|
409
|
+
const pc = peers.get(peerId);
|
|
391
410
|
if (!pc) {
|
|
392
411
|
return;
|
|
393
412
|
}
|
|
@@ -409,15 +428,23 @@ export function createWebRtcManager({ sendSignal, onDataChannel, onLog, udpPort,
|
|
|
409
428
|
* @param {string} sessionId
|
|
410
429
|
* @returns {void}
|
|
411
430
|
*/
|
|
412
|
-
function
|
|
413
|
-
const pc = peers.get(
|
|
431
|
+
function closePeer(peerId) {
|
|
432
|
+
const pc = peers.get(peerId);
|
|
414
433
|
if (pc) {
|
|
434
|
+
peers.delete(peerId);
|
|
435
|
+
const sessionId = peerId.split("/")[0];
|
|
436
|
+
if (controlPeers.get(sessionId) === peerId) controlPeers.delete(sessionId);
|
|
415
437
|
try { pc.close(); } catch { /* ignore */ }
|
|
416
|
-
|
|
417
|
-
log(`[webrtc] Session ${sessionId.slice(0, 8)}: closed`);
|
|
438
|
+
log(`[webrtc] Session ${sessionId.slice(0, 8)}: peer ${peerId} closed`);
|
|
418
439
|
}
|
|
419
440
|
}
|
|
420
441
|
|
|
442
|
+
function closeSession(sessionId) {
|
|
443
|
+
const control = controlPeers.get(sessionId);
|
|
444
|
+
if (control) closePeer(control);
|
|
445
|
+
closePeer(sessionId);
|
|
446
|
+
}
|
|
447
|
+
|
|
421
448
|
/**
|
|
422
449
|
* Close all peer connections and stop the shared UDP mux listener.
|
|
423
450
|
*
|
|
@@ -425,7 +452,7 @@ export function createWebRtcManager({ sendSignal, onDataChannel, onLog, udpPort,
|
|
|
425
452
|
*/
|
|
426
453
|
function dispose() {
|
|
427
454
|
for (const sessionId of [...peers.keys()]) {
|
|
428
|
-
|
|
455
|
+
closePeer(sessionId);
|
|
429
456
|
}
|
|
430
457
|
if (udpMuxListener) {
|
|
431
458
|
try { udpMuxListener.stop(); } catch { /* ignore */ }
|