@linxin666/dsh-remote-web-ui 0.3.23 → 0.4.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.i18n.yaml +2 -2
- package/README.md +13 -10
- package/README.zh.md +13 -10
- package/icon.svg +3 -0
- package/lib/client.js +396 -132
- package/lib/client.js.map +1 -1
- package/lib/index.js +604 -262
- package/lib/types/client/FenceNotice.d.ts.map +1 -1
- package/lib/types/client/FenceNotice.js +0 -3
- package/lib/types/client/PluginSettingsCard.d.ts +4 -0
- package/lib/types/client/PluginSettingsCard.d.ts.map +1 -1
- package/lib/types/client/PluginSettingsCard.js +3 -2
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +14 -9
- package/lib/types/client/RemotePanel.d.ts.map +1 -1
- package/lib/types/client/RemotePanel.js +13 -4
- package/lib/types/client/RemoteSettingsCard.d.ts +6 -6
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +14 -8
- package/lib/types/client/UpdateEntry.js +2 -2
- package/lib/types/client/UpdatePanel.js +2 -2
- package/lib/types/client/deep-link.js +2 -2
- package/lib/types/client/index.d.ts +16 -8
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +56 -32
- package/lib/types/client/locales.d.ts +0 -2
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +6 -8
- package/lib/types/client/mobile-adapt.d.ts.map +1 -1
- package/lib/types/client/mobile-adapt.js +143 -28
- package/lib/types/client/pair-api.d.ts +32 -3
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/client/pair-api.js +44 -6
- package/lib/types/client/plugin-card-seat.d.ts +104 -0
- package/lib/types/client/plugin-card-seat.d.ts.map +1 -0
- package/lib/types/client/plugin-card-seat.js +143 -0
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +18 -5
- package/lib/types/client/settings-form.d.ts +17 -17
- package/lib/types/client/settings-form.d.ts.map +1 -1
- package/lib/types/client/settings-form.js +30 -27
- package/lib/types/console-output.d.ts +41 -0
- package/lib/types/console-output.d.ts.map +1 -0
- package/lib/types/console-output.js +89 -0
- package/lib/types/http.d.ts +18 -1
- package/lib/types/http.d.ts.map +1 -1
- package/lib/types/http.js +22 -1
- package/lib/types/index.d.ts +19 -11
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +91 -76
- package/lib/types/inner-auth.d.ts +3 -2
- package/lib/types/inner-auth.d.ts.map +1 -1
- package/lib/types/inner-auth.js +3 -2
- package/lib/types/pair-grant.d.ts +66 -0
- package/lib/types/pair-grant.d.ts.map +1 -0
- package/lib/types/pair-grant.js +95 -0
- package/lib/types/pairing.d.ts +44 -12
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +69 -20
- package/lib/types/pairing.persist.test.js +26 -0
- package/lib/types/posture.d.ts +11 -3
- package/lib/types/posture.d.ts.map +1 -1
- package/lib/types/posture.js +8 -3
- package/lib/types/remote-api.d.ts +10 -13
- package/lib/types/remote-api.d.ts.map +1 -1
- package/lib/types/remote-api.js +34 -34
- package/lib/types/remote-channel-boot.d.ts +6 -1
- package/lib/types/remote-channel-boot.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.js +16 -8
- package/lib/types/remote-channel-rules.d.ts +14 -0
- package/lib/types/remote-channel-rules.d.ts.map +1 -1
- package/lib/types/remote-channel-rules.js +8 -0
- package/lib/types/remote-methods.d.ts +1 -1
- package/lib/types/remote-methods.d.ts.map +1 -1
- package/lib/types/remote-methods.js +1 -0
- package/lib/types/routes.d.ts +47 -5
- package/lib/types/routes.d.ts.map +1 -1
- package/lib/types/routes.js +160 -58
- package/lib/types/tunnel.d.ts +21 -6
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +58 -11
- package/lib/types/update.d.ts +4 -0
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +74 -33
- package/package.json +23 -20
- package/src/client/FenceNotice.tsx +0 -2
- package/src/client/PluginSettingsCard.tsx +7 -2
- package/src/client/RemoteEntry.tsx +14 -9
- package/src/client/RemotePanel.tsx +17 -7
- package/src/client/RemoteSettingsCard.tsx +16 -9
- package/src/client/UpdateEntry.tsx +2 -2
- package/src/client/UpdatePanel.tsx +3 -3
- package/src/client/deep-link.ts +2 -2
- package/src/client/index.ts +70 -40
- package/src/client/locales.ts +6 -8
- package/src/client/mobile-adapt.ts +136 -25
- package/src/client/pair-api.ts +47 -7
- package/src/client/plugin-card-seat.ts +192 -0
- package/src/client/remote-channel.ts +19 -4
- package/src/client/remote.module.css +38 -0
- package/src/client/settings-form.ts +34 -31
- package/src/console-output.ts +97 -0
- package/src/http.ts +23 -1
- package/src/index.ts +102 -79
- package/src/inner-auth.ts +3 -2
- package/src/pair-grant.ts +131 -0
- package/src/pairing.persist.test.ts +36 -0
- package/src/pairing.ts +75 -24
- package/src/posture.ts +12 -4
- package/src/remote-api.ts +34 -41
- package/src/remote-channel-boot.ts +16 -8
- package/src/remote-channel-rules.ts +17 -0
- package/src/remote-methods.ts +1 -0
- package/src/routes.ts +165 -57
- package/src/tunnel.ts +56 -11
- package/src/update.ts +74 -29
package/lib/types/routes.js
CHANGED
|
@@ -11,12 +11,21 @@
|
|
|
11
11
|
* on the 0.1.2-alpha.2 cohort nothing emits api/gate — direct /api is
|
|
12
12
|
* governed by the harness fence + browser auth — while the /remote channel
|
|
13
13
|
* always enforces the pairing cookie itself.
|
|
14
|
+
*
|
|
15
|
+
* The app landing is reached with a one-time grant, never with the device id
|
|
16
|
+
* itself: /pair-accept mints a short-lived, single-consume grant
|
|
17
|
+
* (pair-grant.ts) and redirects to /pair-app?grant=<g>, and only the landing's
|
|
18
|
+
* own response carries the device credential into the shell. The device cookie
|
|
19
|
+
* stays the primary credential; the grant only keeps a live session credential
|
|
20
|
+
* out of the URL, the address bar, and every log on the redirect path.
|
|
14
21
|
*/
|
|
15
22
|
import { z } from 'zod';
|
|
16
23
|
import { UnknownLanAddressError } from "./pairing.js";
|
|
17
24
|
import { isLoopbackAddress } from "./loopback.js";
|
|
18
25
|
import { isLoopbackClient, readCookie } from "./gate.js";
|
|
19
26
|
import { readJsonBody, writeJson } from "./http.js";
|
|
27
|
+
import { createPairGrantStore } from "./pair-grant.js";
|
|
28
|
+
import { REMOTE_HOST_GRANT_GLOBAL } from "./remote-channel-rules.js";
|
|
20
29
|
/**
|
|
21
30
|
* Browser-trust fence for the /api/pair routes, mirroring the connection
|
|
22
31
|
* package's internal fence semantics (Host/Origin based, DNS-rebinding and
|
|
@@ -75,7 +84,17 @@ export function isTrustedHost(request, trustedHosts) {
|
|
|
75
84
|
return isLoopbackClient(request) || trustedHosts.some(entry => {
|
|
76
85
|
// A port-less entry matches the hostname on any port; an exact host:port
|
|
77
86
|
// entry matches that authority verbatim (WHATWG normalization both sides).
|
|
78
|
-
|
|
87
|
+
// A malformed entry (a config or DSH_REMOTE_TRUSTED_HOSTS typo) contributes
|
|
88
|
+
// no trust instead of throwing out of the fence: every pairing request from
|
|
89
|
+
// a non-loopback origin would otherwise answer 400 with only a generic
|
|
90
|
+
// webserver warning, and the valid entries would never be consulted.
|
|
91
|
+
let entryUrl;
|
|
92
|
+
try {
|
|
93
|
+
entryUrl = new URL(`http://${entry}`);
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
79
98
|
return entryUrl.port === '' ? entryUrl.hostname === hostname : entryUrl.host === hostUrl.host;
|
|
80
99
|
});
|
|
81
100
|
}
|
|
@@ -183,19 +202,31 @@ function privateLanHostOf(request, navigation = false) {
|
|
|
183
202
|
const MAX_BODY_BYTES = 4096;
|
|
184
203
|
/**
|
|
185
204
|
* The rate-limit bucket key for one accept attempt (pure; unit-tested).
|
|
186
|
-
*
|
|
187
|
-
*
|
|
188
|
-
*
|
|
205
|
+
* A forwarded hop separates buckets behind the auto-tunnel (every internet
|
|
206
|
+
* client arrives from 127.0.0.1 there) — but only for loopback peers, since a
|
|
207
|
+
* direct LAN client can rotate the header freely. The caller passes the hop it
|
|
208
|
+
* trusts (the edge-appended LAST one; see rateLimitAccept), and the hop is
|
|
209
|
+
* truncated so one oversized header cannot mint a huge key.
|
|
189
210
|
* @param socketIp - the socket peer address.
|
|
190
|
-
* @param forwarded - the
|
|
211
|
+
* @param forwarded - the trusted XFF hop, already trimmed, if any.
|
|
191
212
|
* @param bucket - page (GET /pair-accept) vs api (POST /api/pair/accept).
|
|
192
213
|
*/
|
|
193
214
|
export function acceptLimitKey(socketIp, forwarded, bucket) {
|
|
194
215
|
if (isLoopbackAddress(socketIp) && forwarded !== undefined && forwarded !== '') {
|
|
195
|
-
return `${bucket}|${socketIp}|${forwarded}`;
|
|
216
|
+
return `${bucket}|${socketIp}|${forwarded.slice(0, MAX_FORWARDED_KEY_CHARS)}`;
|
|
196
217
|
}
|
|
197
218
|
return `${bucket}|${socketIp}`;
|
|
198
219
|
}
|
|
220
|
+
/** Longest forwarded hop kept in a bucket key (the header is caller-sized). */
|
|
221
|
+
const MAX_FORWARDED_KEY_CHARS = 64;
|
|
222
|
+
/**
|
|
223
|
+
* Hard cap on live accept buckets. The key is caller-influenced (a forwarded
|
|
224
|
+
* hop), so a rotating flood must not grow the table without bound: past the
|
|
225
|
+
* cap the oldest bucket is evicted, the same FIFO stance as
|
|
226
|
+
* {@link addBounded}. A legitimate client that loses its bucket merely starts
|
|
227
|
+
* a fresh window.
|
|
228
|
+
*/
|
|
229
|
+
export const MAX_ACCEPT_BUCKETS = 1024;
|
|
199
230
|
/**
|
|
200
231
|
* Cap on the dynamically trusted hosts (see {@link addBounded}): room for
|
|
201
232
|
* every legitimate router/proxy aliasing shape, small enough that a flood of
|
|
@@ -277,17 +308,25 @@ const COOKIE_MAX_AGE_SEC = 365 * 24 * 60 * 60;
|
|
|
277
308
|
export const APP_DEVICE_STORAGE_KEY = 'dsh-remote-device';
|
|
278
309
|
export function appShellCaptureScript(deviceId) {
|
|
279
310
|
const safeId = JSON.stringify(deviceId);
|
|
311
|
+
const grantGlobal = JSON.stringify(REMOTE_HOST_GRANT_GLOBAL);
|
|
280
312
|
const register = `try{if('serviceWorker' in navigator){navigator.serviceWorker.register(${JSON.stringify(PAIR_PATHS.appServiceWorker)},{scope:'/'}).catch(function(e){})}}catch(e){}`;
|
|
281
|
-
return `<script>(function(){try{sessionStorage.setItem(${JSON.stringify(APP_DEVICE_STORAGE_KEY)},${safeId});
|
|
313
|
+
return `<script>(function(){try{sessionStorage.setItem(${JSON.stringify(APP_DEVICE_STORAGE_KEY)},${safeId});}catch(e){}try{history.replaceState(null,'','/')}catch(e){}try{window[${grantGlobal}]=true}catch(e){}${register}})()</script>`;
|
|
282
314
|
}
|
|
283
|
-
/**
|
|
315
|
+
/**
|
|
316
|
+
* Patch the official index document with the device-capture script. The script
|
|
317
|
+
* goes immediately after the opening <head> tag, ahead of the harness-injected
|
|
318
|
+
* parse-time channel boot patch: that patch installs the transport host-mode
|
|
319
|
+
* hook only when this server-served marker is already set (see
|
|
320
|
+
* remote-channel-boot.ts), so host mode is granted by the device-gated landing
|
|
321
|
+
* instead of being asserted from the origin.
|
|
322
|
+
*/
|
|
284
323
|
export function patchAppShell(html, deviceId) {
|
|
285
324
|
const script = appShellCaptureScript(deviceId);
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
if (at === -1)
|
|
325
|
+
const head = /<head[^>]*>/i.exec(html);
|
|
326
|
+
if (head === null)
|
|
289
327
|
return script + html;
|
|
290
|
-
|
|
328
|
+
const end = head.index + head[0].length;
|
|
329
|
+
return html.slice(0, end) + script + html.slice(end);
|
|
291
330
|
}
|
|
292
331
|
/**
|
|
293
332
|
* The reopen service worker served at PAIR_PATHS.appServiceWorker. A paired
|
|
@@ -488,6 +527,7 @@ export function makeRoutes(deps) {
|
|
|
488
527
|
const pairingRequired = () => typeof requirePairingForLan === 'function'
|
|
489
528
|
? requirePairingForLan()
|
|
490
529
|
: requirePairingForLan;
|
|
530
|
+
const grants = deps.grants ?? createPairGrantStore();
|
|
491
531
|
const events = new PairingEventsStream(service);
|
|
492
532
|
const dynamicTrustedHosts = new Set();
|
|
493
533
|
/** Loopback-only fence: the desktop panel's control endpoints. */
|
|
@@ -517,11 +557,18 @@ export function makeRoutes(deps) {
|
|
|
517
557
|
if (privateLanHost !== undefined) {
|
|
518
558
|
const cookieDeviceId = readCookie(req.headers.cookie, service.config.cookieName);
|
|
519
559
|
let queryDeviceId = null;
|
|
560
|
+
let queryGrant;
|
|
520
561
|
try {
|
|
521
|
-
|
|
562
|
+
const params = new URL(req.url ?? '/', 'http://pair.invalid').searchParams;
|
|
563
|
+
queryDeviceId = params.get('device');
|
|
564
|
+
queryGrant = params.get('grant') ?? undefined;
|
|
522
565
|
}
|
|
523
566
|
catch { }
|
|
524
|
-
|
|
567
|
+
// The app landing carries a one-time grant instead of the device id;
|
|
568
|
+
// peek (never spend) so the fence can trust a container/NAT authority
|
|
569
|
+
// whose Host is not among the advertised ones.
|
|
570
|
+
const granted = grants.peek(queryGrant);
|
|
571
|
+
const deviceId = cookieDeviceId ?? queryDeviceId ?? granted ?? undefined;
|
|
525
572
|
if (deviceId !== undefined && service.hasDevice(deviceId)) {
|
|
526
573
|
addBounded(dynamicTrustedHosts, privateLanHost, MAX_DYNAMIC_TRUSTED_HOSTS);
|
|
527
574
|
return true;
|
|
@@ -545,27 +592,50 @@ export function makeRoutes(deps) {
|
|
|
545
592
|
* count separately: a QR re-scan (page navigation) must not consume a
|
|
546
593
|
* brute-force budget that belongs to token guessing (and vice versa).
|
|
547
594
|
*/
|
|
595
|
+
/**
|
|
596
|
+
* Drop expired buckets from the FRONT of the insertion-ordered table, then
|
|
597
|
+
* FIFO-evict past the hard cap. The front always holds the oldest window
|
|
598
|
+
* because a re-armed bucket is re-inserted at the tail, so this is amortized
|
|
599
|
+
* O(1) per request: every iteration deletes an entry that was inserted once.
|
|
600
|
+
* It replaces a full-table scan that ran on every pairing request once the
|
|
601
|
+
* table passed 256 entries (and deleted nothing while all windows were
|
|
602
|
+
* fresh).
|
|
603
|
+
*/
|
|
604
|
+
const pruneAcceptAttempts = (nowMs) => {
|
|
605
|
+
while (acceptAttempts.size > 0) {
|
|
606
|
+
const oldest = acceptAttempts.keys().next().value;
|
|
607
|
+
if (oldest === undefined)
|
|
608
|
+
break;
|
|
609
|
+
const attempt = acceptAttempts.get(oldest);
|
|
610
|
+
if (attempt !== undefined && nowMs - attempt.windowStart <= ACCEPT_WINDOW_MS)
|
|
611
|
+
break;
|
|
612
|
+
acceptAttempts.delete(oldest);
|
|
613
|
+
}
|
|
614
|
+
while (acceptAttempts.size > MAX_ACCEPT_BUCKETS) {
|
|
615
|
+
const oldest = acceptAttempts.keys().next().value;
|
|
616
|
+
if (oldest === undefined)
|
|
617
|
+
break;
|
|
618
|
+
acceptAttempts.delete(oldest);
|
|
619
|
+
}
|
|
620
|
+
};
|
|
548
621
|
const rateLimitAccept = (req, bucket) => {
|
|
549
622
|
const socketIp = req.socket?.remoteAddress ?? 'unknown';
|
|
550
623
|
// XFF is honored only for loopback peers (the tunnel edge); see
|
|
551
624
|
// acceptLimitKey. It is untrusted for authentication and never grants
|
|
552
|
-
// access.
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
625
|
+
// access. Only the LAST hop is used: an edge APPENDS the address it saw, so
|
|
626
|
+
// any earlier hop is whatever the client sent — keying on the first one let
|
|
627
|
+
// an attacker rotate the header for a fresh brute-force budget per request.
|
|
628
|
+
const hops = typeof req.headers['x-forwarded-for'] === 'string'
|
|
629
|
+
? req.headers['x-forwarded-for'].split(',')
|
|
630
|
+
: [];
|
|
631
|
+
const forwarded = (hops[hops.length - 1] ?? '').trim();
|
|
556
632
|
const ip = acceptLimitKey(socketIp, forwarded, bucket);
|
|
557
633
|
const nowMs = Date.now();
|
|
558
|
-
|
|
559
|
-
// table grows past a modest size so distinct source IPs (LAN clients,
|
|
560
|
-
// brute-force scans) cannot accumulate forever.
|
|
561
|
-
if (acceptAttempts.size > 256) {
|
|
562
|
-
for (const [key, attempt] of acceptAttempts) {
|
|
563
|
-
if (nowMs - attempt.windowStart > ACCEPT_WINDOW_MS)
|
|
564
|
-
acceptAttempts.delete(key);
|
|
565
|
-
}
|
|
566
|
-
}
|
|
634
|
+
pruneAcceptAttempts(nowMs);
|
|
567
635
|
const entry = acceptAttempts.get(ip);
|
|
568
636
|
if (entry === undefined || nowMs - entry.windowStart > ACCEPT_WINDOW_MS) {
|
|
637
|
+
// Re-insert at the tail so insertion order stays window order.
|
|
638
|
+
acceptAttempts.delete(ip);
|
|
569
639
|
acceptAttempts.set(ip, { count: 1, windowStart: nowMs });
|
|
570
640
|
return false;
|
|
571
641
|
}
|
|
@@ -646,19 +716,18 @@ export function makeRoutes(deps) {
|
|
|
646
716
|
writeJson(res, 403, { ok: false, code: 'forbidden' });
|
|
647
717
|
return;
|
|
648
718
|
}
|
|
649
|
-
writeJson(res,
|
|
719
|
+
writeJson(res, 404, { ok: false, code: result.code });
|
|
650
720
|
return;
|
|
651
721
|
}
|
|
652
722
|
if (privateLanHost !== undefined) {
|
|
653
723
|
addBounded(dynamicTrustedHosts, privateLanHost, MAX_DYNAMIC_TRUSTED_HOSTS);
|
|
654
724
|
}
|
|
655
|
-
//
|
|
656
|
-
//
|
|
657
|
-
//
|
|
725
|
+
// The Secure attribute follows the transport (deviceCookie): off on
|
|
726
|
+
// plain-HTTP LAN, where a Secure cookie is dropped by the browser, and on
|
|
727
|
+
// for a tunneled request the edge stamped as https. Lax keeps top-level
|
|
728
|
+
// navigations working while blocking cross-site subrequests.
|
|
658
729
|
writeJson(res, 200, { ok: true, deviceId: result.deviceId }, {
|
|
659
|
-
'set-cookie': [
|
|
660
|
-
`${service.config.cookieName}=${result.deviceId}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${String(COOKIE_MAX_AGE_SEC)}`,
|
|
661
|
-
],
|
|
730
|
+
'set-cookie': [deviceCookie(req, service.config.cookieName, result.deviceId)],
|
|
662
731
|
});
|
|
663
732
|
};
|
|
664
733
|
const handleStop = async (req, res) => {
|
|
@@ -724,6 +793,11 @@ export function makeRoutes(deps) {
|
|
|
724
793
|
}
|
|
725
794
|
const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
|
|
726
795
|
const paired = deviceId !== undefined && service.hasDevice(deviceId);
|
|
796
|
+
// The desktop (loopback) keeps the full pairing-relevant view; the LAN IP
|
|
797
|
+
// literals stay behind a live pairing or loopback so an unauthenticated
|
|
798
|
+
// caller on a tunnel cannot map the internal network. A phone already knows
|
|
799
|
+
// its usable literal from the URL it opened.
|
|
800
|
+
const local = isLoopbackClient(req);
|
|
727
801
|
const snapshot = service.snapshot();
|
|
728
802
|
// Unpaired LAN/tunnel clients get only the pairing-relevant fields; the
|
|
729
803
|
// token expiry, public tunnel URL, and counts are an oracle for targeting
|
|
@@ -731,9 +805,9 @@ export function makeRoutes(deps) {
|
|
|
731
805
|
// session credentials) is never returned here — only the loopback events
|
|
732
806
|
// stream carries it to the desktop panel.
|
|
733
807
|
const { devices: _devices, ...rest } = snapshot;
|
|
734
|
-
const visible = paired
|
|
808
|
+
const visible = paired || local
|
|
735
809
|
? rest
|
|
736
|
-
: { phase: snapshot.phase, lanAvailable: snapshot.lanAvailable
|
|
810
|
+
: { phase: snapshot.phase, lanAvailable: snapshot.lanAvailable };
|
|
737
811
|
writeJson(res, 200, { ok: true, paired, requirePairingForLan: pairingRequired(), ...visible });
|
|
738
812
|
};
|
|
739
813
|
const handleEvents = (req, res) => {
|
|
@@ -800,9 +874,10 @@ export function makeRoutes(deps) {
|
|
|
800
874
|
// instead of the harness 401 dead end.
|
|
801
875
|
const deviceId = readCookie(req.headers.cookie, service.config.cookieName);
|
|
802
876
|
if (deviceId !== undefined && service.hasDevice(deviceId)) {
|
|
803
|
-
// An already-paired device re-opening a dead link goes straight to
|
|
804
|
-
//
|
|
805
|
-
|
|
877
|
+
// An already-paired device re-opening a dead link goes straight to the
|
|
878
|
+
// cookieless app landing, again on a fresh one-time grant.
|
|
879
|
+
const { grant } = grants.issue(deviceId);
|
|
880
|
+
res.writeHead(303, { location: `${appOrigin(req)}/pair-app?grant=${encodeURIComponent(grant)}`, 'cache-control': 'no-store', 'referrer-policy': 'no-referrer' });
|
|
806
881
|
res.end();
|
|
807
882
|
return;
|
|
808
883
|
}
|
|
@@ -814,24 +889,27 @@ export function makeRoutes(deps) {
|
|
|
814
889
|
addBounded(dynamicTrustedHosts, privateLanHost, MAX_DYNAMIC_TRUSTED_HOSTS);
|
|
815
890
|
}
|
|
816
891
|
// Land the paired device on the cookieless app page: the official shell
|
|
817
|
-
// served by this plugin
|
|
818
|
-
//
|
|
819
|
-
//
|
|
892
|
+
// served by this plugin. The URL carries a one-time grant, never the
|
|
893
|
+
// device id - a session credential must not travel in a URL (history, the
|
|
894
|
+
// address bar, every log on the redirect path) - and the landing's own
|
|
895
|
+
// response hands the device id to the shell. No harness index gate, no
|
|
896
|
+
// browser-auth cookie hop: the channel gate accepts the device credential
|
|
897
|
+
// whether or not the browser stores cookies.
|
|
898
|
+
const { grant } = grants.issue(result.deviceId);
|
|
820
899
|
res.writeHead(303, {
|
|
821
|
-
location: `${appOrigin(req)}/pair-app?
|
|
900
|
+
location: `${appOrigin(req)}/pair-app?grant=${encodeURIComponent(grant)}`,
|
|
822
901
|
'cache-control': 'no-store',
|
|
823
902
|
'referrer-policy': 'no-referrer',
|
|
824
|
-
'set-cookie': [
|
|
825
|
-
`${service.config.cookieName}=${result.deviceId}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${String(COOKIE_MAX_AGE_SEC)}`,
|
|
826
|
-
],
|
|
903
|
+
'set-cookie': [deviceCookie(req, service.config.cookieName, result.deviceId)],
|
|
827
904
|
});
|
|
828
905
|
res.end();
|
|
829
906
|
};
|
|
830
907
|
/**
|
|
831
|
-
* The cookieless app landing. A paired device (
|
|
832
|
-
* pairing cookie) receives the official
|
|
833
|
-
* device-capture script; the shell itself is
|
|
834
|
-
* carries no data, so serving it needs only the
|
|
908
|
+
* The cookieless app landing. A paired device (a one-time grant minted by
|
|
909
|
+
* /pair-accept, or a live pairing cookie on a reopen) receives the official
|
|
910
|
+
* index shell patched with the device-capture script; the shell itself is
|
|
911
|
+
* the official document and carries no data, so serving it needs only the
|
|
912
|
+
* device credential.
|
|
835
913
|
*/
|
|
836
914
|
const handleAppPage = async (req, res) => {
|
|
837
915
|
if (!requireMethod(req, res, 'GET'))
|
|
@@ -848,10 +926,14 @@ export function makeRoutes(deps) {
|
|
|
848
926
|
return;
|
|
849
927
|
}
|
|
850
928
|
const url = new URL(req.url ?? '/', 'http://pair.invalid');
|
|
851
|
-
|
|
929
|
+
// The one-time grant is spent before anything else: a replayed or expired
|
|
930
|
+
// grant resolves to nothing (pair-grant.ts), so a leaked landing URL can
|
|
931
|
+
// never be re-run. The cookie stays the primary credential for a reopen
|
|
932
|
+
// (the service worker's network-first navigation carries it).
|
|
933
|
+
const grantedDevice = grants.consume(url.searchParams.get('grant') ?? undefined);
|
|
852
934
|
const cookieDevice = readCookie(req.headers.cookie, service.config.cookieName);
|
|
853
|
-
const id = (
|
|
854
|
-
?
|
|
935
|
+
const id = (grantedDevice !== undefined && service.touchDevice(grantedDevice))
|
|
936
|
+
? grantedDevice
|
|
855
937
|
: (cookieDevice !== undefined && service.touchDevice(cookieDevice))
|
|
856
938
|
? cookieDevice
|
|
857
939
|
: undefined;
|
|
@@ -870,9 +952,7 @@ export function makeRoutes(deps) {
|
|
|
870
952
|
'content-type': 'text/html; charset=utf-8',
|
|
871
953
|
'cache-control': 'no-store',
|
|
872
954
|
'referrer-policy': 'no-referrer',
|
|
873
|
-
'set-cookie': [
|
|
874
|
-
`${service.config.cookieName}=${id}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${String(COOKIE_MAX_AGE_SEC)}`,
|
|
875
|
-
],
|
|
955
|
+
'set-cookie': [deviceCookie(req, service.config.cookieName, id)],
|
|
876
956
|
});
|
|
877
957
|
res.end(patchAppShell(html, id));
|
|
878
958
|
};
|
|
@@ -921,9 +1001,31 @@ export function makeRoutes(deps) {
|
|
|
921
1001
|
}
|
|
922
1002
|
return routes;
|
|
923
1003
|
}
|
|
1004
|
+
/**
|
|
1005
|
+
* Whether the request reached this process over TLS. The harness itself serves
|
|
1006
|
+
* plain HTTP; a tunnel edge terminates TLS and stamps `x-forwarded-proto`
|
|
1007
|
+
* (the same signal {@link appOrigin} trusts when it rebuilds a redirect).
|
|
1008
|
+
*/
|
|
1009
|
+
export function isHttpsRequest(req) {
|
|
1010
|
+
const forwarded = req.headers['x-forwarded-proto'];
|
|
1011
|
+
if (typeof forwarded === 'string') {
|
|
1012
|
+
const first = forwarded.split(',')[0]?.trim().toLowerCase();
|
|
1013
|
+
if (first === 'https')
|
|
1014
|
+
return true;
|
|
1015
|
+
}
|
|
1016
|
+
return req.socket.encrypted === true;
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* The device cookie for one request. `Secure` is added only when the request
|
|
1020
|
+
* arrived over TLS: LAN pairing runs on plain HTTP, where a Secure cookie is
|
|
1021
|
+
* simply dropped by the browser and the phone would silently lose its session.
|
|
1022
|
+
*/
|
|
1023
|
+
export function deviceCookie(req, cookieName, deviceId) {
|
|
1024
|
+
const secure = isHttpsRequest(req) ? '; Secure' : '';
|
|
1025
|
+
return `${cookieName}=${deviceId}; Path=/; HttpOnly; SameSite=Lax; Max-Age=${String(COOKIE_MAX_AGE_SEC)}${secure}`;
|
|
1026
|
+
}
|
|
924
1027
|
/** The request origin (https when a tunnel edge says so). */
|
|
925
1028
|
function appOrigin(req) {
|
|
926
|
-
const proto = req.headers['x-forwarded-proto'] === 'https' ? 'https' : 'http';
|
|
927
1029
|
const origin = `http://${req.headers.host ?? '127.0.0.1'}`;
|
|
928
|
-
return
|
|
1030
|
+
return isHttpsRequest(req) ? origin.replace('http://', 'https://') : origin;
|
|
929
1031
|
}
|
package/lib/types/tunnel.d.ts
CHANGED
|
@@ -107,6 +107,8 @@ export interface BinaryReadinessSeams {
|
|
|
107
107
|
* the reinstall), and re-fetch the current platform binary otherwise.
|
|
108
108
|
*/
|
|
109
109
|
export declare function createBinaryReadiness(executable: string, seams?: BinaryReadinessSeams): () => Promise<void>;
|
|
110
|
+
/** How many archive downloads one process may attempt before giving up. */
|
|
111
|
+
export declare const MAX_BINARY_INSTALL_ATTEMPTS = 3;
|
|
110
112
|
/**
|
|
111
113
|
* Spawn flags for one quick target. `originHostHeader` stamps the Host the
|
|
112
114
|
* local webserver sees: the plugin's phone-facing fence trusts the configured
|
|
@@ -117,13 +119,26 @@ export declare function createBinaryReadiness(executable: string, seams?: Binary
|
|
|
117
119
|
*/
|
|
118
120
|
export declare function quickTunnelFlags(originHostHeader?: string): Record<string, string | boolean>;
|
|
119
121
|
/**
|
|
120
|
-
* Command-line arguments for a named tunnel
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
122
|
+
* Command-line arguments for a named tunnel. The account token is deliberately
|
|
123
|
+
* NOT part of the argv: `--token <t>` would expose an account credential to any
|
|
124
|
+
* local user through ps/proc for the whole tunnel lifetime, while the flag's own
|
|
125
|
+
* environment variable (TUNNEL_TOKEN, see cloudflared's tunnel run flags) is not
|
|
126
|
+
* readable that way. Note: `--no-autoupdate` and `--protocol` are flags for the
|
|
127
|
+
* `tunnel` command and must appear BEFORE the `run` subcommand. Putting them
|
|
128
|
+
* after `run` causes cloudflared to exit immediately with "flag provided but not
|
|
129
|
+
* defined: -no-autoupdate" (issues #1432, #1433).
|
|
125
130
|
*/
|
|
126
|
-
export declare function namedTunnelArgs(
|
|
131
|
+
export declare function namedTunnelArgs(): string[];
|
|
132
|
+
/**
|
|
133
|
+
* Run one spawn call with TUNNEL_TOKEN set, restoring the previous value
|
|
134
|
+
* afterwards. The cloudflared package's Tunnel spawns with the inherited
|
|
135
|
+
* environment inside its constructor, so the variable only has to exist for
|
|
136
|
+
* that call; the child keeps its own copy of the environment.
|
|
137
|
+
* @param token - the named-tunnel token.
|
|
138
|
+
* @param spawnTunnel - the call that creates the child process.
|
|
139
|
+
* @returns whatever the spawn call returns.
|
|
140
|
+
*/
|
|
141
|
+
export declare function withTunnelTokenEnv<T>(token: string, spawnTunnel: () => T): T;
|
|
127
142
|
/**
|
|
128
143
|
* Own the lifecycle of one auto-tunnel: start/stop, URL surfacing, and
|
|
129
144
|
* crash-restart backoff.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["../../src/tunnel.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"tunnel.d.ts","sourceRoot":"","sources":["../../src/tunnel.ts"],"names":[],"mappings":"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"}
|
package/lib/types/tunnel.js
CHANGED
|
@@ -96,20 +96,38 @@ export function createBinaryReadiness(executable, seams = {}) {
|
|
|
96
96
|
const runs = seams.runs ?? binaryRuns;
|
|
97
97
|
const installBinary = seams.install ?? install;
|
|
98
98
|
const validated = new Set();
|
|
99
|
+
let installs = 0;
|
|
100
|
+
let lastFailure;
|
|
99
101
|
return async () => {
|
|
100
102
|
if (validated.has(executable))
|
|
101
103
|
return;
|
|
104
|
+
// The manager retries forever with backoff, so a permanently unrunnable
|
|
105
|
+
// binary must not re-download the whole platform archive on every attempt
|
|
106
|
+
// (a wrong-arch payload would otherwise fetch it once per restart round).
|
|
107
|
+
// A transient install failure still gets its retries; past the cap the
|
|
108
|
+
// remembered error is rethrown until the process restarts.
|
|
109
|
+
if (installs >= MAX_BINARY_INSTALL_ATTEMPTS && lastFailure !== undefined)
|
|
110
|
+
throw lastFailure;
|
|
102
111
|
if (exists(executable) && await runs(executable)) {
|
|
103
112
|
validated.add(executable);
|
|
104
113
|
return;
|
|
105
114
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
115
|
+
installs += 1;
|
|
116
|
+
try {
|
|
117
|
+
await installBinary(executable);
|
|
118
|
+
if (!(await runs(executable))) {
|
|
119
|
+
throw new Error(`the cloudflared binary at ${executable} still does not run after the reinstall`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
lastFailure = error instanceof Error ? error : new Error(String(error));
|
|
124
|
+
throw lastFailure;
|
|
109
125
|
}
|
|
110
126
|
validated.add(executable);
|
|
111
127
|
};
|
|
112
128
|
}
|
|
129
|
+
/** How many archive downloads one process may attempt before giving up. */
|
|
130
|
+
export const MAX_BINARY_INSTALL_ATTEMPTS = 3;
|
|
113
131
|
/** The readiness policy the manager uses by default: the package's real binary path. */
|
|
114
132
|
const defaultBinaryReadiness = createBinaryReadiness(bin);
|
|
115
133
|
/** Default binary readiness: keep a working platform binary at `bin`. */
|
|
@@ -132,14 +150,43 @@ export function quickTunnelFlags(originHostHeader) {
|
|
|
132
150
|
: { ...SHARED_TUNNEL_FLAGS, '--http-host-header': originHostHeader };
|
|
133
151
|
}
|
|
134
152
|
/**
|
|
135
|
-
* Command-line arguments for a named tunnel
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
153
|
+
* Command-line arguments for a named tunnel. The account token is deliberately
|
|
154
|
+
* NOT part of the argv: `--token <t>` would expose an account credential to any
|
|
155
|
+
* local user through ps/proc for the whole tunnel lifetime, while the flag's own
|
|
156
|
+
* environment variable (TUNNEL_TOKEN, see cloudflared's tunnel run flags) is not
|
|
157
|
+
* readable that way. Note: `--no-autoupdate` and `--protocol` are flags for the
|
|
158
|
+
* `tunnel` command and must appear BEFORE the `run` subcommand. Putting them
|
|
159
|
+
* after `run` causes cloudflared to exit immediately with "flag provided but not
|
|
160
|
+
* defined: -no-autoupdate" (issues #1432, #1433).
|
|
161
|
+
*/
|
|
162
|
+
export function namedTunnelArgs() {
|
|
163
|
+
return ['tunnel', '--no-autoupdate', '--protocol', 'http2', 'run'];
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Run one spawn call with TUNNEL_TOKEN set, restoring the previous value
|
|
167
|
+
* afterwards. The cloudflared package's Tunnel spawns with the inherited
|
|
168
|
+
* environment inside its constructor, so the variable only has to exist for
|
|
169
|
+
* that call; the child keeps its own copy of the environment.
|
|
170
|
+
* @param token - the named-tunnel token.
|
|
171
|
+
* @param spawnTunnel - the call that creates the child process.
|
|
172
|
+
* @returns whatever the spawn call returns.
|
|
140
173
|
*/
|
|
141
|
-
export function
|
|
142
|
-
|
|
174
|
+
export function withTunnelTokenEnv(token, spawnTunnel) {
|
|
175
|
+
const previous = process.env.TUNNEL_TOKEN;
|
|
176
|
+
process.env.TUNNEL_TOKEN = token;
|
|
177
|
+
try {
|
|
178
|
+
return spawnTunnel();
|
|
179
|
+
}
|
|
180
|
+
finally {
|
|
181
|
+
if (previous === undefined)
|
|
182
|
+
delete process.env.TUNNEL_TOKEN;
|
|
183
|
+
else
|
|
184
|
+
process.env.TUNNEL_TOKEN = previous;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** Spawn one named tunnel without putting its token in the child's argv. */
|
|
188
|
+
function spawnNamedTunnel(token) {
|
|
189
|
+
return withTunnelTokenEnv(token, () => new Tunnel(namedTunnelArgs()));
|
|
143
190
|
}
|
|
144
191
|
/** Default factory: the cloudflared package's quick and named tunnels. */
|
|
145
192
|
function defaultFactory(target) {
|
|
@@ -154,7 +201,7 @@ function defaultFactory(target) {
|
|
|
154
201
|
if (target.kind === 'quick') {
|
|
155
202
|
return Tunnel.quick(target.targetUrl, quickTunnelFlags(target.originHostHeader));
|
|
156
203
|
}
|
|
157
|
-
return namedTunnelHandle(
|
|
204
|
+
return namedTunnelHandle(spawnNamedTunnel(target.token), target.publicUrl);
|
|
158
205
|
}
|
|
159
206
|
/** Node timers. */
|
|
160
207
|
const nodeTimer = { setTimeout, clearTimeout };
|
package/lib/types/update.d.ts
CHANGED
|
@@ -219,7 +219,11 @@ export interface UpdateRunDeps {
|
|
|
219
219
|
timeoutMs?: number;
|
|
220
220
|
/** Platform override (defaults to process.platform; test seam). */
|
|
221
221
|
platform?: NodeJS.Platform;
|
|
222
|
+
/** Process-group kill seam (defaults to process.kill; test seam). */
|
|
223
|
+
killImpl?: KillImpl;
|
|
222
224
|
}
|
|
225
|
+
/** Seam for the process-group kill (tests inject a recorder). */
|
|
226
|
+
export type KillImpl = (pid: number, signal: NodeJS.Signals) => void;
|
|
223
227
|
/**
|
|
224
228
|
* Run the update inside the profile directory. Tries pnpm first, falls back
|
|
225
229
|
* to corepack and then npx when the previous command is missing (ENOENT);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/update.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAA;
|
|
1
|
+
{"version":3,"file":"update.d.ts","sourceRoot":"","sources":["../../src/update.ts"],"names":[],"mappings":"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"}
|