@feiyang666/dsh-mobile-remote 1.7.1 → 1.9.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/lib/external.js CHANGED
@@ -24,6 +24,7 @@ import {
24
24
  } from 'node:fs'
25
25
  import { spawn } from 'node:child_process'
26
26
  import { createWsStatusClient } from './ws-client.js'
27
+ import { ensureTrustedAuthority } from './patch.js'
27
28
 
28
29
  /** 默认中转服务地址(自建 dsh-update-server,Nginx 反代 443→3200,对外无端口)。
29
30
  * 可用 DSH_REMOTE_SERVER 覆盖。 */
@@ -192,44 +193,22 @@ async function downloadFrpc(base) {
192
193
  }
193
194
 
194
195
  /**
195
- * 写 profile 的 connection trustedHosts,放行外网域名。
196
+ * 放行外网域名(加入受管区域的 connection.trustedHosts)。
197
+ *
198
+ * 旧实现写的是 `trustedHosts: !!js "['域名', ...ctx.webRuntime.trustedHosts]"`,
199
+ * 但 dsh 0.1.7 / 0.2.x 的 `webStartup` 服务(见 dsh-web-app/lib/startup.js)只提供
200
+ * `openBrowser / host / port / trustedHosts`,**并不存在 `webRuntime`**。该表达式
201
+ * 因此会抛错或求值成一个对象而非数组,被 connection 的 `z.array(String)` 拒绝,
202
+ * 信任围栏等于从未真正放行——这正是「外网/局域网能打开页面但功能全 403」的根因。
203
+ *
204
+ * 现在改为由 lib/patch.js 统一维护:写出真实的字符串数组,并与 webserver 开关
205
+ * 同处一个受管区域,避免多次追加产生冲突块。
196
206
  * @param {string} patchFile - profile 的 cordis.patch.yml 绝对路径
197
207
  * @param {string} domain - 外网域名(不带端口)
198
208
  * @returns {boolean}
199
209
  */
