@linxin666/dsh-remote-web-ui 0.3.11 → 0.3.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/README.i18n.yaml +2 -2
- package/README.md +24 -4
- package/README.zh.md +24 -4
- package/lib/client.js +77 -8
- package/lib/client.js.map +1 -1
- package/lib/index.js +409 -44
- package/lib/types/client/RemoteEntry.d.ts.map +1 -1
- package/lib/types/client/RemoteEntry.js +1 -0
- package/lib/types/client/RemotePanel.d.ts +3 -1
- package/lib/types/client/RemotePanel.d.ts.map +1 -1
- package/lib/types/client/RemotePanel.js +1 -1
- package/lib/types/client/RemoteSettingsCard.d.ts +16 -0
- package/lib/types/client/RemoteSettingsCard.d.ts.map +1 -1
- package/lib/types/client/RemoteSettingsCard.js +6 -2
- package/lib/types/client/locales.d.ts +12 -0
- package/lib/types/client/locales.d.ts.map +1 -1
- package/lib/types/client/locales.js +16 -4
- package/lib/types/client/pair-api.d.ts +10 -0
- package/lib/types/client/pair-api.d.ts.map +1 -1
- package/lib/types/index.d.ts +28 -2
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/index.js +138 -23
- package/lib/types/pairing.d.ts +17 -0
- package/lib/types/pairing.d.ts.map +1 -1
- package/lib/types/pairing.js +13 -0
- package/lib/types/relay-registry.d.ts +97 -0
- package/lib/types/relay-registry.d.ts.map +1 -0
- package/lib/types/relay-registry.js +191 -0
- package/lib/types/tunnel-plan.d.ts +36 -0
- package/lib/types/tunnel-plan.d.ts.map +1 -0
- package/lib/types/tunnel-plan.js +43 -0
- package/lib/types/tunnel.d.ts +66 -13
- package/lib/types/tunnel.d.ts.map +1 -1
- package/lib/types/tunnel.js +93 -21
- package/package.json +18 -18
- package/src/client/RemoteEntry.tsx +2 -1
- package/src/client/RemotePanel.tsx +11 -0
- package/src/client/RemoteSettingsCard.tsx +43 -1
- package/src/client/locales.ts +16 -4
- package/src/client/pair-api.ts +11 -0
- package/src/client/settings-card.module.css +14 -8
- package/src/index.ts +153 -25
- package/src/pairing.ts +30 -0
- package/src/relay-registry.ts +232 -0
- package/src/tunnel-plan.ts +51 -0
- package/src/tunnel.ts +120 -24
package/src/tunnel.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Auto-tunnel manager: spawns a Cloudflare
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* Auto-tunnel manager: spawns a Cloudflare tunnel through the `cloudflared`
|
|
3
|
+
* npm package — its postinstall downloads the platform binary, so no
|
|
4
|
+
* user-side tooling is involved — surfaces the public URL, and restarts the
|
|
5
|
+
* process after unexpected exits with exponential backoff. Two modes: the
|
|
6
|
+
* accountless quick tunnel (`https://xxx.trycloudflare.com`, hostname minted
|
|
7
|
+
* per start) and an account named tunnel (`cloudflared tunnel run --token`,
|
|
8
|
+
* fixed public hostname — the mode that lets a paired phone keep its
|
|
9
|
+
* bookmark and pairing cookie across restarts).
|
|
7
10
|
*
|
|
8
11
|
* The cloudflared package's Tunnel is a thin spawn wrapper; this manager
|
|
9
12
|
* owns the lifecycle policy (binary readiness, URL timeout, restart
|
|
@@ -13,11 +16,79 @@
|
|
|
13
16
|
*/
|
|
14
17
|
|
|
15
18
|
import { existsSync } from 'node:fs'
|
|
19
|
+
import { EventEmitter } from 'node:events'
|
|
16
20
|
import { bin, install, Tunnel } from 'cloudflared'
|
|
17
21
|
|
|
18
22
|
/** The observable tunnel lifecycle the settings/panel surfaces render. */
|
|
19
23
|
export type TunnelPhase = 'stopped' | 'starting' | 'running' | 'failed'
|
|
20
24
|
|
|
25
|
+
/**
|
|
26
|
+
* What the manager should run. `quick` is the accountless trycloudflare
|
|
27
|
+
* tunnel whose hostname is minted per start (and dies with the process);
|
|
28
|
+
* `named` is an account tunnel whose public hostname is fixed — the point
|
|
29
|
+
* of the named mode is that a paired phone keeps its bookmark and its
|
|
30
|
+
* pairing cookie across restarts.
|
|
31
|
+
*/
|
|
32
|
+
export type TunnelTarget =
|
|
33
|
+
| { kind: 'quick'; targetUrl: string; originHostHeader?: string }
|
|
34
|
+
| { kind: 'named'; token: string; publicUrl: string }
|
|
35
|
+
|
|
36
|
+
/** Compare two targets for the start idempotence check. */
|
|
37
|
+
function sameTarget(left: TunnelTarget | undefined, right: TunnelTarget): boolean {
|
|
38
|
+
if (left === undefined) return false
|
|
39
|
+
return JSON.stringify(left) === JSON.stringify(right)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The event face the named adapter wraps (a cloudflared package Tunnel). */
|
|
43
|
+
interface NamedTunnelProcess {
|
|
44
|
+
on(event: 'connected', listener: () => void): unknown
|
|
45
|
+
on(event: 'exit', listener: (code: number | null, signal: NodeJS.Signals | null) => void): unknown
|
|
46
|
+
on(event: 'error', listener: (value: Error) => void): unknown
|
|
47
|
+
off(event: string, listener: (...args: any[]) => void): unknown
|
|
48
|
+
stop(): boolean
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Wrap a named-tunnel process so it fits the quick-tunnel handle shape: the
|
|
53
|
+
* fixed public URL is reported through the same `url` event once the first
|
|
54
|
+
* edge connection registers (cloudflared fires `connected` per connection,
|
|
55
|
+
* so only the first is taken), and exit/error pass through. The manager's
|
|
56
|
+
* URL timeout, crash-restart backoff, and stop semantics then stay fully
|
|
57
|
+
* mode-agnostic.
|
|
58
|
+
* @param inner - the running named-tunnel process.
|
|
59
|
+
* @param publicUrl - the fixed public hostname ingress maps to this server.
|
|
60
|
+
* @returns a handle emitting `url` once the tunnel is reachable.
|
|
61
|
+
*/
|
|
62
|
+
export function namedTunnelHandle(inner: NamedTunnelProcess, publicUrl: string): TunnelHandle {
|
|
63
|
+
const handle = new EventEmitter() as unknown as TunnelHandle & {
|
|
64
|
+
emit(event: string, ...args: any[]): boolean
|
|
65
|
+
}
|
|
66
|
+
let registered = false
|
|
67
|
+
const onConnected = (): void => {
|
|
68
|
+
// cloudflared fires `connected` per edge connection (four on a healthy
|
|
69
|
+
// registration); the fixed URL only needs reporting once.
|
|
70
|
+
if (registered) return
|
|
71
|
+
registered = true
|
|
72
|
+
handle.emit('url', publicUrl)
|
|
73
|
+
}
|
|
74
|
+
const onExit = (code: number | null, signal: NodeJS.Signals | null): void => {
|
|
75
|
+
handle.emit('exit', code, signal)
|
|
76
|
+
}
|
|
77
|
+
const onError = (value: Error): void => {
|
|
78
|
+
handle.emit('error', value)
|
|
79
|
+
}
|
|
80
|
+
inner.on('connected', onConnected)
|
|
81
|
+
inner.on('exit', onExit)
|
|
82
|
+
inner.on('error', onError)
|
|
83
|
+
handle.stop = (): boolean => {
|
|
84
|
+
inner.off('connected', onConnected)
|
|
85
|
+
inner.off('exit', onExit)
|
|
86
|
+
inner.off('error', onError)
|
|
87
|
+
return inner.stop()
|
|
88
|
+
}
|
|
89
|
+
return handle
|
|
90
|
+
}
|
|
91
|
+
|
|
21
92
|
/** One tunnel status frame. */
|
|
22
93
|
export interface TunnelInfo {
|
|
23
94
|
phase: TunnelPhase
|
|
@@ -36,8 +107,8 @@ export interface TunnelHandle {
|
|
|
36
107
|
|
|
37
108
|
/** Injectable seams (defaults are the real cloudflared package + node timers). */
|
|
38
109
|
export interface TunnelManagerOptions {
|
|
39
|
-
/** Spawn one
|
|
40
|
-
factory?: (
|
|
110
|
+
/** Spawn one tunnel process for the target (quick or named). */
|
|
111
|
+
factory?: (target: TunnelTarget) => TunnelHandle
|
|
41
112
|
/** Make sure the cloudflared binary exists, downloading it when absent. */
|
|
42
113
|
ensureBinary?: () => Promise<void>
|
|
43
114
|
/** Wait up to this long for the tunnel URL before failing the attempt. */
|
|
@@ -56,8 +127,25 @@ async function defaultEnsureBinary(): Promise<void> {
|
|
|
56
127
|
await install(bin)
|
|
57
128
|
}
|
|
58
129
|
|
|
59
|
-
/**
|
|
60
|
-
|
|
130
|
+
/** The flags every tunnel mode shares (see the quick factory comment). */
|
|
131
|
+
const SHARED_TUNNEL_FLAGS = { '--no-autoupdate': true, '--protocol': 'http2' } as const
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Spawn flags for one quick target. `originHostHeader` stamps the Host the
|
|
135
|
+
* local webserver sees: the plugin's phone-facing fence trusts the configured
|
|
136
|
+
* public host (the stable relay origin, Host-bound) and the harness
|
|
137
|
+
* browser-auth binds cookies to the Host authority, while the Workers relay
|
|
138
|
+
* cannot control the origin-side Host at all (fetch forces it to the URL
|
|
139
|
+
* authority) — so the connector must restamp it with the stable origin.
|
|
140
|
+
*/
|
|
141
|
+
export function quickTunnelFlags(originHostHeader?: string): Record<string, string | boolean> {
|
|
142
|
+
return originHostHeader === undefined
|
|
143
|
+
? { ...SHARED_TUNNEL_FLAGS }
|
|
144
|
+
: { ...SHARED_TUNNEL_FLAGS, '--http-host-header': originHostHeader }
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Default factory: the cloudflared package's quick and named tunnels. */
|
|
148
|
+
function defaultFactory(target: TunnelTarget): TunnelHandle {
|
|
61
149
|
// `--no-autoupdate`: the binary must never upgrade itself out from under
|
|
62
150
|
// the manager (a self-updated binary would break the pinned lifecycle).
|
|
63
151
|
// `--protocol http2`: the default `auto` prefers QUIC (UDP 7844), which
|
|
@@ -66,7 +154,10 @@ function defaultFactory(targetUrl: string): TunnelHandle {
|
|
|
66
154
|
// and the auto fallback never fires on a half-open QUIC handshake. http2
|
|
67
155
|
// rides TCP through the same paths reliably (verified live: auto stuck at
|
|
68
156
|
// ready 0 on two fresh registrations; http2 ready immediately).
|
|
69
|
-
|
|
157
|
+
if (target.kind === 'quick') {
|
|
158
|
+
return Tunnel.quick(target.targetUrl, quickTunnelFlags(target.originHostHeader))
|
|
159
|
+
}
|
|
160
|
+
return namedTunnelHandle(Tunnel.withToken(target.token, { ...SHARED_TUNNEL_FLAGS }), target.publicUrl)
|
|
70
161
|
}
|
|
71
162
|
|
|
72
163
|
/** Node timers. */
|
|
@@ -77,7 +168,7 @@ const nodeTimer = { setTimeout, clearTimeout }
|
|
|
77
168
|
* crash-restart backoff.
|
|
78
169
|
*/
|
|
79
170
|
export class TunnelManager {
|
|
80
|
-
private readonly factory: (
|
|
171
|
+
private readonly factory: (target: TunnelTarget) => TunnelHandle
|
|
81
172
|
private readonly ensureBinary: () => Promise<void>
|
|
82
173
|
private readonly urlTimeoutMs: number
|
|
83
174
|
private readonly restartBaseMs: number
|
|
@@ -87,7 +178,7 @@ export class TunnelManager {
|
|
|
87
178
|
private phase: TunnelPhase = 'stopped'
|
|
88
179
|
private url: string | undefined
|
|
89
180
|
private error: string | undefined
|
|
90
|
-
private
|
|
181
|
+
private target: TunnelTarget | undefined
|
|
91
182
|
private handle: TunnelHandle | undefined
|
|
92
183
|
private urlTimer: unknown | undefined
|
|
93
184
|
private restartTimer: unknown | undefined
|
|
@@ -121,16 +212,21 @@ export class TunnelManager {
|
|
|
121
212
|
}
|
|
122
213
|
|
|
123
214
|
/**
|
|
124
|
-
* Start (or keep) a
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
215
|
+
* Start (or keep) a tunnel toward `target`. Restarting with a different
|
|
216
|
+
* target tears the old tunnel down first; restarting with the same target
|
|
217
|
+
* while running is a no-op. A string is the quick mode's local target URL.
|
|
218
|
+
*
|
|
219
|
+
* In quick mode the URL surfaces when cloudflared mints the ephemeral
|
|
220
|
+
* trycloudflare hostname; in named mode the fixed `publicUrl` surfaces
|
|
221
|
+
* once the first edge connection registers (see {@link namedTunnelHandle}).
|
|
222
|
+
* @param target - what to run (a string means quick toward that local URL).
|
|
128
223
|
*/
|
|
129
|
-
start(
|
|
130
|
-
|
|
224
|
+
start(target: TunnelTarget | string): void {
|
|
225
|
+
const resolved: TunnelTarget = typeof target === 'string' ? { kind: 'quick', targetUrl: target } : target
|
|
226
|
+
if (sameTarget(this.target, resolved) && (this.phase === 'starting' || this.phase === 'running')) return
|
|
131
227
|
this.teardown()
|
|
132
228
|
this.stopping = false
|
|
133
|
-
this.
|
|
229
|
+
this.target = resolved
|
|
134
230
|
this.attempts = 0
|
|
135
231
|
this.generation += 1
|
|
136
232
|
this.attempt()
|
|
@@ -140,7 +236,7 @@ export class TunnelManager {
|
|
|
140
236
|
stop(): void {
|
|
141
237
|
this.teardown()
|
|
142
238
|
this.stopping = false
|
|
143
|
-
this.
|
|
239
|
+
this.target = undefined
|
|
144
240
|
this.setPhase('stopped')
|
|
145
241
|
}
|
|
146
242
|
|
|
@@ -162,15 +258,15 @@ export class TunnelManager {
|
|
|
162
258
|
}
|
|
163
259
|
|
|
164
260
|
private attempt(): void {
|
|
165
|
-
if (this.stopping || this.
|
|
261
|
+
if (this.stopping || this.target === undefined) return
|
|
166
262
|
const gen = this.generation
|
|
167
263
|
this.setPhase('starting')
|
|
168
264
|
this.handle = undefined
|
|
169
265
|
this.url = undefined
|
|
170
266
|
this.error = undefined
|
|
171
267
|
void this.ensureBinary().then(() => {
|
|
172
|
-
if (this.stopping || this.
|
|
173
|
-
const handle = this.factory(this.
|
|
268
|
+
if (this.stopping || this.target === undefined || gen !== this.generation) return
|
|
269
|
+
const handle = this.factory(this.target)
|
|
174
270
|
this.handle = handle
|
|
175
271
|
this.urlTimer = this.timer.setTimeout(() => {
|
|
176
272
|
// The tunnel never reported a URL: kill it and retry with backoff.
|
|
@@ -192,7 +288,7 @@ export class TunnelManager {
|
|
|
192
288
|
})
|
|
193
289
|
}).catch((value: unknown) => {
|
|
194
290
|
// Binary install failed (no network, no platform build): report it.
|
|
195
|
-
if (this.stopping || this.
|
|
291
|
+
if (this.stopping || this.target === undefined || gen !== this.generation) return
|
|
196
292
|
const message = value instanceof Error ? value.message : String(value)
|
|
197
293
|
this.fail(`could not obtain the cloudflared binary: ${message}`)
|
|
198
294
|
})
|