dsh-remote-hub 0.3.4 → 0.4.1
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 +2 -2
- package/bin/dsh-remote.mjs +18 -1
- package/lib/bridge.js +5 -1
- package/lib/index.js +1 -1
- package/lib/pairing-routes.js +13 -9
- package/lib/supervisor.js +25 -17
- package/lib/tunnel.js +1 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,9 +19,9 @@ Kotlin/Compose 直连 DSH 自带的 HTTP API + WebSocket 网关。
|
|
|
19
19
|
|
|
20
20
|
## 下载手机端 App
|
|
21
21
|
|
|
22
|
-
**DSH Remote v4.21.
|
|
22
|
+
**DSH Remote v4.21.2** · Android 8.0 (API 26) 及以上 · 2.8 MB
|
|
23
23
|
|
|
24
|
-
[⬇️ 下载 APK](https://github.com/liaoguogang-hub/dsh-remote-public/releases/download/v4.21.
|
|
24
|
+
[⬇️ 下载 APK](https://github.com/liaoguogang-hub/dsh-remote-public/releases/download/v4.21.2/DSH-Remote-v4.21.2.apk) ·
|
|
25
25
|
[全部版本](https://github.com/liaoguogang-hub/dsh-remote-public/releases)
|
|
26
26
|
|
|
27
27
|
安装后应用名「DSH Remote」,包名 `com.dsh.remote.ngrok`(可与作者的 `DSH` 自用版共存)。
|
package/bin/dsh-remote.mjs
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* dsh-remote status show LAN URL, public URL, cookie and upstream port
|
|
6
6
|
* dsh-remote connect print the two lines to paste into the phone
|
|
7
7
|
* dsh-remote cookie print only the cookie
|
|
8
|
-
* dsh-remote start [--open] [--public-url=URL]
|
|
8
|
+
* dsh-remote start [--open] [--restart] [--public-url=URL] --restart 会先停掉配置不同的旧桥接
|
|
9
9
|
* dsh-remote stop ask a foreground `start` to stop (best effort, via status file)
|
|
10
10
|
* dsh-remote doctor check every prerequisite in one pass
|
|
11
11
|
* dsh-remote qr open the pairing page (QR) in the default browser
|
|
@@ -260,9 +260,26 @@ async function main() {
|
|
|
260
260
|
break
|
|
261
261
|
case 'start': {
|
|
262
262
|
const wantOpen = process.argv.includes('--open')
|
|
263
|
+
const wantRestart = process.argv.includes('--restart')
|
|
263
264
|
// --public-url=https://relay.example advertises a self-hosted relay in the QR payload.
|
|
264
265
|
const publicArg = process.argv.find((a) => a.startsWith('--public-url='))
|
|
265
266
|
const publicUrl = publicArg ? publicArg.slice('--public-url='.length) : process.env.DSH_REMOTE_PUBLIC_URL || ''
|
|
267
|
+
// A launcher that carries its own transport config must not silently adopt a bridge started
|
|
268
|
+
// with different config (that is how a public launcher ended up advertising a self-use relay).
|
|
269
|
+
if (wantRestart) {
|
|
270
|
+
const live = await bridgeHealthy(Number(process.env.DSH_REMOTE_PORT || 6300))
|
|
271
|
+
const mismatch = live && (live.relay || null) !== (publicUrl || null)
|
|
272
|
+
if (mismatch && live.pid) {
|
|
273
|
+
console.log(`已有桥接配置不同(relay=${live.relay ?? '无'}),先停止它(PID ${live.pid})再按当前配置启动`)
|
|
274
|
+
try {
|
|
275
|
+
process.kill(Number(live.pid))
|
|
276
|
+
} catch (err) {
|
|
277
|
+
console.log(`停止失败:${err.message}`)
|
|
278
|
+
}
|
|
279
|
+
await new Promise((r) => setTimeout(r, 1200))
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
266
283
|
let handle
|
|
267
284
|
try {
|
|
268
285
|
handle = await start({ publicUrl, log: (m) => console.log(m) })
|
package/lib/bridge.js
CHANGED
|
@@ -68,7 +68,7 @@ function trimSessionList(body) {
|
|
|
68
68
|
* @param {(msg: string) => void} [options.log]
|
|
69
69
|
* @returns {Promise<{close: () => Promise<void>, port: number}>}
|
|
70
70
|
*/
|
|
71
|
-
export function startBridge({ listenPort, authority, upstream, pairing = null, log = () => {} }) {
|
|
71
|
+
export function startBridge({ listenPort, authority, upstream, pairing = null, relayUrl = null, log = () => {} }) {
|
|
72
72
|
const forward = (req, res, attempt = 0) =>
|
|
73
73
|
new Promise((resolve) => {
|
|
74
74
|
upstream.resolve(attempt > 0).then((port) => {
|
|
@@ -155,6 +155,10 @@ export function startBridge({ listenPort, authority, upstream, pairing = null, l
|
|
|
155
155
|
const body = JSON.stringify({
|
|
156
156
|
ok: true,
|
|
157
157
|
service: 'dsh-remote-bridge',
|
|
158
|
+
pid: process.pid,
|
|
159
|
+
// Whether this bridge advertises a self-hosted relay; a launcher that expects none must not
|
|
160
|
+
// adopt a bridge that has one, or the public app would show the self-use relay.
|
|
161
|
+
relay: relayUrl || null,
|
|
158
162
|
// A bridge built before pairing existed has no such route; callers use this flag to decide
|
|
159
163
|
// whether an already-listening port can be adopted or must be bypassed.
|
|
160
164
|
pairing: pairing != null,
|
package/lib/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import { start } from './supervisor.js'
|
|
|
16
16
|
|
|
17
17
|
export const name = 'dsh-remote-hub'
|
|
18
18
|
|
|
19
|
-
export const version = '0.
|
|
19
|
+
export const version = '0.4.1'
|
|
20
20
|
|
|
21
21
|
/** No host services are required: the plugin only needs Node builtins. */
|
|
22
22
|
export const inject = []
|
package/lib/pairing-routes.js
CHANGED
|
@@ -22,11 +22,13 @@ export const PAIR_PREFIX = '/_dsh-remote/pair'
|
|
|
22
22
|
* fastest one at connect time: `l` = LAN (private address), `u` = public tunnel. Either may be null
|
|
23
23
|
* when that transport is unavailable that moment.
|
|
24
24
|
*/
|
|
25
|
-
export function buildPairPayload({ lan,
|
|
26
|
-
//
|
|
27
|
-
//
|
|
25
|
+
export function buildPairPayload({ lan, relay, tunnel, code, expiresAt }) {
|
|
26
|
+
// v3 ships all three transports so one scan can fill every tab:
|
|
27
|
+
// l = LAN addresses (list; only one adapter may be reachable from the phone)
|
|
28
|
+
// r = self-hosted relay (Aliyun/Caddy + reverse SSH)
|
|
29
|
+
// u = auto tunnel (ngrok / cloudflared)
|
|
28
30
|
const lanList = Array.isArray(lan) ? lan.filter(Boolean) : lan ? [lan] : []
|
|
29
|
-
return JSON.stringify({ v:
|
|
31
|
+
return JSON.stringify({ v: 3, l: lanList, r: relay ?? null, u: tunnel ?? null, c: code, e: expiresAt })
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
/**
|
|
@@ -76,10 +78,12 @@ export async function handlePairing({ req, res, current, store }) {
|
|
|
76
78
|
res.end(
|
|
77
79
|
JSON.stringify({
|
|
78
80
|
ok: true,
|
|
79
|
-
// `url`
|
|
80
|
-
url: snapshot.
|
|
81
|
+
// `url`/`public` keep the old single-address shape working; new clients read the three.
|
|
82
|
+
url: snapshot.relay ?? snapshot.tunnel ?? null,
|
|
81
83
|
lan: Array.isArray(snapshot.lan) ? snapshot.lan : snapshot.lan ? [snapshot.lan] : [],
|
|
82
|
-
|
|
84
|
+
relay: snapshot.relay ?? null,
|
|
85
|
+
tunnel: snapshot.tunnel ?? null,
|
|
86
|
+
public: snapshot.relay ?? snapshot.tunnel ?? null,
|
|
83
87
|
cookie: result.cookie,
|
|
84
88
|
issuedAt: result.issuedAt,
|
|
85
89
|
}),
|
|
@@ -89,7 +93,7 @@ export async function handlePairing({ req, res, current, store }) {
|
|
|
89
93
|
|
|
90
94
|
// Any other GET under the prefix hands out a fresh code.
|
|
91
95
|
const { code, expiresAt, ttlMs } = store.issue(snapshot.cookie)
|
|
92
|
-
const payload = buildPairPayload({ lan: snapshot.lan,
|
|
96
|
+
const payload = buildPairPayload({ lan: snapshot.lan, relay: snapshot.relay, tunnel: snapshot.tunnel, code, expiresAt })
|
|
93
97
|
|
|
94
98
|
if (url === PAIR_PREFIX + '/svg') {
|
|
95
99
|
const svg = await QRCode.toString(payload, { type: 'svg', margin: 1, width: 320, errorCorrectionLevel: 'M' })
|
|
@@ -122,7 +126,7 @@ export async function handlePairing({ req, res, current, store }) {
|
|
|
122
126
|
<p>配对码 <code>${code}</code> · ${minutes} 分钟内有效,且只能用一次</p>
|
|
123
127
|
<div class="qr">${svg}</div>
|
|
124
128
|
<p class="ok">扫码后自动填入地址并换取登录凭据</p>
|
|
125
|
-
<p class="addr">${(Array.isArray(snapshot.lan) ? snapshot.lan : snapshot.lan ? [snapshot.lan] : []).map((u) => `局域网:${u}`).join('<br>')}${snapshot.
|
|
129
|
+
<p class="addr">${(Array.isArray(snapshot.lan) ? snapshot.lan : snapshot.lan ? [snapshot.lan] : []).map((u) => `局域网:${u}`).join('<br>')}${snapshot.relay ? `<br>中转:${snapshot.relay}` : ''}${snapshot.tunnel ? `<br>隧道:${snapshot.tunnel}` : ''}</p>
|
|
126
130
|
<p class="addr">扫一次会同时存下局域网与公网地址,之后自动优先走局域网</p>
|
|
127
131
|
</div></body></html>`
|
|
128
132
|
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' })
|
package/lib/supervisor.js
CHANGED
|
@@ -274,6 +274,10 @@ export async function start(config = {}) {
|
|
|
274
274
|
const lanUrlsFor = (bridgePort) =>
|
|
275
275
|
lanAddresses().map((address) => `http://${address}:${bridgePort}`)
|
|
276
276
|
|
|
277
|
+
// The self-hosted relay (if configured) and the auto tunnel are reported SEPARATELY: a self-use
|
|
278
|
+
// setup wants both (relay for 4G, ngrok as a second path), and the phone decides which to use.
|
|
279
|
+
const relayUrl = String(publicUrl || '').trim().replace(/\/+$/, '') || null
|
|
280
|
+
|
|
277
281
|
let bridgeInstance = null
|
|
278
282
|
const actualBridgePort = () => (bridgeInstance ? bridgeInstance.port : Number(port) || 6300)
|
|
279
283
|
|
|
@@ -284,7 +288,8 @@ export async function start(config = {}) {
|
|
|
284
288
|
// Read at redeem time so a tunnel URL that appears later is still handed to the phone.
|
|
285
289
|
current: () => ({
|
|
286
290
|
lan: lanUrlsFor(actualBridgePort()),
|
|
287
|
-
|
|
291
|
+
relay: relayUrl,
|
|
292
|
+
tunnel: tunnelRef?.publicUrl ?? null,
|
|
288
293
|
cookie,
|
|
289
294
|
}),
|
|
290
295
|
}
|
|
@@ -311,6 +316,7 @@ export async function start(config = {}) {
|
|
|
311
316
|
authority,
|
|
312
317
|
upstream: discovery,
|
|
313
318
|
pairing,
|
|
319
|
+
relayUrl,
|
|
314
320
|
log,
|
|
315
321
|
})
|
|
316
322
|
} catch (err) {
|
|
@@ -336,7 +342,13 @@ export async function start(config = {}) {
|
|
|
336
342
|
authority: existing.authority ?? authority,
|
|
337
343
|
upstreamPort: existing.upstreamPort ?? null,
|
|
338
344
|
lan: { ip, url: ip ? `http://${ip}:${livePort}` : null },
|
|
339
|
-
|
|
345
|
+
// Never inherit the previous run's public address: it may describe a different transport
|
|
346
|
+
// (e.g. a self-use relay) than the launcher that is running now.
|
|
347
|
+
public: existing.relay
|
|
348
|
+
? { url: existing.relay, kind: 'relay' }
|
|
349
|
+
: { url: null, kind: 'none', note: 'served by an existing bridge' },
|
|
350
|
+
relay: existing.relay ? { url: existing.relay } : null,
|
|
351
|
+
tunnel: null,
|
|
340
352
|
cookie: prev.cookie ?? null,
|
|
341
353
|
cookieExpiresAt: prev.cookieExpiresAt ?? null,
|
|
342
354
|
pairing: { url: prev.public?.url ?? (ip ? `http://${ip}:${livePort}` : null), page: '/_dsh-remote/pair' },
|
|
@@ -385,7 +397,6 @@ export async function start(config = {}) {
|
|
|
385
397
|
|
|
386
398
|
const tunnel = await startTunnel({
|
|
387
399
|
kind: tunnelKind,
|
|
388
|
-
publicUrl,
|
|
389
400
|
bridgePort: bridge.port,
|
|
390
401
|
authority,
|
|
391
402
|
domain,
|
|
@@ -405,14 +416,12 @@ export async function start(config = {}) {
|
|
|
405
416
|
},
|
|
406
417
|
})
|
|
407
418
|
|
|
408
|
-
lastPublicUrl = tunnel.publicUrl
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if (publicReachable === false) {
|
|
415
|
-
log(`dsh-remote: WARNING ${tunnel.publicUrl} does not answer — is the relay's SSH tunnel up?`)
|
|
419
|
+
lastPublicUrl = relayUrl ?? tunnel.publicUrl
|
|
420
|
+
let relayReachable = null
|
|
421
|
+
if (relayUrl) {
|
|
422
|
+
relayReachable = await probeHealthUrl(`${relayUrl}/_dsh-remote/health`)
|
|
423
|
+
if (relayReachable === false) {
|
|
424
|
+
log(`dsh-remote: WARNING ${relayUrl} does not answer — is the relay's SSH tunnel up?`)
|
|
416
425
|
}
|
|
417
426
|
}
|
|
418
427
|
const ip = ipForPairing
|
|
@@ -423,12 +432,11 @@ export async function start(config = {}) {
|
|
|
423
432
|
authority,
|
|
424
433
|
upstreamPort: discovery.port || null,
|
|
425
434
|
lan: { ip, url: ip ? `http://${ip}:${bridge.port}` : null },
|
|
426
|
-
public:
|
|
427
|
-
url:
|
|
428
|
-
kind: tunnel.kind,
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
},
|
|
435
|
+
public: relayUrl
|
|
436
|
+
? { url: relayUrl, kind: 'relay', ...(relayReachable === null ? {} : { reachable: relayReachable }) }
|
|
437
|
+
: { url: tunnel.publicUrl, kind: tunnel.kind, ...(tunnel.note ? { note: tunnel.note } : {}) },
|
|
438
|
+
relay: relayUrl ? { url: relayUrl, ...(relayReachable === null ? {} : { reachable: relayReachable }) } : null,
|
|
439
|
+
tunnel: tunnel.publicUrl ? { url: tunnel.publicUrl, kind: tunnel.kind, ...(tunnel.note ? { note: tunnel.note } : {}) } : null,
|
|
432
440
|
cookie,
|
|
433
441
|
cookieExpiresAt: expiresAt ?? cookieExpiry(cookie),
|
|
434
442
|
pairing: {
|
package/lib/tunnel.js
CHANGED
|
@@ -148,16 +148,7 @@ export async function ensureCloudflared({ log = () => {} } = {}) {
|
|
|
148
148
|
* @param {(msg: string) => void} [options.log]
|
|
149
149
|
* @returns {Promise<{publicUrl: string|null, kind: string, close: () => void, note?: string}>}
|
|
150
150
|
*/
|
|
151
|
-
export async function startTunnel({ kind = 'auto', bridgePort, authority, domain = '', publicUrl = '', log = () => {} }) {
|
|
152
|
-
// An explicitly configured public URL (a self-hosted relay, e.g. the Aliyun box) wins over any
|
|
153
|
-
// detected tunnel: for that setup there is nothing to launch here, the relay is kept alive by its
|
|
154
|
-
// own reverse SSH tunnel.
|
|
155
|
-
if (publicUrl) {
|
|
156
|
-
const trimmed = String(publicUrl).trim().replace(/\/+$/, '')
|
|
157
|
-
log(`using the configured public URL: ${trimmed}`)
|
|
158
|
-
return { publicUrl: trimmed, kind: 'relay', close: () => {} }
|
|
159
|
-
}
|
|
160
|
-
if (kind === 'off') return { publicUrl: null, kind: 'none', close: () => {}, note: 'tunnel disabled by config' }
|
|
151
|
+
export async function startTunnel({ kind = 'auto', bridgePort, authority, domain = '', publicUrl = '', log = () => {} }) { if (kind === 'off') return { publicUrl: null, kind: 'none', close: () => {}, note: 'tunnel disabled by config' }
|
|
161
152
|
|
|
162
153
|
const wantNgrok = kind === 'auto' || kind === 'ngrok'
|
|
163
154
|
const wantCloudflared = kind === 'auto' || kind === 'cloudflared'
|
package/package.json
CHANGED