dsh-remote-hub 0.3.1 → 0.3.3
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 +34 -2
- package/bin/dsh-remote.mjs +65 -4
- package/cordis.patch.yml +3 -0
- package/lib/index.js +1 -1
- package/lib/pairing-routes.js +7 -4
- package/lib/supervisor.js +93 -5
- package/lib/tunnel.js +9 -1
- 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.
|
|
22
|
+
**DSH Remote v4.21.0** · Android 8.0 (API 26) 及以上 · 2.8 MB
|
|
23
23
|
|
|
24
|
-
[⬇️ 下载 APK](https://github.com/liaoguogang-hub/dsh-remote-public/releases/download/v4.
|
|
24
|
+
[⬇️ 下载 APK](https://github.com/liaoguogang-hub/dsh-remote-public/releases/download/v4.21.0/DSH-Remote-v4.21.0.apk) ·
|
|
25
25
|
[全部版本](https://github.com/liaoguogang-hub/dsh-remote-public/releases)
|
|
26
26
|
|
|
27
27
|
安装后应用名「DSH Remote」,包名 `com.dsh.remote.ngrok`(可与作者的 `DSH` 自用版共存)。
|
|
@@ -235,6 +235,38 @@ dsh-remote: ready — 手机端扫码:dsh-remote qr(或浏览器打开 http:
|
|
|
235
235
|
**Cookie 跨重启有效**:桥接始终向 DSH 呈现固定 authority(默认 `127.0.0.1:1815`),
|
|
236
236
|
所以 DSH 换端口、桥接重启都不会让手机上的凭据失效;只有签名密钥变化或 Cookie 到期(30 天)才需要重新扫码。
|
|
237
237
|
|
|
238
|
+
### 升级插件(重要:必须完全重启 DSH)
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
dsh plugin --profile desktop add dsh-remote-hub # 或 --profile web
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
**升级后请完全退出 DSH 再重新打开,不要只靠插件热重载。** 原因是旧版本的桥接监听可能不会被热重载回收,
|
|
245
|
+
它会继续占着端口,而这个"旧桥接"里没有新版本的路由:
|
|
246
|
+
|
|
247
|
+
| 症状 | 含义 |
|
|
248
|
+
|---|---|
|
|
249
|
+
| 配对页打不开,或打开是**乱码/无关页面** | 端口被**旧版桥接**占着,请求被它转发走了 |
|
|
250
|
+
| `dsh-remote status` 里 `bridgePort` 是 `0`、地址是 `http://x.x.x.x:0` | 旧版本绑不上端口的回退 bug(0.3.0 及更早) |
|
|
251
|
+
|
|
252
|
+
0.3.1 起这些都能自愈,但仍建议完全重启,理由是会残留**旧代码的监听**:
|
|
253
|
+
|
|
254
|
+
- 绑不上端口时先探 `/_dsh-remote/health`:
|
|
255
|
+
- 是支持配对的本插件桥接 → 直接**采纳**,不重复开隧道;
|
|
256
|
+
- 是旧版桥接(没有配对路由)或别的程序 → 自动**改用临时端口**,并在日志/`status.json` 里写明真实端口;
|
|
257
|
+
- 插件热重载时通过进程内注册表**先关闭上一实例**的监听。
|
|
258
|
+
|
|
259
|
+
升级后确认当前真实端口:
|
|
260
|
+
|
|
261
|
+
```bash
|
|
262
|
+
dsh-remote status # 打印 bridgePort / 局域网 / 公网 / Cookie
|
|
263
|
+
# 或直接看健康检查
|
|
264
|
+
curl http://127.0.0.1:<bridgePort>/_dsh-remote/health
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
> 提示:`cordis.patch.yml` 里的 `port` 只是**首选**端口。若被占用,实际端口会写在 `~/.dsh/dsh-remote/status.json`,
|
|
268
|
+
> 一切链接与二维码都以那里为准,不必手改配置。
|
|
269
|
+
|
|
238
270
|
### 扫码配对(一次性配对码)
|
|
239
271
|
|
|
240
272
|
不想手抄 Cookie 就用二维码:
|
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] run the supervisor
|
|
8
|
+
* dsh-remote start [--open] [--public-url=URL] run the supervisor; --open 打印地址并打开配对页
|
|
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
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
import fs from 'node:fs'
|
|
17
|
+
import http from 'node:http'
|
|
17
18
|
import { execFile } from 'node:child_process'
|
|
18
19
|
import os from 'node:os'
|
|
19
20
|
import path from 'node:path'
|
|
@@ -69,8 +70,59 @@ function openBrowser(url) {
|
|
|
69
70
|
})
|
|
70
71
|
}
|
|
71
72
|
|
|
72
|
-
/**
|
|
73
|
+
/** Is a live dsh-remote bridge answering on this port? */
|
|
74
|
+
function bridgeHealthy(port, timeoutMs = 1200) {
|
|
75
|
+
return new Promise((resolve) => {
|
|
76
|
+
const req = http.request(
|
|
77
|
+
{ host: '127.0.0.1', port, path: '/_dsh-remote/health', method: 'GET', timeout: timeoutMs },
|
|
78
|
+
(res) => {
|
|
79
|
+
const chunks = []
|
|
80
|
+
res.on('data', (c) => chunks.push(c))
|
|
81
|
+
res.on('end', () => {
|
|
82
|
+
try {
|
|
83
|
+
const doc = JSON.parse(Buffer.concat(chunks).toString('utf8'))
|
|
84
|
+
resolve(doc?.ok ? doc : null)
|
|
85
|
+
} catch {
|
|
86
|
+
resolve(null)
|
|
87
|
+
}
|
|
88
|
+
})
|
|
89
|
+
},
|
|
90
|
+
)
|
|
91
|
+
req.on('error', () => resolve(null))
|
|
92
|
+
req.on('timeout', () => {
|
|
93
|
+
req.destroy()
|
|
94
|
+
resolve(null)
|
|
95
|
+
})
|
|
96
|
+
req.end()
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Point the browser at the running bridge's pairing page.
|
|
102
|
+
*
|
|
103
|
+
* The port is verified against the bridge's own health route first: status.json can be stale after
|
|
104
|
+
* a crashed run or a test on another port, and opening a dead URL looks like "the QR page is broken".
|
|
105
|
+
*/
|
|
106
|
+
async function resolveLiveBridge(preferred) {
|
|
107
|
+
const candidates = [preferred, 6300, readStatus()?.bridgePort]
|
|
108
|
+
.filter((p, i, arr) => Number.isInteger(p) && p > 0 && arr.indexOf(p) === i)
|
|
109
|
+
for (const port of candidates) {
|
|
110
|
+
// eslint-disable-next-line no-await-in-loop
|
|
111
|
+
const live = await bridgeHealthy(port)
|
|
112
|
+
if (live) return Number(live.bridgePort) || port
|
|
113
|
+
}
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
116
|
+
|
|
73
117
|
async function openPairPage(bridgePort) {
|
|
118
|
+
const livePort = await resolveLiveBridge(bridgePort)
|
|
119
|
+
if (!livePort) {
|
|
120
|
+
console.log('')
|
|
121
|
+
console.log(' 找不到正在运行的桥接:先运行 dsh-remote start,或双击 connect.cmd')
|
|
122
|
+
console.log('')
|
|
123
|
+
return
|
|
124
|
+
}
|
|
125
|
+
bridgePort = livePort
|
|
74
126
|
const page = `http://127.0.0.1:${bridgePort}/_dsh-remote/pair`
|
|
75
127
|
console.log('')
|
|
76
128
|
console.log(' 配对页:', page)
|
|
@@ -81,7 +133,13 @@ async function openPairPage(bridgePort) {
|
|
|
81
133
|
|
|
82
134
|
async function qrCommand() {
|
|
83
135
|
const status = readStatus()
|
|
84
|
-
const port = status?.bridgePort ?? Number(process.env.DSH_REMOTE_PORT || 6300)
|
|
136
|
+
const port = await resolveLiveBridge(status?.bridgePort ?? Number(process.env.DSH_REMOTE_PORT || 6300))
|
|
137
|
+
if (!port) {
|
|
138
|
+
console.log('')
|
|
139
|
+
console.log(' 桥接还没运行:先执行 dsh-remote start --open(或双击 connect.cmd)')
|
|
140
|
+
console.log('')
|
|
141
|
+
return
|
|
142
|
+
}
|
|
85
143
|
const page = `http://127.0.0.1:${port}/_dsh-remote/pair`
|
|
86
144
|
console.log('')
|
|
87
145
|
console.log(' 配对页:', page)
|
|
@@ -164,9 +222,12 @@ async function main() {
|
|
|
164
222
|
break
|
|
165
223
|
case 'start': {
|
|
166
224
|
const wantOpen = process.argv.includes('--open')
|
|
225
|
+
// --public-url=https://relay.example advertises a self-hosted relay in the QR payload.
|
|
226
|
+
const publicArg = process.argv.find((a) => a.startsWith('--public-url='))
|
|
227
|
+
const publicUrl = publicArg ? publicArg.slice('--public-url='.length) : process.env.DSH_REMOTE_PUBLIC_URL || ''
|
|
167
228
|
let handle
|
|
168
229
|
try {
|
|
169
|
-
handle = await start({ log: (m) => console.log(m) })
|
|
230
|
+
handle = await start({ publicUrl, log: (m) => console.log(m) })
|
|
170
231
|
} catch (err) {
|
|
171
232
|
// Port already taken: another bridge is serving; just show its pairing page.
|
|
172
233
|
if (/EADDRINUSE/.test(String(err?.message ?? err))) {
|
package/cordis.patch.yml
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
# port local bridge port the phone connects to on the LAN
|
|
10
10
|
# authority Host header the bridge presents; the signed cookie is bound to it
|
|
11
11
|
# tunnel "auto" (ngrok if found, else cloudflared), "off", "ngrok" or "cloudflared"
|
|
12
|
+
# publicUrl explicit public address to advertise (a self-hosted relay, e.g. the Aliyun box).
|
|
13
|
+
# When set, nothing is launched for the tunnel and this URL goes into the QR code.
|
|
12
14
|
# upstreamPort pin the DSH host port; 0 means discover it automatically
|
|
13
15
|
- insert:
|
|
14
16
|
- id: dsh-remote
|
|
@@ -18,4 +20,5 @@
|
|
|
18
20
|
port: 6300
|
|
19
21
|
authority: '127.0.0.1:1815'
|
|
20
22
|
tunnel: 'auto'
|
|
23
|
+
publicUrl: ''
|
|
21
24
|
upstreamPort: 0
|
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.3.
|
|
19
|
+
export const version = '0.3.3'
|
|
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
|
@@ -23,7 +23,10 @@ export const PAIR_PREFIX = '/_dsh-remote/pair'
|
|
|
23
23
|
* when that transport is unavailable that moment.
|
|
24
24
|
*/
|
|
25
25
|
export function buildPairPayload({ lan, publicUrl, code, expiresAt }) {
|
|
26
|
-
|
|
26
|
+
// `l` is a LIST of LAN URLs: a machine can have several private adapters and only one of them is
|
|
27
|
+
// reachable from the phone, so the client probes them in order instead of trusting our pick.
|
|
28
|
+
const lanList = Array.isArray(lan) ? lan.filter(Boolean) : lan ? [lan] : []
|
|
29
|
+
return JSON.stringify({ v: 2, l: lanList, u: publicUrl ?? null, c: code, e: expiresAt })
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
/**
|
|
@@ -74,8 +77,8 @@ export async function handlePairing({ req, res, current, store }) {
|
|
|
74
77
|
JSON.stringify({
|
|
75
78
|
ok: true,
|
|
76
79
|
// `url` keeps the old single-address shape working; new clients read lan/public.
|
|
77
|
-
url: snapshot.public ?? snapshot.lan ?? null,
|
|
78
|
-
lan: snapshot.lan
|
|
80
|
+
url: snapshot.public ?? (Array.isArray(snapshot.lan) ? snapshot.lan[0] : snapshot.lan) ?? null,
|
|
81
|
+
lan: Array.isArray(snapshot.lan) ? snapshot.lan : snapshot.lan ? [snapshot.lan] : [],
|
|
79
82
|
public: snapshot.public ?? null,
|
|
80
83
|
cookie: result.cookie,
|
|
81
84
|
issuedAt: result.issuedAt,
|
|
@@ -119,7 +122,7 @@ export async function handlePairing({ req, res, current, store }) {
|
|
|
119
122
|
<p>配对码 <code>${code}</code> · ${minutes} 分钟内有效,且只能用一次</p>
|
|
120
123
|
<div class="qr">${svg}</div>
|
|
121
124
|
<p class="ok">扫码后自动填入地址并换取登录凭据</p>
|
|
122
|
-
<p class="addr">${snapshot.lan ?
|
|
125
|
+
<p class="addr">${(Array.isArray(snapshot.lan) ? snapshot.lan : snapshot.lan ? [snapshot.lan] : []).map((u) => `局域网:${u}`).join('<br>')}${snapshot.public ? `<br>公网:${snapshot.public}` : ''}</p>
|
|
123
126
|
<p class="addr">扫一次会同时存下局域网与公网地址,之后自动优先走局域网</p>
|
|
124
127
|
</div></body></html>`
|
|
125
128
|
res.writeHead(200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-store' })
|
package/lib/supervisor.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import http from 'node:http'
|
|
13
|
+
import https from 'node:https'
|
|
13
14
|
import fs from 'node:fs'
|
|
14
15
|
import os from 'node:os'
|
|
15
16
|
import path from 'node:path'
|
|
@@ -36,6 +37,29 @@ function appendLog(line) {
|
|
|
36
37
|
} catch {}
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
/** Every private IPv4 address this machine has, best candidate first. */
|
|
41
|
+
export function lanAddresses() {
|
|
42
|
+
const nets = os.networkInterfaces()
|
|
43
|
+
const VIRTUAL = /vEthernet|WSL|Docker|Hyper-V|VMware|VirtualBox|Loopback|Tunnel|Tailscale|ZeroTier/i
|
|
44
|
+
const scored = []
|
|
45
|
+
for (const [name, addrs] of Object.entries(nets)) {
|
|
46
|
+
for (const a of addrs || []) {
|
|
47
|
+
if (a.family !== 'IPv4' || a.internal) continue
|
|
48
|
+
const isPrivate = /^192\.168\.|^10\.|^172\.(1[6-9]|2\d|3[01])\./.test(a.address)
|
|
49
|
+
if (!isPrivate) continue
|
|
50
|
+
let score = 0
|
|
51
|
+
if (/^192\.168\./.test(a.address)) score += 40
|
|
52
|
+
else if (/^10\./.test(a.address)) score += 20
|
|
53
|
+
else score += 5
|
|
54
|
+
if (/wi-?fi|wlan|WLAN|en0|Ethernet|以太网/i.test(name)) score += 10
|
|
55
|
+
if (VIRTUAL.test(name)) score -= 60
|
|
56
|
+
scored.push({ address: a.address, score })
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
scored.sort((a, b) => b.score - a.score)
|
|
60
|
+
return [...new Set(scored.filter((s2) => s2.score > 0).map((s2) => s2.address))]
|
|
61
|
+
}
|
|
62
|
+
|
|
39
63
|
/**
|
|
40
64
|
* LAN address a phone on the same network can reach.
|
|
41
65
|
*
|
|
@@ -102,6 +126,35 @@ function writeStatus(status) {
|
|
|
102
126
|
fs.writeFileSync(CONNECT_FILE, lines.join(os.EOL), 'utf8')
|
|
103
127
|
}
|
|
104
128
|
|
|
129
|
+
/** Probe a full URL (used for the configured public relay) and report whether it answers. */
|
|
130
|
+
function probeHealthUrl(url, timeoutMs = 4000) {
|
|
131
|
+
return new Promise((resolve) => {
|
|
132
|
+
let parsed
|
|
133
|
+
try {
|
|
134
|
+
parsed = new URL(url)
|
|
135
|
+
} catch {
|
|
136
|
+
return resolve(false)
|
|
137
|
+
}
|
|
138
|
+
// A relay is usually HTTPS, so pick the transport from the scheme instead of hard-coding http.
|
|
139
|
+
const transport = parsed.protocol === 'https:' ? https : http
|
|
140
|
+
let req
|
|
141
|
+
try {
|
|
142
|
+
req = transport.request(parsed, { method: 'GET', timeout: timeoutMs }, (res) => {
|
|
143
|
+
res.resume()
|
|
144
|
+
resolve(res.statusCode >= 200 && res.statusCode < 500)
|
|
145
|
+
})
|
|
146
|
+
} catch {
|
|
147
|
+
return resolve(false)
|
|
148
|
+
}
|
|
149
|
+
req.on('error', () => resolve(false))
|
|
150
|
+
req.on('timeout', () => {
|
|
151
|
+
req.destroy()
|
|
152
|
+
resolve(false)
|
|
153
|
+
})
|
|
154
|
+
req.end()
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
105
158
|
/** Ask whatever listens on `port` whether it is a dsh-remote bridge, and what it supports. */
|
|
106
159
|
function probeHealth(port, timeoutMs = 1200) {
|
|
107
160
|
return new Promise((resolve) => {
|
|
@@ -158,6 +211,7 @@ export async function start(config = {}) {
|
|
|
158
211
|
port = 6300,
|
|
159
212
|
authority = '127.0.0.1:1815',
|
|
160
213
|
tunnel: tunnelKind = 'auto',
|
|
214
|
+
publicUrl = '',
|
|
161
215
|
upstreamPort = 0,
|
|
162
216
|
domain = '',
|
|
163
217
|
log = appendLog,
|
|
@@ -201,6 +255,9 @@ export async function start(config = {}) {
|
|
|
201
255
|
let lastPublicUrl = null
|
|
202
256
|
|
|
203
257
|
/** The bridge may have fallen back to an ephemeral port; everything must report the real one. */
|
|
258
|
+
const lanUrlsFor = (bridgePort) =>
|
|
259
|
+
lanAddresses().map((address) => `http://${address}:${bridgePort}`)
|
|
260
|
+
|
|
204
261
|
let bridgeInstance = null
|
|
205
262
|
const actualBridgePort = () => (bridgeInstance ? bridgeInstance.port : Number(port) || 6300)
|
|
206
263
|
|
|
@@ -210,7 +267,7 @@ export async function start(config = {}) {
|
|
|
210
267
|
store: pairingStore,
|
|
211
268
|
// Read at redeem time so a tunnel URL that appears later is still handed to the phone.
|
|
212
269
|
current: () => ({
|
|
213
|
-
lan:
|
|
270
|
+
lan: lanUrlsFor(actualBridgePort()),
|
|
214
271
|
public: lastPublicUrl ?? null,
|
|
215
272
|
cookie,
|
|
216
273
|
}),
|
|
@@ -250,10 +307,26 @@ export async function start(config = {}) {
|
|
|
250
307
|
const existing = await probeHealth(wanted)
|
|
251
308
|
if (existing?.ok && existing.pairing) {
|
|
252
309
|
log(`dsh-remote: port ${wanted} is already served by a dsh-remote bridge; adopting it`)
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
310
|
+
// Rebuild the status from the LIVE bridge's own health report instead of trusting
|
|
311
|
+
// status.json: a stale file (from an earlier run on another port) is exactly what made
|
|
312
|
+
// connect.cmd open a dead URL.
|
|
313
|
+
const prev = readStatus() ?? {}
|
|
314
|
+
const livePort = Number(existing.bridgePort) || wanted
|
|
315
|
+
const ip = lanAddress()
|
|
316
|
+
const adopted = {
|
|
317
|
+
updatedAt: Date.now(),
|
|
318
|
+
phase: 'adopted',
|
|
319
|
+
bridgePort: livePort,
|
|
320
|
+
authority: existing.authority ?? authority,
|
|
321
|
+
upstreamPort: existing.upstreamPort ?? null,
|
|
322
|
+
lan: { ip, url: ip ? `http://${ip}:${livePort}` : null },
|
|
323
|
+
public: prev.public ?? { url: null, kind: 'none', note: 'served by an existing bridge' },
|
|
324
|
+
cookie: prev.cookie ?? null,
|
|
325
|
+
cookieExpiresAt: prev.cookieExpiresAt ?? null,
|
|
326
|
+
pairing: { url: prev.public?.url ?? (ip ? `http://${ip}:${livePort}` : null), page: '/_dsh-remote/pair' },
|
|
256
327
|
}
|
|
328
|
+
writeStatus(adopted)
|
|
329
|
+
return { status: adopted, close: async () => {} }
|
|
257
330
|
}
|
|
258
331
|
if (existing?.ok) {
|
|
259
332
|
log('dsh-remote: port is held by an old bridge WITHOUT pairing support; using another port')
|
|
@@ -292,6 +365,7 @@ export async function start(config = {}) {
|
|
|
292
365
|
|
|
293
366
|
const tunnel = await startTunnel({
|
|
294
367
|
kind: tunnelKind,
|
|
368
|
+
publicUrl,
|
|
295
369
|
bridgePort: bridge.port,
|
|
296
370
|
authority,
|
|
297
371
|
domain,
|
|
@@ -312,6 +386,15 @@ export async function start(config = {}) {
|
|
|
312
386
|
})
|
|
313
387
|
|
|
314
388
|
lastPublicUrl = tunnel.publicUrl
|
|
389
|
+
// A configured relay can be "up" while its reverse tunnel is down; probe it so status does not
|
|
390
|
+
// advertise a URL that cannot answer.
|
|
391
|
+
let publicReachable = null
|
|
392
|
+
if (publicUrl && tunnel.publicUrl) {
|
|
393
|
+
publicReachable = await probeHealthUrl(`${tunnel.publicUrl}/_dsh-remote/health`)
|
|
394
|
+
if (publicReachable === false) {
|
|
395
|
+
log(`dsh-remote: WARNING ${tunnel.publicUrl} does not answer — is the relay's SSH tunnel up?`)
|
|
396
|
+
}
|
|
397
|
+
}
|
|
315
398
|
const ip = ipForPairing
|
|
316
399
|
const status = {
|
|
317
400
|
updatedAt: Date.now(),
|
|
@@ -320,7 +403,12 @@ export async function start(config = {}) {
|
|
|
320
403
|
authority,
|
|
321
404
|
upstreamPort: discovery.port || null,
|
|
322
405
|
lan: { ip, url: ip ? `http://${ip}:${bridge.port}` : null },
|
|
323
|
-
public: {
|
|
406
|
+
public: {
|
|
407
|
+
url: tunnel.publicUrl,
|
|
408
|
+
kind: tunnel.kind,
|
|
409
|
+
...(publicReachable === null ? {} : { reachable: publicReachable }),
|
|
410
|
+
...(tunnel.note ? { note: tunnel.note } : {}),
|
|
411
|
+
},
|
|
324
412
|
cookie,
|
|
325
413
|
cookieExpiresAt: expiresAt ?? cookieExpiry(cookie),
|
|
326
414
|
pairing: {
|
package/lib/tunnel.js
CHANGED
|
@@ -148,7 +148,15 @@ 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 = '', log = () => {} }) {
|
|
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
|
+
}
|
|
152
160
|
if (kind === 'off') return { publicUrl: null, kind: 'none', close: () => {}, note: 'tunnel disabled by config' }
|
|
153
161
|
|
|
154
162
|
const wantNgrok = kind === 'auto' || kind === 'ngrok'
|
package/package.json
CHANGED