200
- /**
201
- * 写 profile 的 connection trustedHosts,放行外网域名。
202
- * 必须使用 loader 中的正确插件 id `connection`,并用 !!js 表达式保留
203
- * webRuntime 自动推导的 LAN/CLI trusted hosts,否则 patch 不生效 / 覆盖损坏。
204
- */
205
210
  function ensureTrustedHost(patchFile, domain) {
206
- let content = existsSync(patchFile) ? readFileSync(patchFile, 'utf8') : ''
207
- // 移除官方模板的空数组占位行 `[]`(与 index.js setPatchEnabled 同理):
208
- // 它单独构成一个 YAML 文档,与追加的列表块拼接会形成无分隔符的多文档流,
209
- // js-yaml 解析报 "end of the stream or a document separator is expected"。
210
- content = content.replace(/^\s*\[\s*\]\s*$/gm, '')
211
- // 1) 清理旧的错误块(id: client-connection 或旧格式的纯数组),避免堆积/语法冲突
212
- content = content.replace(/\n*(# --- dsh-mobile-remote external trusted-host ---)\n[\s\S]*?(# --- end dsh-mobile-remote external trusted-host ---)\n*/g, '\n')
213
- content = content.replace(/\n{3,}/g, '\n\n').replace(/\s*$/, '\n')
214
- // 2) 若已含目标域名的正确配置则直接返回
215
- if (content.includes(`'${domain}'`)) return true
216
- // 3) 追加正确块:id=connection + !!js 表达式(保留 webRuntime.trustedHosts)
217
- // 注意:dsh-app-boot 的 entryListSchema(js-yaml JSON_SCHEMA + JsExpr)中 !!js 为
218
- // scalar kind,只能修饰标量字符串。若直接写在 flow sequence 前(!!js [...]),
219
- // js-yaml 会报 `unknown tag !<tag:yaml.org,2002:js>`,导致 profile 无法启动。
220
- // 因此表达式必须用双引号包成 YAML 标量,loader 求值时再还原为数组。
221
- const block = [
222
- '',
223
- '# --- dsh-mobile-remote external trusted-host ---',
224
- '- id: connection',
225
- ' config:',
226
- ` trustedHosts: !!js "['${domain}', ...ctx.webRuntime.trustedHosts]"`,
227
- '# --- end dsh-mobile-remote external trusted-host ---',
228
- '',
229
- ]
230
- const next = (content + '\n' + block.join('\n')).replace(/\n{3,}/g, '\n\n').replace(/\s*$/, '\n')
231
- writeFileSync(patchFile, next, 'utf8')
232
- return true
211
+ return ensureTrustedAuthority(patchFile, domain)
233
212
  }
234
213
 
235
214
  /** frpc 进程 pid 文件路径(用于清理残留实例,避免多进程抢同一代理名)。 */
@@ -395,7 +374,10 @@ async function startExternal(state, body, patchFile, defaultLocalPort) {
395
374
  }
396
375
 
397
376
  async function startExternalInner(state, body, patchFile, defaultLocalPort) {
398
- const data = await bindToServer(body, Number((body && body.localPort) || defaultLocalPort))
377
+ // 端口优先级:调用方显式指定 > 当前真实监听端口。
378
+ // 不再让过期的持久化 localPort 覆盖当前端口(否则 frpc 会转发到无人监听的端口)。
379
+ const localPort = Number((body && body.localPort) || defaultLocalPort || 0)
380
+ const data = await bindToServer(body, localPort)
399
381
  const base = serverBase(body)
400
382
  const domain = data.externalDomain
401
383
  || String(data.externalUrl || '').replace(/^https?:\/\//, '').split(':')[0]
@@ -419,7 +401,17 @@ async function startExternalInner(state, body, patchFile, defaultLocalPort) {
419
401
 
420
402
  // 2) 写 frpc.toml(含 loginFailExit=false 与回环地址覆写)
421
403
  const cfgPath = frpcConfigPath()
422
- writeFileSync(cfgPath, finalizeFrpcConfig(data.frpcConfig), 'utf8')
404
+ const frpcConfig = finalizeFrpcConfig(data.frpcConfig)
405
+ // 防线:确认代理确实指向当前监听端口。错配时 frpc 会连不上本地服务,
406
+ // 表现为外网域名 502 Bad Gateway(日志:connect to local service ... refused)。
407
+ if (localPort) {
408
+ const m = /localPort\s*=\s*(\d+)/.exec(frpcConfig)
409
+ const proxyPort = m ? Number(m[1]) : 0
410
+ if (proxyPort && proxyPort !== localPort) {
411
+ throw new Error(`frpc 配置的 localPort=${proxyPort} 与当前监听端口 ${localPort} 不一致,已中止以避免外网 502`)
412
+ }
413
+ }
414
+ writeFileSync(cfgPath, frpcConfig, 'utf8')
423
415
 
424
416
  // 2.5) 清理残留旧 frpc 实例,避免多个进程抢同一代理名(proxy already exists)
425
417
  killStaleFrpc()
@@ -480,7 +472,7 @@ async function startExternalInner(state, body, patchFile, defaultLocalPort) {
480
472
  // 记录实际生效的绑定参数并持久化,dsh 重启后由 autoRestoreExternal 自动恢复
481
473
  state.bindCode = (body && body.bindCode) || state.bindCode || ''
482
474
  state.serverBase = base
483
- state.localPort = Number((body && body.localPort) || defaultLocalPort)
475
+ state.localPort = localPort
484
476
  savePersistedState(state)
485
477
  // 建立 WS 长连接接收隧道状态推送(替代每 5s HTTP 轮询中转服务器);
486
478
  // ws 依赖缺失(离线安装)时 wsClient.connect() 自动 no-op,心跳走 HTTP 兜底
@@ -613,12 +605,22 @@ async function autoRestoreExternal(state, patchFile, defaultLocalPort) {
613
605
  const saved = loadPersistedState()
614
606
  if (!saved) return
615
607
  try {
608
+ // 端口以「当前真实监听端口」为准。
609
+ // 持久化的 localPort 可能来自上一次运行(例如默认 3080,或用户改过 --port),
610
+ // 若直接当成显式端口传下去,会覆盖当前端口,让 frpc 转发到一个没人监听的
611
+ // 端口上——表现为外网域名 502 Bad Gateway(frpc 日志:connect to local
612
+ // service 127.0.0.1:3080 refused)。这里只沿用与当前端口一致的旧值,
613
+ // 不一致时改用当前端口。
614
+ const live = Number(defaultLocalPort) || 0
615
+ const stale = Number(saved.localPort) || 0
616
616
  const body = {
617
617
  bindCode: saved.bindCode,
618
618
  serverBase: saved.serverBase || undefined,
619
- localPort: saved.localPort,
620
619
  }
621
- await startExternal(state, body, patchFile, defaultLocalPort)
620
+ if (stale && live && stale !== live) {
621
+ state.lastLog = `检测到端口变化(持久化 ${stale} → 当前 ${live}),外网隧道已改用当前端口`
622
+ }
623
+ await startExternal(state, body, patchFile, live || stale)
622
624
  } catch (e) {
623
625
  state.status = 'error'
624
626
  state.error = `自动恢复外网访问失败:${String((e && e.message) || e)}`