@feiyang666/dsh-mobile-remote 1.9.0 → 2.0.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/CHANGELOG.md +45 -0
- package/README.md +3 -2
- package/README.zh.md +6 -8
- package/RELEASE_NOTES.md +36 -43
- package/cordis.patch.yml +4 -1
- package/lib/client.js +1096 -1059
- package/lib/external.js +97 -8
- package/lib/index.js +143 -956
- package/lib/lanproxy.js +40 -10
- package/lib/password.js +245 -245
- package/package.json +2 -2
- package/lib/patch.js.bak-0000 +0 -244
package/lib/external.js
CHANGED
|
@@ -124,9 +124,10 @@ const POOL_COUNT_RE = /^\s*(transport\.)?poolCount\s*=.*(\r?\n|$)/gm
|
|
|
124
124
|
* 连接复用 tcpMux 自动管理;服务端可能仍在下发该字段,本地一律剥离,
|
|
125
125
|
* 否则 frpc 解析配置失败、启动即退出)。
|
|
126
126
|
* @param {string} cfg - 后端返回的原始 frpc 配置
|
|
127
|
+
* @param {string} [localHost] - frpc 应当转发到的本地地址(宿主真实监听地址)
|
|
127
128
|
* @returns {string} 修正后的配置
|
|
128
129
|
*/
|
|
129
|
-
function finalizeFrpcConfig(cfg) {
|
|
130
|
+
function finalizeFrpcConfig(cfg, localHost) {
|
|
130
131
|
let text = String(cfg || '')
|
|
131
132
|
// 3) 先剥离 client 端已不支持的 poolCount 字段(放最前,保证后续注入干净)
|
|
132
133
|
text = text.replace(POOL_COUNT_RE, '')
|
|
@@ -140,7 +141,19 @@ function finalizeFrpcConfig(cfg) {
|
|
|
140
141
|
}
|
|
141
142
|
text = text.replace(/^(\s*serverAddr\s*=\s*)[^\n]+/m, `$1"${override.trim()}"`)
|
|
142
143
|
}
|
|
143
|
-
// 2)
|
|
144
|
+
// 2.5) localIP 覆写:宿主如果只绑了某块网卡(官方桌面端/第三方壳的 `--host 192.168.x.x`),
|
|
145
|
+
// 127.0.0.1 上没有任何监听,frpc 转发过去就是 connection refused →
|
|
146
|
+
// 中转服务器/nginx 对外只能是 502 Bad Gateway。这里把代理的 localIP 改成宿主真实地址。
|
|
147
|
+
const target = String(localHost || '127.0.0.1').trim() || '127.0.0.1'
|
|
148
|
+
if (target !== '127.0.0.1') {
|
|
149
|
+
if (/^\s*localIP\s*=/m.test(text)) {
|
|
150
|
+
text = text.replace(/^(\s*localIP\s*=\s*)[^\n]*/gm, `$1"${target}"`)
|
|
151
|
+
} else {
|
|
152
|
+
// 没有 localIP 行时补在每条 localPort 之前(与 frp 配置顺序无关,读者更直观)
|
|
153
|
+
text = text.replace(/^(\s*localPort\s*=\s*)/gm, `localIP = "${target}"\n$1`)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// 3) loginFailExit 必须放顶层(第一个 [[proxies]] 之前),否则 frpc 解析报错
|
|
144
157
|
if (!/loginFailExit\s*=/.test(text)) {
|
|
145
158
|
const injectLine = '# 由 dsh-mobile-remote 注入:连不上 frps 时持续重试不退出\nloginFailExit = false\n'
|
|
146
159
|
const proxiesIdx = text.indexOf('[[proxies]]')
|
|
@@ -351,7 +364,7 @@ async function ensureFrpcBinary(base) {
|
|
|
351
364
|
* 启动外网隧道。做了稳健性处理,避免"反复点/开了又关"。
|
|
352
365
|
* @returns {Promise<{ok:boolean, url?:string, status?:string, error?:string, message?:string, log?:string}>}
|
|
353
366
|
*/
|
|
354
|
-
async function startExternal(state, body, patchFile, defaultLocalPort) {
|
|
367
|
+
async function startExternal(state, body, patchFile, defaultLocalPort, localHost) {
|
|
355
368
|
// 已运行则直接返回,避免重复点重复启动;
|
|
356
369
|
// 但状态为 error(如鉴权失败)时允许重试:kill 旧进程并重新绑定。
|
|
357
370
|
if (state.proc && state.proc.exitCode === null && state.status !== 'error') {
|
|
@@ -363,8 +376,9 @@ async function startExternal(state, body, patchFile, defaultLocalPort) {
|
|
|
363
376
|
|
|
364
377
|
state.status = 'connecting'
|
|
365
378
|
state.error = null
|
|
379
|
+
state.localHost = String(localHost || '127.0.0.1')
|
|
366
380
|
try {
|
|
367
|
-
return await startExternalInner(state, body, patchFile, defaultLocalPort)
|
|
381
|
+
return await startExternalInner(state, body, patchFile, defaultLocalPort, state.localHost)
|
|
368
382
|
} catch (error) {
|
|
369
383
|
// 启动失败必须复位状态,否则前端会一直卡在"连接中"且无法关闭
|
|
370
384
|
state.status = 'error'
|
|
@@ -373,7 +387,7 @@ async function startExternal(state, body, patchFile, defaultLocalPort) {
|
|
|
373
387
|
}
|
|
374
388
|
}
|
|
375
389
|
|
|
376
|
-
async function startExternalInner(state, body, patchFile, defaultLocalPort) {
|
|
390
|
+
async function startExternalInner(state, body, patchFile, defaultLocalPort, localHost) {
|
|
377
391
|
// 端口优先级:调用方显式指定 > 当前真实监听端口。
|
|
378
392
|
// 不再让过期的持久化 localPort 覆盖当前端口(否则 frpc 会转发到无人监听的端口)。
|
|
379
393
|
const localPort = Number((body && body.localPort) || defaultLocalPort || 0)
|
|
@@ -401,7 +415,7 @@ async function startExternalInner(state, body, patchFile, defaultLocalPort) {
|
|
|
401
415
|
|
|
402
416
|
// 2) 写 frpc.toml(含 loginFailExit=false 与回环地址覆写)
|
|
403
417
|
const cfgPath = frpcConfigPath()
|
|
404
|
-
const frpcConfig = finalizeFrpcConfig(data.frpcConfig)
|
|
418
|
+
const frpcConfig = finalizeFrpcConfig(data.frpcConfig, localHost)
|
|
405
419
|
// 防线:确认代理确实指向当前监听端口。错配时 frpc 会连不上本地服务,
|
|
406
420
|
// 表现为外网域名 502 Bad Gateway(日志:connect to local service ... refused)。
|
|
407
421
|
if (localPort) {
|
|
@@ -489,6 +503,9 @@ async function startExternalInner(state, body, patchFile, defaultLocalPort) {
|
|
|
489
503
|
state.wsClient.connect()
|
|
490
504
|
// 隧道确认建立后启动持续心跳,让后端 last_seen 保持新鲜(管理端在线判定依赖它)
|
|
491
505
|
startKeepAlive(state)
|
|
506
|
+
// 同时开始探测外网入口的真实可达性:frpc 进程活着并不代表 nginx/中转侧能拿到
|
|
507
|
+
// 200 —— 本地服务没起来(端口/绑定地址不对)时对外就是 502,必须自己暴露出来。
|
|
508
|
+
startUrlProbe(state)
|
|
492
509
|
}
|
|
493
510
|
|
|
494
511
|
const probe = () => {
|
|
@@ -575,6 +592,7 @@ async function startExternalInner(state, body, patchFile, defaultLocalPort) {
|
|
|
575
592
|
*/
|
|
576
593
|
function stopExternal(state, opts = {}) {
|
|
577
594
|
stopKeepAlive(state)
|
|
595
|
+
stopUrlProbe(state)
|
|
578
596
|
// 断开 WS 长连接(中转服务端据此判定离线)
|
|
579
597
|
if (state.wsClient) {
|
|
580
598
|
try { state.wsClient.disconnect() } catch { /* ignore */ }
|
|
@@ -601,7 +619,7 @@ function stopExternal(state, opts = {}) {
|
|
|
601
619
|
* 重新绑定并拉起 frpc,免去每次手动输入验证码与中转服务地址。
|
|
602
620
|
* 失败时把状态置为 error 并保留持久化,供用户手动处理或下次重启重试。
|
|
603
621
|
*/
|
|
604
|
-
async function autoRestoreExternal(state, patchFile, defaultLocalPort) {
|
|
622
|
+
async function autoRestoreExternal(state, patchFile, defaultLocalPort, localHost) {
|
|
605
623
|
const saved = loadPersistedState()
|
|
606
624
|
if (!saved) return
|
|
607
625
|
try {
|
|
@@ -620,7 +638,7 @@ async function autoRestoreExternal(state, patchFile, defaultLocalPort) {
|
|
|
620
638
|
if (stale && live && stale !== live) {
|
|
621
639
|
state.lastLog = `检测到端口变化(持久化 ${stale} → 当前 ${live}),外网隧道已改用当前端口`
|
|
622
640
|
}
|
|
623
|
-
await startExternal(state, body, patchFile, live || stale)
|
|
641
|
+
await startExternal(state, body, patchFile, live || stale, localHost)
|
|
624
642
|
} catch (e) {
|
|
625
643
|
state.status = 'error'
|
|
626
644
|
state.error = `自动恢复外网访问失败:${String((e && e.message) || e)}`
|
|
@@ -709,6 +727,71 @@ function probeTunnelHealth(state) {
|
|
|
709
727
|
return verdict
|
|
710
728
|
}
|
|
711
729
|
|
|
730
|
+
/* ------------------------------------------------------------------ */
|
|
731
|
+
/* 外网入口可达性探测 */
|
|
732
|
+
/* ------------------------------------------------------------------ */
|
|
733
|
+
|
|
734
|
+
/** 探测结果缓存:同一 URL 30 秒内不重复请求(status 每 5s 轮询一次)。 */
|
|
735
|
+
let urlProbeCache = { url: '', at: 0, value: null }
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* 用 HTTP GET 探测外网入口的真实状态。
|
|
739
|
+
* frpc 进程活着只代表「已经连上 frps」,不代表中转服务器能把请求送到本机:
|
|
740
|
+
* 本机端口没人监听 / 绑定地址不对时,nginx 对外一律回 502 —— 这里把它探测出来。
|
|
741
|
+
* @param {string} url - 外网访问地址(https://xxx)
|
|
742
|
+
* @returns {Promise<{ok: boolean, status: number, at: number, error?: string}|null>}
|
|
743
|
+
*/
|
|
744
|
+
async function probeExternalUrl(url) {
|
|
745
|
+
if (!url) return null
|
|
746
|
+
const now = Date.now()
|
|
747
|
+
if (urlProbeCache.url === url && now - urlProbeCache.at < 30_000) return urlProbeCache.value
|
|
748
|
+
let value
|
|
749
|
+
try {
|
|
750
|
+
const res = await fetch(url, {
|
|
751
|
+
method: 'GET',
|
|
752
|
+
redirect: 'follow',
|
|
753
|
+
headers: { 'user-agent': 'dsh-mobile-remote/healthcheck' },
|
|
754
|
+
signal: AbortSignal.timeout(8000),
|
|
755
|
+
})
|
|
756
|
+
// 只用状态码判断可达性,正文直接丢弃(避免把整个前端页面读进内存)
|
|
757
|
+
try { await res.arrayBuffer() } catch { /* ignore */ }
|
|
758
|
+
value = {
|
|
759
|
+
ok: res.status >= 200 && res.status < 400,
|
|
760
|
+
status: res.status,
|
|
761
|
+
at: Date.now(),
|
|
762
|
+
error: res.status === 502
|
|
763
|
+
? '中转服务器返回 502:frps 已连上,但本机端口没有响应(转发地址/端口不对,或 dsh 未监听该地址)'
|
|
764
|
+
: null,
|
|
765
|
+
}
|
|
766
|
+
} catch (error) {
|
|
767
|
+
value = { ok: false, status: 0, at: Date.now(), error: String((error && error.message) || error) }
|
|
768
|
+
}
|
|
769
|
+
urlProbeCache = { url, at: now, value }
|
|
770
|
+
return value
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/** 启动外网入口可达性轮询(30s 一次,随隧道停止而停止)。 */
|
|
774
|
+
function startUrlProbe(state) {
|
|
775
|
+
if (state.probeTimer) return
|
|
776
|
+
const tick = async () => {
|
|
777
|
+
if (!state.url) return
|
|
778
|
+
const value = await probeExternalUrl(state.url)
|
|
779
|
+
if (value) state.probe = value
|
|
780
|
+
}
|
|
781
|
+
void tick()
|
|
782
|
+
state.probeTimer = setInterval(() => { void tick() }, 30_000)
|
|
783
|
+
if (state.probeTimer.unref) state.probeTimer.unref()
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/** 停止外网入口探测并清空缓存结果。 */
|
|
787
|
+
function stopUrlProbe(state) {
|
|
788
|
+
if (state.probeTimer) {
|
|
789
|
+
clearInterval(state.probeTimer)
|
|
790
|
+
state.probeTimer = null
|
|
791
|
+
}
|
|
792
|
+
state.probe = null
|
|
793
|
+
}
|
|
794
|
+
|
|
712
795
|
/** 当前外网状态摘要(供 /status 使用)。 */
|
|
713
796
|
function externalView(state) {
|
|
714
797
|
const running = !!state.proc && state.proc.exitCode === null
|
|
@@ -737,6 +820,10 @@ function externalView(state) {
|
|
|
737
820
|
pid: running && state.proc ? state.proc.pid : null,
|
|
738
821
|
startedAt: state.startedAt || null,
|
|
739
822
|
lastLog: state.lastLog || null,
|
|
823
|
+
// frpc 实际转发的本地地址(非回环时是宿主的网卡 IP)
|
|
824
|
+
localHost: state.localHost || null,
|
|
825
|
+
// 外网入口真实可达性:{ ok, status, at, error }
|
|
826
|
+
probe: state.probe || null,
|
|
740
827
|
error,
|
|
741
828
|
}
|
|
742
829
|
}
|
|
@@ -746,6 +833,8 @@ export {
|
|
|
746
833
|
externalDir,
|
|
747
834
|
frpcBinaryPath,
|
|
748
835
|
frpcConfigPath,
|
|
836
|
+
// 供单元测试直接断言配置改写(localIP / loginFailExit / poolCount)
|
|
837
|
+
finalizeFrpcConfig,
|
|
749
838
|
startExternal,
|
|
750
839
|
stopExternal,
|
|
751
840
|
externalView,
|