rechrome 1.45.0 → 1.47.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/README.md +24 -11
- package/listeners.js +124 -3
- package/listeners.ts +79 -1
- package/package.json +1 -1
- package/rechrome.js +348 -100
- package/rechrome.ts +348 -100
- package/serve.js +21 -8
- package/serve.ts +21 -8
- package/shares.js +5 -2
- package/shares.ts +8 -2
- package/webrtc.js +16 -8
- package/webrtc.ts +15 -10
package/serve.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { receiveUpload, readRequestJson } from "./scoped-upload.js";
|
|
2
|
-
import { readListeners, listenerAddress, authorizeProfileRequest, canReadProfileFile, profileOutputPrefix, normalizePrefix, resolveAllowedProfile, type Listener } from "./listeners.js";
|
|
2
|
+
import { readListeners, listenerAddress, authorizeProfileRequest, canReadProfileFile, profileOutputPrefix, normalizePrefix, resolveAllowedProfile, shareRtcListeners, rtcListeners, type Listener } from "./listeners.js";
|
|
3
3
|
import { WEBRTC, authorizeCredential, credentialForBearer, readShares, redeemInvite, shareProblem, updateShares, webrtcCredentials, type Credential, type ShareConfig } from "./shares.js";
|
|
4
4
|
import { bearerOf, safeEqual } from "./secret.js";
|
|
5
5
|
import { WebrtcHost } from "./webrtc.js";
|
|
@@ -1350,11 +1350,18 @@ export async function serve() {
|
|
|
1350
1350
|
|
|
1351
1351
|
// WebRTC: the same handler, reached over an encrypted DataChannel. Each request carries the
|
|
1352
1352
|
// bearer of the credential whose room it came in on, so share checks apply unchanged.
|
|
1353
|
-
|
|
1354
|
-
const
|
|
1353
|
+
// Each WebRTC listener is its own transport: a share is checked against the one its room is on.
|
|
1354
|
+
const webrtcKey = randomBytes(24).toString("base64url");
|
|
1355
|
+
const webrtcListeners = new Map<string, Listener>();
|
|
1356
|
+
const webrtcListener = (name = WEBRTC): Listener => {
|
|
1357
|
+
let l = webrtcListeners.get(name);
|
|
1358
|
+
if (!l) webrtcListeners.set(name, l = { name, host: "webrtc", port: 0, key: webrtcKey, profiles: [] });
|
|
1359
|
+
return l;
|
|
1360
|
+
};
|
|
1361
|
+
const webrtc = new WebrtcHost(async (req, token, listenerName) => {
|
|
1355
1362
|
const headers = new Headers(req.headers);
|
|
1356
1363
|
headers.set("Authorization", `Bearer ${bearerOf(token)}`);
|
|
1357
|
-
return handle(new Request(req, { headers }), webrtcListener, () => "webrtc");
|
|
1364
|
+
return handle(new Request(req, { headers }), webrtcListener(listenerName), () => "webrtc");
|
|
1358
1365
|
}, log);
|
|
1359
1366
|
|
|
1360
1367
|
const startServer = (initial: Listener, reusePort = false) => Bun.serve({
|
|
@@ -1446,10 +1453,11 @@ export async function serve() {
|
|
|
1446
1453
|
let appliedShares = "";
|
|
1447
1454
|
const reloadShares = async () => {
|
|
1448
1455
|
const next = await readShares();
|
|
1456
|
+
const listenerConfig = await readListeners().catch(() => null);
|
|
1449
1457
|
// Expiry is time-based: fold "what is live right now" into the fingerprint so an expiring
|
|
1450
1458
|
// share or invite closes its rooms and sessions without anyone touching the file.
|
|
1451
1459
|
const now = Date.now();
|
|
1452
|
-
const fingerprint = JSON.stringify(next) + JSON.stringify(next.shares.map(s => [!!shareProblem(s, now), !!(s.inviteExpiresAt && now >= s.inviteExpiresAt)]));
|
|
1460
|
+
const fingerprint = JSON.stringify(next) + JSON.stringify(listenerConfig?.rtcListeners ?? []) + JSON.stringify(next.shares.map(s => [!!shareProblem(s, now), !!(s.inviteExpiresAt && now >= s.inviteExpiresAt)]));
|
|
1453
1461
|
if (fingerprint === appliedShares) return;
|
|
1454
1462
|
const allowed = new Set(next.shares.filter(s => !shareProblem(s, now)).flatMap(s => s.devices.map(d => `share:${s.id}:${d.id}`)));
|
|
1455
1463
|
const dropped = [...sessionOwners.entries()].filter(([, o]) => o.owner.startsWith("share:") && !allowed.has(o.owner));
|
|
@@ -1464,9 +1472,14 @@ export async function serve() {
|
|
|
1464
1472
|
log(`closed session ${session} of ${o.label}: its share was revoked or its device removed`);
|
|
1465
1473
|
}
|
|
1466
1474
|
}
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1475
|
+
// One room per share credential on each WebRTC listener the share names.
|
|
1476
|
+
const creds = webrtcCredentials(next, now, rtcListeners(listenerConfig).map(l => l.name));
|
|
1477
|
+
const rooms = creds.flatMap(c => shareRtcListeners(c.share, listenerConfig).map(l => ({
|
|
1478
|
+
token: c.token, signal: l.signal, listener: l.name, ...(l.ice?.length ? { ice: l.ice } : {}),
|
|
1479
|
+
label: `${c.kind === "device" ? `${c.share.label}/${c.device.name}` : `${c.share.label} (invite)`} via ${l.name}`,
|
|
1480
|
+
})));
|
|
1481
|
+
webrtc.sync(rooms);
|
|
1482
|
+
if (rooms.length || webrtc.size) log(`webrtc: listening for ${creds.length} share credential(s) in ${rooms.length} room(s)`);
|
|
1470
1483
|
};
|
|
1471
1484
|
await reloadShares().catch(error => log(`shares.json rejected: ${error}`));
|
|
1472
1485
|
await reload(true);
|
package/serve.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { receiveUpload, readRequestJson } from "./scoped-upload.ts";
|
|
2
|
-
import { readListeners, listenerAddress, authorizeProfileRequest, canReadProfileFile, profileOutputPrefix, normalizePrefix, resolveAllowedProfile, type Listener } from "./listeners.ts";
|
|
2
|
+
import { readListeners, listenerAddress, authorizeProfileRequest, canReadProfileFile, profileOutputPrefix, normalizePrefix, resolveAllowedProfile, shareRtcListeners, rtcListeners, type Listener } from "./listeners.ts";
|
|
3
3
|
import { WEBRTC, authorizeCredential, credentialForBearer, readShares, redeemInvite, shareProblem, updateShares, webrtcCredentials, type Credential, type ShareConfig } from "./shares.ts";
|
|
4
4
|
import { bearerOf, safeEqual } from "./secret.ts";
|
|
5
5
|
import { WebrtcHost } from "./webrtc.ts";
|
|
@@ -1350,11 +1350,18 @@ export async function serve() {
|
|
|
1350
1350
|
|
|
1351
1351
|
// WebRTC: the same handler, reached over an encrypted DataChannel. Each request carries the
|
|
1352
1352
|
// bearer of the credential whose room it came in on, so share checks apply unchanged.
|
|
1353
|
-
|
|
1354
|
-
const
|
|
1353
|
+
// Each WebRTC listener is its own transport: a share is checked against the one its room is on.
|
|
1354
|
+
const webrtcKey = randomBytes(24).toString("base64url");
|
|
1355
|
+
const webrtcListeners = new Map<string, Listener>();
|
|
1356
|
+
const webrtcListener = (name = WEBRTC): Listener => {
|
|
1357
|
+
let l = webrtcListeners.get(name);
|
|
1358
|
+
if (!l) webrtcListeners.set(name, l = { name, host: "webrtc", port: 0, key: webrtcKey, profiles: [] });
|
|
1359
|
+
return l;
|
|
1360
|
+
};
|
|
1361
|
+
const webrtc = new WebrtcHost(async (req, token, listenerName) => {
|
|
1355
1362
|
const headers = new Headers(req.headers);
|
|
1356
1363
|
headers.set("Authorization", `Bearer ${bearerOf(token)}`);
|
|
1357
|
-
return handle(new Request(req, { headers }), webrtcListener, () => "webrtc");
|
|
1364
|
+
return handle(new Request(req, { headers }), webrtcListener(listenerName), () => "webrtc");
|
|
1358
1365
|
}, log);
|
|
1359
1366
|
|
|
1360
1367
|
const startServer = (initial: Listener, reusePort = false) => Bun.serve({
|
|
@@ -1446,10 +1453,11 @@ export async function serve() {
|
|
|
1446
1453
|
let appliedShares = "";
|
|
1447
1454
|
const reloadShares = async () => {
|
|
1448
1455
|
const next = await readShares();
|
|
1456
|
+
const listenerConfig = await readListeners().catch(() => null);
|
|
1449
1457
|
// Expiry is time-based: fold "what is live right now" into the fingerprint so an expiring
|
|
1450
1458
|
// share or invite closes its rooms and sessions without anyone touching the file.
|
|
1451
1459
|
const now = Date.now();
|
|
1452
|
-
const fingerprint = JSON.stringify(next) + JSON.stringify(next.shares.map(s => [!!shareProblem(s, now), !!(s.inviteExpiresAt && now >= s.inviteExpiresAt)]));
|
|
1460
|
+
const fingerprint = JSON.stringify(next) + JSON.stringify(listenerConfig?.rtcListeners ?? []) + JSON.stringify(next.shares.map(s => [!!shareProblem(s, now), !!(s.inviteExpiresAt && now >= s.inviteExpiresAt)]));
|
|
1453
1461
|
if (fingerprint === appliedShares) return;
|
|
1454
1462
|
const allowed = new Set(next.shares.filter(s => !shareProblem(s, now)).flatMap(s => s.devices.map(d => `share:${s.id}:${d.id}`)));
|
|
1455
1463
|
const dropped = [...sessionOwners.entries()].filter(([, o]) => o.owner.startsWith("share:") && !allowed.has(o.owner));
|
|
@@ -1464,9 +1472,14 @@ export async function serve() {
|
|
|
1464
1472
|
log(`closed session ${session} of ${o.label}: its share was revoked or its device removed`);
|
|
1465
1473
|
}
|
|
1466
1474
|
}
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1475
|
+
// One room per share credential on each WebRTC listener the share names.
|
|
1476
|
+
const creds = webrtcCredentials(next, now, rtcListeners(listenerConfig).map(l => l.name));
|
|
1477
|
+
const rooms = creds.flatMap(c => shareRtcListeners(c.share, listenerConfig).map(l => ({
|
|
1478
|
+
token: c.token, signal: l.signal, listener: l.name, ...(l.ice?.length ? { ice: l.ice } : {}),
|
|
1479
|
+
label: `${c.kind === "device" ? `${c.share.label}/${c.device.name}` : `${c.share.label} (invite)`} via ${l.name}`,
|
|
1480
|
+
})));
|
|
1481
|
+
webrtc.sync(rooms);
|
|
1482
|
+
if (rooms.length || webrtc.size) log(`webrtc: listening for ${creds.length} share credential(s) in ${rooms.length} room(s)`);
|
|
1470
1483
|
};
|
|
1471
1484
|
await reloadShares().catch(error => log(`shares.json rejected: ${error}`));
|
|
1472
1485
|
await reload(true);
|
package/shares.js
CHANGED
|
@@ -95,6 +95,7 @@ function safeEqual(a, b) {
|
|
|
95
95
|
// shares.ts
|
|
96
96
|
var WEBRTC = "webrtc";
|
|
97
97
|
var DEFAULT_SIGNAL = "https://rechrome.pages.dev/";
|
|
98
|
+
var DEFAULT_RTC = "webrtc-default";
|
|
98
99
|
var SHARES_FILE = join(homedir(), ".rechrome", "shares.json");
|
|
99
100
|
var LABEL = /^[a-z0-9][a-z0-9_-]{0,39}$/;
|
|
100
101
|
function validateShares(value) {
|
|
@@ -314,10 +315,11 @@ function parseDuration(text) {
|
|
|
314
315
|
const unit = { m: 60000, min: 60000, h: 3600000, d: 86400000, w: 604800000 }[m[2]];
|
|
315
316
|
return Math.round(Number(m[1]) * unit);
|
|
316
317
|
}
|
|
317
|
-
function webrtcCredentials(config, now = Date.now()) {
|
|
318
|
+
function webrtcCredentials(config, now = Date.now(), rtcNames = [DEFAULT_RTC]) {
|
|
319
|
+
const names = new Set([WEBRTC, DEFAULT_RTC, ...rtcNames]);
|
|
318
320
|
const out = [];
|
|
319
321
|
for (const share of config.shares) {
|
|
320
|
-
if (!share.via.
|
|
322
|
+
if (!share.via.some((v) => names.has(v)) || shareProblem(share, now))
|
|
321
323
|
continue;
|
|
322
324
|
if (!(share.inviteExpiresAt && now >= share.inviteExpiresAt))
|
|
323
325
|
out.push({ share, kind: "invite", token: share.invite });
|
|
@@ -327,6 +329,7 @@ function webrtcCredentials(config, now = Date.now()) {
|
|
|
327
329
|
return out;
|
|
328
330
|
}
|
|
329
331
|
export {
|
|
332
|
+
DEFAULT_RTC,
|
|
330
333
|
DEFAULT_SIGNAL,
|
|
331
334
|
SHARES_FILE,
|
|
332
335
|
WEBRTC,
|
package/shares.ts
CHANGED
|
@@ -13,6 +13,8 @@ import { bearerOf, credentialIdOf, newToken, safeEqual } from "./secret.ts";
|
|
|
13
13
|
|
|
14
14
|
export const WEBRTC = "webrtc";
|
|
15
15
|
export const DEFAULT_SIGNAL = "https://rechrome.pages.dev/";
|
|
16
|
+
// The built-in WebRTC listener's name (see listeners.ts): the public signal server above.
|
|
17
|
+
export const DEFAULT_RTC = "webrtc-default";
|
|
16
18
|
export const SHARES_FILE = join(homedir(), ".rechrome", "shares.json");
|
|
17
19
|
|
|
18
20
|
export type Device = { id: string; name: string; token: string; addedAt: number; lastSeen?: number; approved: boolean };
|
|
@@ -127,6 +129,8 @@ export function createShare(config: ShareConfig, opts: NewShareOptions): Share {
|
|
|
127
129
|
inviteTtlMs: opts.inviteTtlMs,
|
|
128
130
|
profiles: [...new Set(opts.profiles)],
|
|
129
131
|
via: [...new Set(opts.via)],
|
|
132
|
+
// Only the legacy "webrtc" transport keeps its signal server on the share; a WebRTC
|
|
133
|
+
// listener carries its own.
|
|
130
134
|
signal: opts.via.includes(WEBRTC) ? normalizeSignal(opts.signal ?? DEFAULT_SIGNAL) : undefined,
|
|
131
135
|
createdAt: now,
|
|
132
136
|
expiresAt: opts.expiresAt,
|
|
@@ -244,10 +248,12 @@ export function parseDuration(text: string): number | undefined {
|
|
|
244
248
|
}
|
|
245
249
|
|
|
246
250
|
/** Every credential a WebRTC host should be reachable on: unused invites and devices. */
|
|
247
|
-
|
|
251
|
+
/** Live credentials of shares offered on a WebRTC listener (`rtcNames`; the legacy "webrtc" always counts). */
|
|
252
|
+
export function webrtcCredentials(config: ShareConfig, now = Date.now(), rtcNames: Iterable<string> = [DEFAULT_RTC]): Credential[] {
|
|
253
|
+
const names = new Set([WEBRTC, DEFAULT_RTC, ...rtcNames]);
|
|
248
254
|
const out: Credential[] = [];
|
|
249
255
|
for (const share of config.shares) {
|
|
250
|
-
if (!share.via.
|
|
256
|
+
if (!share.via.some(v => names.has(v)) || shareProblem(share, now)) continue;
|
|
251
257
|
// A used-up invite keeps its room while the share lives, so whoever tries it again hears
|
|
252
258
|
// "already used" (a sign the link leaked) rather than "host offline".
|
|
253
259
|
if (!(share.inviteExpiresAt && now >= share.inviteExpiresAt)) out.push({ share, kind: "invite", token: share.invite });
|
package/webrtc.js
CHANGED
|
@@ -231,8 +231,8 @@ function iceServersFromEnv(env = process.env) {
|
|
|
231
231
|
return DEFAULT_STUN;
|
|
232
232
|
}
|
|
233
233
|
var turnCache = null;
|
|
234
|
-
async function hostIceServers(log, env = process.env) {
|
|
235
|
-
const base = iceServersFromEnv(env);
|
|
234
|
+
async function hostIceServers(log, env = process.env, extra = []) {
|
|
235
|
+
const base = [...iceServersFromEnv(env), ...extra];
|
|
236
236
|
const { CF_TURN_KEY_ID: keyId, CF_TURN_API_TOKEN: apiToken } = env;
|
|
237
237
|
if (!keyId || !apiToken)
|
|
238
238
|
return base;
|
|
@@ -421,15 +421,23 @@ class WebrtcHost {
|
|
|
421
421
|
this.log = log;
|
|
422
422
|
}
|
|
423
423
|
sync(creds) {
|
|
424
|
-
const wanted = new Map
|
|
424
|
+
const wanted = new Map;
|
|
425
|
+
for (const c of creds) {
|
|
426
|
+
const key = `${c.signal}\x00${c.token}`;
|
|
427
|
+
if (!wanted.has(key))
|
|
428
|
+
wanted.set(key, c);
|
|
429
|
+
}
|
|
425
430
|
for (const [key, room] of this.rooms)
|
|
426
431
|
if (!wanted.has(key)) {
|
|
427
432
|
this.stopRoom(room);
|
|
428
433
|
this.rooms.delete(key);
|
|
429
434
|
}
|
|
430
435
|
for (const [key, cred] of wanted) {
|
|
431
|
-
|
|
436
|
+
const live = this.rooms.get(key);
|
|
437
|
+
if (live) {
|
|
438
|
+
live.cred = cred;
|
|
432
439
|
continue;
|
|
440
|
+
}
|
|
433
441
|
const room = { cred, peers: new Map, backoff: 1000, stopped: false };
|
|
434
442
|
this.rooms.set(key, room);
|
|
435
443
|
this.connect(room);
|
|
@@ -515,7 +523,7 @@ class WebrtcHost {
|
|
|
515
523
|
}
|
|
516
524
|
async startPeer(room, peerId) {
|
|
517
525
|
const { RTCPeerConnection } = await loadRTC(this.log);
|
|
518
|
-
const ice = await hostIceServers(this.log);
|
|
526
|
+
const ice = await hostIceServers(this.log, process.env, room.cred.ice ?? []);
|
|
519
527
|
const pc = new RTCPeerConnection({ iceServers: ice });
|
|
520
528
|
let keysReady;
|
|
521
529
|
const keys = new Promise((r) => keysReady = r);
|
|
@@ -539,7 +547,7 @@ class WebrtcHost {
|
|
|
539
547
|
a.abort();
|
|
540
548
|
this.closePeer(room, peerId);
|
|
541
549
|
};
|
|
542
|
-
channel.onFrame = (frame) => void this.onFrame(channel, room.cred.token, aborts, frame);
|
|
550
|
+
channel.onFrame = (frame) => void this.onFrame(channel, room.cred.token, aborts, frame, room.cred.listener);
|
|
543
551
|
dc.onopen = async () => {
|
|
544
552
|
await channel.setKeys(room.cred.token, await keys);
|
|
545
553
|
if (await channel.confirm())
|
|
@@ -557,7 +565,7 @@ class WebrtcHost {
|
|
|
557
565
|
await pc.setLocalDescription(offer);
|
|
558
566
|
send({ type: "offer", sdp: pc.localDescription.sdp, ice });
|
|
559
567
|
}
|
|
560
|
-
async onFrame(channel, token, aborts, f) {
|
|
568
|
+
async onFrame(channel, token, aborts, f, listener) {
|
|
561
569
|
if (f?.t === "abort") {
|
|
562
570
|
aborts.get(f.id)?.abort();
|
|
563
571
|
aborts.delete(f.id);
|
|
@@ -574,7 +582,7 @@ class WebrtcHost {
|
|
|
574
582
|
body: f.body != null && !["GET", "HEAD"].includes(f.method) ? f.body : undefined,
|
|
575
583
|
signal: ac.signal
|
|
576
584
|
});
|
|
577
|
-
const res = await this.handler(req, token);
|
|
585
|
+
const res = await this.handler(req, token, listener);
|
|
578
586
|
await channel.sendFrame({ t: "res", id: f.id, status: res.status, ct: res.headers.get("content-type") ?? "" });
|
|
579
587
|
if (res.body) {
|
|
580
588
|
const reader = res.body.getReader();
|
package/webrtc.ts
CHANGED
|
@@ -61,8 +61,9 @@ export function iceServersFromEnv(env = process.env): IceServer[] {
|
|
|
61
61
|
// Short-lived Cloudflare TURN credentials from the HOST owner's own TURN key (CF_TURN_KEY_ID +
|
|
62
62
|
// CF_TURN_API_TOKEN), handed to each peer in the offer. Without them: STUN only.
|
|
63
63
|
let turnCache: { servers: IceServer[]; exp: number } | null = null;
|
|
64
|
-
export async function hostIceServers(log: Log, env = process.env): Promise<IceServer[]> {
|
|
65
|
-
|
|
64
|
+
export async function hostIceServers(log: Log, env = process.env, extra: IceServer[] = []): Promise<IceServer[]> {
|
|
65
|
+
// A listener's own STUN/TURN servers come after the base ones (RECH_ICE_SERVERS, else public STUN).
|
|
66
|
+
const base = [...iceServersFromEnv(env), ...extra];
|
|
66
67
|
const keyId = env.CF_TURN_KEY_ID, apiToken = env.CF_TURN_API_TOKEN;
|
|
67
68
|
if (!keyId || !apiToken) return base;
|
|
68
69
|
if (turnCache && turnCache.exp > Date.now()) return [...base, ...turnCache.servers];
|
|
@@ -205,8 +206,9 @@ class SignalSocket {
|
|
|
205
206
|
}
|
|
206
207
|
|
|
207
208
|
// ---- host ------------------------------------------------------------------------------
|
|
208
|
-
|
|
209
|
-
export type
|
|
209
|
+
// listener: the WebRTC listener this room belongs to (a share's `via` entry); ice: its STUN/TURN.
|
|
210
|
+
export type HostCredential = { token: string; signal: string; label: string; listener?: string; ice?: IceServer[] };
|
|
211
|
+
export type HostHandler = (req: Request, token: string, listener?: string) => Promise<Response>;
|
|
210
212
|
|
|
211
213
|
/** Keeps one signal room per credential and serves each joining peer through `handler`. */
|
|
212
214
|
export class WebrtcHost {
|
|
@@ -215,10 +217,13 @@ export class WebrtcHost {
|
|
|
215
217
|
|
|
216
218
|
/** Open rooms for new credentials, close rooms for ones that are gone. */
|
|
217
219
|
sync(creds: HostCredential[]): void {
|
|
218
|
-
|
|
220
|
+
// One room per (signal server, credential): the first listener naming a signal server wins.
|
|
221
|
+
const wanted = new Map<string, HostCredential>();
|
|
222
|
+
for (const c of creds) { const key = `${c.signal}\0${c.token}`; if (!wanted.has(key)) wanted.set(key, c); }
|
|
219
223
|
for (const [key, room] of this.rooms) if (!wanted.has(key)) { this.stopRoom(room); this.rooms.delete(key); }
|
|
220
224
|
for (const [key, cred] of wanted) {
|
|
221
|
-
|
|
225
|
+
const live = this.rooms.get(key);
|
|
226
|
+
if (live) { live.cred = cred; continue; } // a listener's ICE may have changed: next peer uses it
|
|
222
227
|
const room = { cred, peers: new Map(), backoff: 1000, stopped: false } as any;
|
|
223
228
|
this.rooms.set(key, room);
|
|
224
229
|
void this.connect(room);
|
|
@@ -288,7 +293,7 @@ export class WebrtcHost {
|
|
|
288
293
|
|
|
289
294
|
private async startPeer(room: any, peerId: string): Promise<void> {
|
|
290
295
|
const { RTCPeerConnection } = await loadRTC(this.log);
|
|
291
|
-
const ice = await hostIceServers(this.log);
|
|
296
|
+
const ice = await hostIceServers(this.log, process.env, room.cred.ice ?? []);
|
|
292
297
|
const pc = new RTCPeerConnection({ iceServers: ice });
|
|
293
298
|
let keysReady!: (th: Uint8Array) => void;
|
|
294
299
|
const keys = new Promise<Uint8Array>(r => (keysReady = r));
|
|
@@ -302,7 +307,7 @@ export class WebrtcHost {
|
|
|
302
307
|
peer.channel = channel;
|
|
303
308
|
const aborts = new Map<string, AbortController>();
|
|
304
309
|
channel.onClose = () => { for (const a of aborts.values()) a.abort(); this.closePeer(room, peerId); };
|
|
305
|
-
channel.onFrame = frame => void this.onFrame(channel, room.cred.token, aborts, frame);
|
|
310
|
+
channel.onFrame = frame => void this.onFrame(channel, room.cred.token, aborts, frame, room.cred.listener);
|
|
306
311
|
dc.onopen = async () => {
|
|
307
312
|
await channel.setKeys(room.cred.token, await keys);
|
|
308
313
|
if (await channel.confirm()) this.log(`webrtc ${room.cred.label}: peer ${peerId} connected`);
|
|
@@ -315,7 +320,7 @@ export class WebrtcHost {
|
|
|
315
320
|
send({ type: "offer", sdp: pc.localDescription.sdp, ice });
|
|
316
321
|
}
|
|
317
322
|
|
|
318
|
-
private async onFrame(channel: SealedChannel, token: string, aborts: Map<string, AbortController>, f: any): Promise<void> {
|
|
323
|
+
private async onFrame(channel: SealedChannel, token: string, aborts: Map<string, AbortController>, f: any, listener?: string): Promise<void> {
|
|
319
324
|
if (f?.t === "abort") { aborts.get(f.id)?.abort(); aborts.delete(f.id); return; }
|
|
320
325
|
if (f?.t !== "req" || typeof f.id !== "string" || typeof f.path !== "string" || !f.path.startsWith("/")) return;
|
|
321
326
|
const ac = new AbortController();
|
|
@@ -327,7 +332,7 @@ export class WebrtcHost {
|
|
|
327
332
|
body: f.body != null && !["GET", "HEAD"].includes(f.method) ? f.body : undefined,
|
|
328
333
|
signal: ac.signal,
|
|
329
334
|
});
|
|
330
|
-
const res = await this.handler(req, token);
|
|
335
|
+
const res = await this.handler(req, token, listener);
|
|
331
336
|
await channel.sendFrame({ t: "res", id: f.id, status: res.status, ct: res.headers.get("content-type") ?? "" });
|
|
332
337
|
if (res.body) {
|
|
333
338
|
const reader = res.body.getReader();
|