@linxin666/dsh-remote-web-ui 0.4.1 → 0.4.2
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 +1 -1
- package/README.zh.md +1 -1
- package/lib/client.js +132 -29
- package/lib/client.js.map +1 -1
- package/lib/index.js +117 -15
- package/lib/types/client/RemoteEntry.js +1 -1
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/index.js +5 -3
- package/lib/types/client/locales.js +2 -2
- package/lib/types/client/pair-api.d.ts +4 -0
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/client/pair-api.js +11 -7
- package/lib/types/client/remote-channel.d.ts +27 -7
- package/lib/types/client/remote-channel.d.ts.map +1 -1
- package/lib/types/client/remote-channel.js +25 -17
- package/lib/types/client/update-api.js +2 -2
- package/lib/types/lan-bind.d.ts +4 -0
- package/lib/types/lan-bind.d.ts.map +1 -1
- package/lib/types/lan-bind.js +64 -1
- package/lib/types/pairing.d.ts +15 -2
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +26 -14
- package/lib/types/pairing.persist.test.js +43 -3
- package/lib/types/remote-channel-boot.d.ts.map +1 -1
- package/lib/types/remote-channel-boot.js +13 -3
- package/lib/types/remote-channel-rules.d.ts +74 -0
- package/lib/types/remote-channel-rules.d.ts.map +1 -1
- package/lib/types/remote-channel-rules.js +104 -0
- package/lib/types/update.d.ts +10 -0
- package/lib/types/update.d.ts.map +1 -1
- package/lib/types/update.js +32 -3
- package/package.json +20 -19
- package/src/client/RemoteEntry.tsx +1 -1
- package/src/client/index.ts +5 -3
- package/src/client/locales.ts +2 -2
- package/src/client/pair-api.ts +11 -7
- package/src/client/remote-channel.ts +38 -15
- package/src/client/remote.module.css +18 -0
- package/src/client/update-api.ts +2 -2
- package/src/lan-bind.ts +62 -1
- package/src/pairing.persist.test.ts +47 -3
- package/src/pairing.ts +26 -12
- package/src/remote-channel-boot.ts +13 -3
- package/src/remote-channel-rules.ts +113 -0
- package/src/update.ts +36 -3
|
@@ -11,6 +11,111 @@ export const REMOTE_PREFIX = '/remote'
|
|
|
11
11
|
|
|
12
12
|
import { REMOTE_DEVICE_HEADER, REMOTE_DEVICE_QUERY } from './remote-methods.ts'
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* The schemes that can deliver a page a remote party controls: the network
|
|
16
|
+
* transports, plus the documents a network page can mint. Every other scheme
|
|
17
|
+
* is registered and served by an application on this machine, so its page is
|
|
18
|
+
* the machine's own page whatever the application calls the scheme.
|
|
19
|
+
*
|
|
20
|
+
* The list is deliberately the *web* side rather than an allowlist of known
|
|
21
|
+
* desktop shells. The official DSH Desktop shell serves its Web GUI from
|
|
22
|
+
* `dsh-app://app/` (`location.hostname === 'app'`), which no hostname
|
|
23
|
+
* predicate can recognise; a scheme allowlist fixed that instance (#1682) but
|
|
24
|
+
* left the next shell — or a `file:` page — fenced behind a pairing page it
|
|
25
|
+
* can never complete, because a pairing link is reachable only over the
|
|
26
|
+
* network. Naming the web side instead makes an unknown application scheme
|
|
27
|
+
* local by construction.
|
|
28
|
+
*
|
|
29
|
+
* `blob:`, `data:`, `about:` and `filesystem:` stay on the web side: a
|
|
30
|
+
* network page mints those documents, so they must keep the fence.
|
|
31
|
+
*/
|
|
32
|
+
export const WEB_PAGE_PROTOCOLS: readonly string[] = [
|
|
33
|
+
'http:',
|
|
34
|
+
'https:',
|
|
35
|
+
'blob:',
|
|
36
|
+
'data:',
|
|
37
|
+
'about:',
|
|
38
|
+
'filesystem:',
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Whether one page scheme can carry a document a remote party controls.
|
|
43
|
+
* @param protocol - `location.protocol` of the page (for example `https:`).
|
|
44
|
+
* @returns true for the network transports and the documents they mint.
|
|
45
|
+
*/
|
|
46
|
+
export function isWebPageProtocol(protocol: string): boolean {
|
|
47
|
+
return WEB_PAGE_PROTOCOLS.includes(protocol)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Hostname-only loopback classification: localhost, the IPv6 loopback literal
|
|
52
|
+
* (WHATWG keeps its brackets) and any 127/8 IPv4 literal. Kept string-in,
|
|
53
|
+
* boolean-out and dependency-free so the browser half and the inlined boot
|
|
54
|
+
* script can both run it verbatim.
|
|
55
|
+
* @param hostname - a page/cookie hostname, IPv6 literals bracketed.
|
|
56
|
+
* @returns true for a loopback name or literal.
|
|
57
|
+
*/
|
|
58
|
+
export function isLoopbackHostname(hostname: string): boolean {
|
|
59
|
+
if (hostname === 'localhost' || hostname === '::1' || hostname === '[::1]') return true
|
|
60
|
+
const parts = hostname.split('.')
|
|
61
|
+
return parts.length === 4 && parts[0] === '127' && parts.every(part => /^\d{1,3}$/.test(part) && Number(part) <= 255)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Whether the current page is served from the machine running the host, so
|
|
66
|
+
* its same-origin traffic must NOT be fenced behind the pairing channel.
|
|
67
|
+
* Three independent facts all describe the page itself and any one is enough:
|
|
68
|
+
*
|
|
69
|
+
* - the page's own hostname is loopback;
|
|
70
|
+
* - the page's protocol is not a {@link WEB_PAGE_PROTOCOLS} web scheme, so an
|
|
71
|
+
* application on this machine delivered it — the desktop shell's
|
|
72
|
+
* `dsh-app://app/` is one such page, and a scheme this build has never
|
|
73
|
+
* heard of is another;
|
|
74
|
+
* - the official connection transport already declared this shell the host
|
|
75
|
+
* owner (`__DSH_TRANSPORT__.ownsHost === true`) AND the page is not a
|
|
76
|
+
* network origin. The official client reads the same hook to derive
|
|
77
|
+
* `connection.isLoopback`, and the desktop shell sets it before any boot
|
|
78
|
+
* entry — without this term the plugin's predicate contradicts the host's
|
|
79
|
+
* own conclusion (#1682). The network-origin guard is deliberate: this
|
|
80
|
+
* plugin's own device-gated landing (a LAN or tunnel page) publishes the
|
|
81
|
+
* same hook to grant a paired remote the full UI, and that page must keep
|
|
82
|
+
* riding the gated channel — `ownsHost` buys the presentation, never an
|
|
83
|
+
* exemption from the pairing fence.
|
|
84
|
+
*
|
|
85
|
+
* The order matters: loopback is checked first so a loopback page keeps its
|
|
86
|
+
* exemption even on a scheme no list carries, and the scheme test runs before
|
|
87
|
+
* the hook test so a web page always reaches the network-origin guard. A
|
|
88
|
+
* missing or empty scheme is not evidence of a local page - the fence stays
|
|
89
|
+
* up - because a real document always reports its own scheme.
|
|
90
|
+
*
|
|
91
|
+
* @param hostname - `location.hostname` of the page.
|
|
92
|
+
* @param protocol - `location.protocol` of the page (for example `https:`).
|
|
93
|
+
* @param transportOwnsHost - `__DSH_TRANSPORT__?.ownsHost` as read by the caller.
|
|
94
|
+
* @returns true when the page is local and needs no remote channel.
|
|
95
|
+
*/
|
|
96
|
+
export function isLocalPage(hostname: string, protocol?: string, transportOwnsHost?: boolean): boolean {
|
|
97
|
+
if (isLoopbackHostname(hostname)) return true
|
|
98
|
+
// A missing or empty scheme is not evidence of a local page: a real
|
|
99
|
+
// document always reports one, so the fence stays up when it is unreadable.
|
|
100
|
+
if (protocol !== undefined && protocol !== '' && !isWebPageProtocol(protocol)) return true
|
|
101
|
+
// A granted remote also sets ownsHost; only a page whose own origin is
|
|
102
|
+
// otherwise local may treat the hook as proof of a local page.
|
|
103
|
+
return transportOwnsHost === true && !isNetworkOrigin(hostname)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Whether a hostname can only be reached across a network, so a page on it is
|
|
108
|
+
* remote whatever transport hook it carries: any IPv4/IPv6 literal (a dot or a
|
|
109
|
+
* colon) or any dotted DNS name. A hostname with no dot and no colon is a
|
|
110
|
+
* scheme-local authority (the desktop shell's `app`), which is why this test,
|
|
111
|
+
* not a loopback test, is what the `ownsHost` term is gated on.
|
|
112
|
+
* @param hostname - the page hostname.
|
|
113
|
+
* @returns true when the page cannot be the machine's own local page.
|
|
114
|
+
*/
|
|
115
|
+
function isNetworkOrigin(hostname: string): boolean {
|
|
116
|
+
return hostname.includes('.') || hostname.includes(':')
|
|
117
|
+
}
|
|
118
|
+
|
|
14
119
|
export { REMOTE_DEVICE_HEADER, REMOTE_DEVICE_QUERY }
|
|
15
120
|
|
|
16
121
|
/** Connection-plugin method prefix under the gated channel. */
|
|
@@ -44,6 +149,13 @@ export interface RemoteChannelRules {
|
|
|
44
149
|
readonly uploadPath: string
|
|
45
150
|
/** Page global the pre-Cordis upload hook is published under. */
|
|
46
151
|
readonly uploadHookGlobal: string
|
|
152
|
+
/**
|
|
153
|
+
* Schemes that can carry a remotely controlled document (see
|
|
154
|
+
* WEB_PAGE_PROTOCOLS): a page on any other scheme is delivered by an
|
|
155
|
+
* application on this machine. Carried in the rules so the inlined boot
|
|
156
|
+
* script applies the same list.
|
|
157
|
+
*/
|
|
158
|
+
readonly webProtocols: readonly string[]
|
|
47
159
|
/**
|
|
48
160
|
* Page global carrying the server-issued host-mode grant. Only the plugin's
|
|
49
161
|
* own device-gated app landing (/pair-app) publishes it; the boot patch
|
|
@@ -79,6 +191,7 @@ export const REMOTE_CHANNEL_RULES: RemoteChannelRules = {
|
|
|
79
191
|
deviceQuery: REMOTE_DEVICE_QUERY,
|
|
80
192
|
uploadPath: '/api/session/uploadFileBinary',
|
|
81
193
|
uploadHookGlobal: '__DSH_FILE_UPLOAD__',
|
|
194
|
+
webProtocols: WEB_PAGE_PROTOCOLS,
|
|
82
195
|
hostGrantGlobal: REMOTE_HOST_GRANT_GLOBAL,
|
|
83
196
|
}
|
|
84
197
|
|
package/src/update.ts
CHANGED
|
@@ -307,6 +307,34 @@ function familyUpdatePackages(
|
|
|
307
307
|
return [...names]
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
+
/** Concurrent registry probes in flight for one update-status call. */
|
|
311
|
+
export const UPDATE_PROBE_CONCURRENCY = 4
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Probe every package's latest version with a bounded number of requests in
|
|
315
|
+
* flight, preserving the caller's order in the result.
|
|
316
|
+
* @param names - package names to probe.
|
|
317
|
+
* @param fetchLatest - the probe for one package (returns undefined on failure).
|
|
318
|
+
* @returns one result per name, in the input order.
|
|
319
|
+
*/
|
|
320
|
+
export async function probeLatestVersions(
|
|
321
|
+
names: readonly string[],
|
|
322
|
+
fetchLatest: (name: string) => Promise<string | undefined>,
|
|
323
|
+
): Promise<(string | undefined)[]> {
|
|
324
|
+
const results: (string | undefined)[] = new Array(names.length)
|
|
325
|
+
let next = 0
|
|
326
|
+
const worker = async (): Promise<void> => {
|
|
327
|
+
for (;;) {
|
|
328
|
+
const index = next++
|
|
329
|
+
if (index >= names.length) return
|
|
330
|
+
results[index] = await fetchLatest(names[index]!)
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
const workers = Math.min(UPDATE_PROBE_CONCURRENCY, names.length)
|
|
334
|
+
await Promise.all(Array.from({ length: workers }, () => worker()))
|
|
335
|
+
return results
|
|
336
|
+
}
|
|
337
|
+
|
|
310
338
|
/**
|
|
311
339
|
* Probe the npm registry for one package's latest release.
|
|
312
340
|
* @param name - the package name (scope slash URL-encoded).
|
|
@@ -483,9 +511,14 @@ export async function checkUpdates(deps: UpdateCheckDeps): Promise<UpdateStatus>
|
|
|
483
511
|
// aggregate) still check every installed @linxin666/* plugin (#377);
|
|
484
512
|
// familyUpdatePackages skips link:/file: development dependencies.
|
|
485
513
|
const names = familyUpdatePackages(anchor, manifest, profileManifest)
|
|
486
|
-
// The registry probes are independent
|
|
487
|
-
//
|
|
488
|
-
|
|
514
|
+
// The registry probes are independent, so they run concurrently — but with a
|
|
515
|
+
// small ceiling rather than all at once. A full install fans out ~20 probes,
|
|
516
|
+
// and on hosts behind an application-aware security middlebox that burst is
|
|
517
|
+
// what gets the node process's outbound connections throttled (the update
|
|
518
|
+
// check itself only ever loses latency: one probe is 10s-bounded and a
|
|
519
|
+
// failure is shown as a probe failure). Four keeps the status call quick
|
|
520
|
+
// while staying inside the concurrency such a middlebox tolerates (#1677).
|
|
521
|
+
const latestList = await probeLatestVersions(names, deps.fetchLatest)
|
|
489
522
|
const packages: UpdatePackageStatus[] = []
|
|
490
523
|
let probeFailures = 0
|
|
491
524
|
names.forEach((name, index) => {
|