rechrome 1.45.0 → 1.46.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 +16 -10
- package/listeners.js +124 -3
- package/listeners.ts +79 -1
- package/package.json +1 -1
- package/rechrome.js +292 -87
- package/rechrome.ts +292 -87
- 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/README.md
CHANGED
|
@@ -151,10 +151,12 @@ comment for some .env loaders. rech quotes it when it saves one.
|
|
|
151
151
|
|
|
152
152
|
### 4. Share with another machine
|
|
153
153
|
|
|
154
|
-
On the host (the machine with Chrome), make an invite:
|
|
154
|
+
On the host (the machine with Chrome), make an invite. `rech share` has no default transport:
|
|
155
|
+
in a terminal, `rech share` asks which profile and which listeners to share over; in a script,
|
|
156
|
+
name them:
|
|
155
157
|
|
|
156
158
|
```bash
|
|
157
|
-
rech share
|
|
159
|
+
rech share you@example.com --webrtc --name alice
|
|
158
160
|
# Share "alice": you@example.com
|
|
159
161
|
# Invite for one device (unused, it expires …; secret: send it privately):
|
|
160
162
|
# https://rechrome.pages.dev/#rech1_7fQxC9tZp2mVb8wLrN4hdK3kT9aQ
|
|
@@ -168,8 +170,7 @@ rech connect 'https://rechrome.pages.dev/#rech1_…'
|
|
|
168
170
|
rech open https://example.com
|
|
169
171
|
```
|
|
170
172
|
|
|
171
|
-
|
|
172
|
-
domain. A small signal server introduces them, and then traffic flows peer to peer, end-to-end
|
|
173
|
+
Over **WebRTC** the two machines need no open port, VPN or domain. A small signal server introduces them, and then traffic flows peer to peer, end-to-end
|
|
173
174
|
encrypted with keys the signal server never sees. The first `rech` command after connecting starts
|
|
174
175
|
a small local bridge that keeps the connection open between commands.
|
|
175
176
|
|
|
@@ -180,10 +181,12 @@ expire after 24 hours, and WebRTC shares after 30 days unless renewed.
|
|
|
180
181
|
|
|
181
182
|
| On the host | |
|
|
182
183
|
|---|---|
|
|
183
|
-
| `rech share
|
|
184
|
-
| `--
|
|
184
|
+
| `rech share [<profile..>] [--name n]` | Invite link; in a terminal, with no transport flag, pick profile and listeners from a list |
|
|
185
|
+
| `--webrtc` · `--webrtc=<signal-url>` | WebRTC via the built-in `webrtc-default` listener (rechrome.pages.dev: best effort, for trying it out; no speed or uptime promise) · via your own signal server (its listener is made if needed) |
|
|
186
|
+
| `--lan` · `--tailscale` · `--via <listener,…>` | Direct HTTP on this network · your tailnet · existing listeners (`rech listener ls`); combine several for one link each |
|
|
185
187
|
| `--devices 3` · `--approve` | Let 3 devices redeem it · make each new device wait for `rech share approve` |
|
|
186
|
-
| `--expires 7d` · `--invite-ttl 1h`
|
|
188
|
+
| `--expires 7d` · `--invite-ttl 1h` | Lifetimes |
|
|
189
|
+
| `rech listener add <name> --webrtc=<signal-url> [--turn <url>]… [--turn-env VAR]…` | A WebRTC listener on your [own signal server](docs/signal-selfhost.md), with optional STUN/TURN |
|
|
187
190
|
| `rech share ls` · `rech share devices <share> [--rm <device>]` | Shares, devices, expiry · drop one device |
|
|
188
191
|
| `rech share allow\|deny <share> <profile..>` | Change the profiles; the link and devices stay |
|
|
189
192
|
| `rech share rotate <share>` | A fresh invite; the old one stops working |
|
|
@@ -201,9 +204,12 @@ navigation, tabs, snapshots, screenshots and basic interactions, but not `eval`/
|
|
|
201
204
|
filesystem commands (see [Remote access](#remote-access)).
|
|
202
205
|
|
|
203
206
|
**WebRTC needs** `node-datachannel`, installed on first use into `~/.rechrome/webrtc`. When a
|
|
204
|
-
direct path can't be found (some corporate networks, symmetric NAT), give the host TURN:
|
|
205
|
-
|
|
206
|
-
|
|
207
|
+
direct path can't be found (some corporate networks, symmetric NAT), give the host TURN: add it to
|
|
208
|
+
a WebRTC listener as one URL, `rech listener add home --webrtc=https://signal.example.com/ --turn-env TEAM_TURN`
|
|
209
|
+
with `TEAM_TURN=turns://user:pass@turn.example.com:5349` (user and password percent-encoded;
|
|
210
|
+
`rech listener ls` shows the username, never the password). Or set `CF_TURN_KEY_ID` and
|
|
211
|
+
`CF_TURN_API_TOKEN` (a [Cloudflare TURN](https://developers.cloudflare.com/realtime/turn/) key; its
|
|
212
|
+
free tier is 1,000 GB/month) in the daemon's environment, or share `--tailscale`.
|
|
207
213
|
|
|
208
214
|
**Keep links out of git.** `rech status`, `rech connect` and `rech share --save` warn when the file
|
|
209
215
|
holding `RECHROME_URL` is committed, staged, not git-ignored, or readable by other users. Links
|
package/listeners.js
CHANGED
|
@@ -21,11 +21,123 @@ Types: ${UPLOAD_TYPES.join(", ")}, up to ${MAX_UPLOAD_BYTES / 1024 / 1024} MiB.
|
|
|
21
21
|
// listeners.ts
|
|
22
22
|
import { isIP } from "net";
|
|
23
23
|
import { createHash, randomBytes } from "crypto";
|
|
24
|
+
import { homedir as homedir2 } from "os";
|
|
25
|
+
import { join as join3 } from "path";
|
|
26
|
+
import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
27
|
+
|
|
28
|
+
// shares.ts
|
|
24
29
|
import { homedir } from "os";
|
|
25
30
|
import { join as join2 } from "path";
|
|
26
|
-
|
|
31
|
+
|
|
32
|
+
// secret.ts
|
|
33
|
+
var B58 = "[1-9A-HJ-NP-Za-km-z]";
|
|
34
|
+
var BODY_LEN = 22;
|
|
35
|
+
var SUM_LEN = 6;
|
|
36
|
+
var TOKEN_PATTERN = new RegExp(`\\brech1_${B58}{${BODY_LEN + SUM_LEN}}\\b`, "g");
|
|
37
|
+
var EXACT = new RegExp(`^rech1_(${B58}{${BODY_LEN}})(${B58}{${SUM_LEN}})$`);
|
|
38
|
+
var CRC_TABLE = (() => {
|
|
39
|
+
const t = new Uint32Array(256);
|
|
40
|
+
for (let i = 0;i < 256; i++) {
|
|
41
|
+
let c = i;
|
|
42
|
+
for (let k = 0;k < 8; k++)
|
|
43
|
+
c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
44
|
+
t[i] = c >>> 0;
|
|
45
|
+
}
|
|
46
|
+
return t;
|
|
47
|
+
})();
|
|
48
|
+
|
|
49
|
+
// shares.ts
|
|
50
|
+
var DEFAULT_SIGNAL = "https://rechrome.pages.dev/";
|
|
51
|
+
var DEFAULT_RTC = "webrtc-default";
|
|
52
|
+
var SHARES_FILE = join2(homedir(), ".rechrome", "shares.json");
|
|
53
|
+
function normalizeSignal(value) {
|
|
54
|
+
let url;
|
|
55
|
+
try {
|
|
56
|
+
url = new URL(value);
|
|
57
|
+
} catch {
|
|
58
|
+
throw new Error(`Signal server must be an http(s) URL, e.g. https://signal.example.com; got "${value}"`);
|
|
59
|
+
}
|
|
60
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash)
|
|
61
|
+
throw new Error("Signal server must be a plain http(s) URL, without credentials, query or fragment");
|
|
62
|
+
if (!url.pathname.endsWith("/"))
|
|
63
|
+
url.pathname += "/";
|
|
64
|
+
return url.toString();
|
|
65
|
+
}
|
|
66
|
+
var bearerCache = new Map;
|
|
67
|
+
|
|
68
|
+
// listeners.ts
|
|
69
|
+
var LEGACY_RTC = "webrtc";
|
|
70
|
+
var DEFAULT_RTC_NOTE = "best effort, for trying it out: no speed or uptime promise. Host your own: rech listener add <name> --webrtc=<url>";
|
|
71
|
+
var defaultRtcListener = () => ({ name: DEFAULT_RTC, kind: "webrtc", signal: DEFAULT_SIGNAL, builtin: true });
|
|
72
|
+
function rtcListeners(config) {
|
|
73
|
+
return [...config?.rtcListeners ?? [], defaultRtcListener()];
|
|
74
|
+
}
|
|
75
|
+
function shareRtcListeners(share, config) {
|
|
76
|
+
const all = rtcListeners(config);
|
|
77
|
+
const out = [];
|
|
78
|
+
for (const via of share.via) {
|
|
79
|
+
if (via === LEGACY_RTC)
|
|
80
|
+
out.push({ name: LEGACY_RTC, kind: "webrtc", signal: normalizeSignal(share.signal ?? DEFAULT_SIGNAL), builtin: true });
|
|
81
|
+
else {
|
|
82
|
+
const l = all.find((x) => x.name === via);
|
|
83
|
+
if (l)
|
|
84
|
+
out.push(l);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
function parseIceUrl(value) {
|
|
90
|
+
const raw = value.trim();
|
|
91
|
+
const m = /^(stuns?|turns?):(?:\/\/)?(?:([^:@/\s]*)(?::([^@/\s]*))?@)?([^/?#@\s]+)(\?[^#]*)?$/i.exec(raw);
|
|
92
|
+
if (!m)
|
|
93
|
+
throw new Error("A STUN/TURN server is a URL like turns://user:pass@turn.example.com:5349 or stun:stun.example.com:3478");
|
|
94
|
+
const [, scheme, user, pass, hostPort, query = ""] = m;
|
|
95
|
+
const kind = scheme.toLowerCase();
|
|
96
|
+
if (query && !/^\?transport=(udp|tcp)$/i.test(query))
|
|
97
|
+
throw new Error("A STUN/TURN URL may only carry ?transport=udp or ?transport=tcp");
|
|
98
|
+
const urls = `${kind}:${hostPort}${query}`;
|
|
99
|
+
if (user === undefined)
|
|
100
|
+
return { urls };
|
|
101
|
+
if (kind.startsWith("stun"))
|
|
102
|
+
throw new Error("A STUN server takes no username or password");
|
|
103
|
+
let username, credential;
|
|
104
|
+
try {
|
|
105
|
+
username = decodeURIComponent(user);
|
|
106
|
+
credential = decodeURIComponent(pass ?? "");
|
|
107
|
+
} catch {
|
|
108
|
+
throw new Error("The TURN username or password has a bad %-escape");
|
|
109
|
+
}
|
|
110
|
+
if (!username || !credential)
|
|
111
|
+
throw new Error("A TURN URL with credentials needs both: turns://user:pass@host:port");
|
|
112
|
+
return { urls, username, credential };
|
|
113
|
+
}
|
|
114
|
+
function iceLabel(s) {
|
|
115
|
+
const url = [s.urls].flat()[0] ?? "";
|
|
116
|
+
if (!s.username)
|
|
117
|
+
return url;
|
|
118
|
+
const [scheme, ...rest] = url.split(":");
|
|
119
|
+
return `${scheme}://${encodeURIComponent(s.username)}:\u2026@${rest.join(":")}`;
|
|
120
|
+
}
|
|
121
|
+
function validateRtcListeners(value, socketNames) {
|
|
122
|
+
if (value === undefined)
|
|
123
|
+
return;
|
|
124
|
+
if (!Array.isArray(value))
|
|
125
|
+
throw new Error("listeners.json rtcListeners must be a list");
|
|
126
|
+
const names = new Set;
|
|
127
|
+
for (const l of value) {
|
|
128
|
+
if (!l || l.kind !== "webrtc" || typeof l.name !== "string" || !/^[a-zA-Z0-9_-]{1,40}$/.test(l.name) || typeof l.signal !== "string")
|
|
129
|
+
throw new Error("Each WebRTC listener needs a name and a signal server URL");
|
|
130
|
+
if (l.name === DEFAULT_RTC || l.name === LEGACY_RTC || names.has(l.name) || socketNames.has(l.name))
|
|
131
|
+
throw new Error(`Listener names must be unique ("${DEFAULT_RTC}" and "${LEGACY_RTC}" are built in)`);
|
|
132
|
+
l.signal = normalizeSignal(l.signal);
|
|
133
|
+
if (l.ice !== undefined && (!Array.isArray(l.ice) || l.ice.some((s) => !s || typeof s.urls !== "string" && !Array.isArray(s.urls) || s.username !== undefined && typeof s.username !== "string" || s.credential !== undefined && typeof s.credential !== "string")))
|
|
134
|
+
throw new Error(`WebRTC listener "${l.name}": ice must be a list of {urls, username?, credential?}`);
|
|
135
|
+
delete l.builtin;
|
|
136
|
+
names.add(l.name);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
27
139
|
var MAX_RETIRED_PORTS = 32;
|
|
28
|
-
var LISTENERS_FILE =
|
|
140
|
+
var LISTENERS_FILE = join3(homedir2(), ".rechrome", "listeners.json");
|
|
29
141
|
var isLoopback = (host) => host === "::1" || /^127\./.test(host);
|
|
30
142
|
var listenerAddress = (l) => `${l.host.includes(":") ? `[${l.host}]` : l.host}:${l.port}`;
|
|
31
143
|
var profileOutputPrefix = (profile) => `profiles/${createHash("sha256").update(profile).digest("hex")}/`;
|
|
@@ -89,6 +201,7 @@ function validateListeners(value) {
|
|
|
89
201
|
addresses.add(address);
|
|
90
202
|
keys.add(l.key);
|
|
91
203
|
}
|
|
204
|
+
validateRtcListeners(config.rtcListeners, names);
|
|
92
205
|
if (config.retiredPorts !== undefined && (!Array.isArray(config.retiredPorts) || config.retiredPorts.some((p) => !Number.isInteger(p) || p < 1 || p > 65535)))
|
|
93
206
|
throw new Error("listeners.json retiredPorts must be a list of ports");
|
|
94
207
|
return config;
|
|
@@ -118,7 +231,7 @@ async function writeListeners(config) {
|
|
|
118
231
|
config.retiredPorts = retired;
|
|
119
232
|
else
|
|
120
233
|
delete config.retiredPorts;
|
|
121
|
-
await mkdir(
|
|
234
|
+
await mkdir(join3(homedir2(), ".rechrome"), { recursive: true });
|
|
122
235
|
const temp = `${LISTENERS_FILE}.${randomBytes(6).toString("hex")}.tmp`;
|
|
123
236
|
await writeFile(temp, JSON.stringify(config, null, 2) + `
|
|
124
237
|
`, { mode: 384 });
|
|
@@ -243,24 +356,32 @@ function planProfileRemoval(key, registry, listeners, managedRoot) {
|
|
|
243
356
|
return { keys, listeners: edits, ...managed ? { dataDir: entry.userDataDir } : {} };
|
|
244
357
|
}
|
|
245
358
|
export {
|
|
359
|
+
DEFAULT_RTC,
|
|
360
|
+
DEFAULT_RTC_NOTE,
|
|
361
|
+
LEGACY_RTC,
|
|
246
362
|
LISTENERS_FILE,
|
|
247
363
|
allowProfiles,
|
|
248
364
|
authorizeProfileRequest,
|
|
249
365
|
canReadProfileFile,
|
|
250
366
|
canonicalProfileKeys,
|
|
367
|
+
defaultRtcListener,
|
|
251
368
|
denyProfiles,
|
|
369
|
+
iceLabel,
|
|
252
370
|
isLoopback,
|
|
253
371
|
listenerAddress,
|
|
254
372
|
normalizePrefix,
|
|
255
373
|
normalizePublicUrl,
|
|
374
|
+
parseIceUrl,
|
|
256
375
|
planProfileRemoval,
|
|
257
376
|
profileOutputPrefix,
|
|
258
377
|
readListeners,
|
|
259
378
|
resolveAllowedProfile,
|
|
260
379
|
retirePorts,
|
|
261
380
|
rotateListenerKey,
|
|
381
|
+
rtcListeners,
|
|
262
382
|
serviceUrl,
|
|
263
383
|
setPublicUrl,
|
|
384
|
+
shareRtcListeners,
|
|
264
385
|
validateListeners,
|
|
265
386
|
writeListeners
|
|
266
387
|
};
|
package/listeners.ts
CHANGED
|
@@ -4,12 +4,89 @@ import { createHash, randomBytes } from "crypto";
|
|
|
4
4
|
import { homedir } from "os";
|
|
5
5
|
import { join } from "path";
|
|
6
6
|
import { mkdir, readFile, rename, writeFile } from "fs/promises";
|
|
7
|
+
import { DEFAULT_RTC, DEFAULT_SIGNAL, normalizeSignal } from "./shares.ts";
|
|
7
8
|
|
|
8
9
|
// publicUrl: where a reverse proxy (Tailscale Serve, Caddy, nginx, ...) exposes this listener.
|
|
9
10
|
export type Listener = { name: string; host: string; port: number; key: string; profiles: string[] | "*"; prefix?: string; publicUrl?: string; uploads?: UploadPolicy | false; readSecrets?: boolean; readCookies?: boolean; keepDownloads?: boolean };
|
|
10
11
|
// retiredPorts: ports of removed listeners, newest last. New listeners skip them so a link
|
|
11
12
|
// saved for a removed listener gets "connection refused", not some other listener's answer.
|
|
12
|
-
|
|
13
|
+
// rtcListeners: WebRTC transports (a signal server, plus optional STUN/TURN). They live under
|
|
14
|
+
// their own key so a daemon from before they existed reads the file unchanged and ignores them.
|
|
15
|
+
export type ListenerConfig = { version: 1; listeners: Listener[]; retiredPorts?: number[]; rtcListeners?: RtcListener[] };
|
|
16
|
+
export type IceServer = { urls: string | string[]; username?: string; credential?: string };
|
|
17
|
+
export type RtcListener = { name: string; kind: "webrtc"; signal: string; ice?: IceServer[]; builtin?: true };
|
|
18
|
+
|
|
19
|
+
// The built-in WebRTC listener: the public signal server, for trying sharing out. Never written
|
|
20
|
+
// to listeners.json; a share names it in `via`. The legacy `via: ["webrtc"]` (+ share.signal)
|
|
21
|
+
// maps to a virtual listener named "webrtc".
|
|
22
|
+
export { DEFAULT_RTC };
|
|
23
|
+
export const LEGACY_RTC = "webrtc";
|
|
24
|
+
export const DEFAULT_RTC_NOTE = "best effort, for trying it out: no speed or uptime promise. Host your own: rech listener add <name> --webrtc=<url>";
|
|
25
|
+
export const defaultRtcListener = (): RtcListener => ({ name: DEFAULT_RTC, kind: "webrtc", signal: DEFAULT_SIGNAL, builtin: true });
|
|
26
|
+
|
|
27
|
+
/** Every WebRTC listener a share can name: the saved ones plus the built-in default. */
|
|
28
|
+
export function rtcListeners(config: ListenerConfig | null): RtcListener[] {
|
|
29
|
+
return [...(config?.rtcListeners ?? []), defaultRtcListener()];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The WebRTC listeners a share is offered on, in `via` order. */
|
|
33
|
+
export function shareRtcListeners(share: { via: string[]; signal?: string }, config: ListenerConfig | null): RtcListener[] {
|
|
34
|
+
const all = rtcListeners(config);
|
|
35
|
+
const out: RtcListener[] = [];
|
|
36
|
+
for (const via of share.via) {
|
|
37
|
+
if (via === LEGACY_RTC) out.push({ name: LEGACY_RTC, kind: "webrtc", signal: normalizeSignal(share.signal ?? DEFAULT_SIGNAL), builtin: true });
|
|
38
|
+
else { const l = all.find(x => x.name === via); if (l) out.push(l); }
|
|
39
|
+
}
|
|
40
|
+
return out;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* One STUN/TURN server from a URL: `turn[s]://user:pass@host:port?transport=tcp` (user and
|
|
45
|
+
* password percent-encoded) becomes {urls: "turn[s]:host:port?transport=tcp", username, credential}.
|
|
46
|
+
* `stun:host:port` and `turn:host:port` without credentials are accepted as they are.
|
|
47
|
+
*/
|
|
48
|
+
export function parseIceUrl(value: string): IceServer {
|
|
49
|
+
const raw = value.trim();
|
|
50
|
+
const m = /^(stuns?|turns?):(?:\/\/)?(?:([^:@/\s]*)(?::([^@/\s]*))?@)?([^/?#@\s]+)(\?[^#]*)?$/i.exec(raw);
|
|
51
|
+
if (!m) throw new Error("A STUN/TURN server is a URL like turns://user:pass@turn.example.com:5349 or stun:stun.example.com:3478");
|
|
52
|
+
const [, scheme, user, pass, hostPort, query = ""] = m;
|
|
53
|
+
const kind = scheme!.toLowerCase();
|
|
54
|
+
if (query && !/^\?transport=(udp|tcp)$/i.test(query)) throw new Error("A STUN/TURN URL may only carry ?transport=udp or ?transport=tcp");
|
|
55
|
+
const urls = `${kind}:${hostPort}${query}`;
|
|
56
|
+
if (user === undefined) return { urls };
|
|
57
|
+
if (kind.startsWith("stun")) throw new Error("A STUN server takes no username or password");
|
|
58
|
+
let username: string, credential: string;
|
|
59
|
+
try { username = decodeURIComponent(user); credential = decodeURIComponent(pass ?? ""); }
|
|
60
|
+
catch { throw new Error("The TURN username or password has a bad %-escape"); }
|
|
61
|
+
if (!username || !credential) throw new Error("A TURN URL with credentials needs both: turns://user:pass@host:port");
|
|
62
|
+
return { urls, username, credential };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** How a STUN/TURN server is shown: the username, never the password. */
|
|
66
|
+
export function iceLabel(s: IceServer): string {
|
|
67
|
+
const url = [s.urls].flat()[0] ?? "";
|
|
68
|
+
if (!s.username) return url;
|
|
69
|
+
const [scheme, ...rest] = url.split(":");
|
|
70
|
+
return `${scheme}://${encodeURIComponent(s.username)}:…@${rest.join(":")}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function validateRtcListeners(value: unknown, socketNames: Set<string>): void {
|
|
74
|
+
if (value === undefined) return;
|
|
75
|
+
if (!Array.isArray(value)) throw new Error("listeners.json rtcListeners must be a list");
|
|
76
|
+
const names = new Set<string>();
|
|
77
|
+
for (const l of value as RtcListener[]) {
|
|
78
|
+
if (!l || l.kind !== "webrtc" || typeof l.name !== "string" || !/^[a-zA-Z0-9_-]{1,40}$/.test(l.name) || typeof l.signal !== "string")
|
|
79
|
+
throw new Error("Each WebRTC listener needs a name and a signal server URL");
|
|
80
|
+
if (l.name === DEFAULT_RTC || l.name === LEGACY_RTC || names.has(l.name) || socketNames.has(l.name))
|
|
81
|
+
throw new Error(`Listener names must be unique ("${DEFAULT_RTC}" and "${LEGACY_RTC}" are built in)`);
|
|
82
|
+
l.signal = normalizeSignal(l.signal);
|
|
83
|
+
if (l.ice !== undefined && (!Array.isArray(l.ice) || l.ice.some(s => !s || (typeof s.urls !== "string" && !Array.isArray(s.urls))
|
|
84
|
+
|| (s.username !== undefined && typeof s.username !== "string") || (s.credential !== undefined && typeof s.credential !== "string"))))
|
|
85
|
+
throw new Error(`WebRTC listener "${l.name}": ice must be a list of {urls, username?, credential?}`);
|
|
86
|
+
delete l.builtin;
|
|
87
|
+
names.add(l.name);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
13
90
|
const MAX_RETIRED_PORTS = 32;
|
|
14
91
|
export const LISTENERS_FILE = join(homedir(), ".rechrome", "listeners.json");
|
|
15
92
|
export const isLoopback = (host: string) => host === "::1" || /^127\./.test(host);
|
|
@@ -68,6 +145,7 @@ export function validateListeners(value: unknown): ListenerConfig {
|
|
|
68
145
|
if (names.has(l.name) || addresses.has(address) || keys.has(l.key)) throw new Error("Listener names, addresses, and credentials must be unique");
|
|
69
146
|
names.add(l.name); addresses.add(address); keys.add(l.key);
|
|
70
147
|
}
|
|
148
|
+
validateRtcListeners(config.rtcListeners, names);
|
|
71
149
|
if (config.retiredPorts !== undefined && (!Array.isArray(config.retiredPorts) || config.retiredPorts.some(p => !Number.isInteger(p) || p < 1 || p > 65535)))
|
|
72
150
|
throw new Error("listeners.json retiredPorts must be a list of ports");
|
|
73
151
|
return config;
|