dsh-plugin-mobile-gateway 0.8.1 → 0.10.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/PROTOCOL.md +37 -2
- package/README.md +16 -4
- package/docs/dsh-0.2.0-rc.2-plugin-management-mobile-integration.md +109 -0
- package/lib/client.js +424 -130
- package/lib/cloudflare-tunnel.mjs +43 -2
- package/lib/devices.js +23 -7
- package/lib/dsh-host-adapter.mjs +28 -1
- package/lib/gateway-probe.mjs +6 -0
- package/lib/index.mjs +72 -11
- package/lib/plugin-management.mjs +194 -0
- package/lib/public-endpoint-check.mjs +61 -0
- package/package.json +3 -3
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import dns from 'node:dns/promises'
|
|
2
|
+
import https from 'node:https'
|
|
3
|
+
import crypto from 'node:crypto'
|
|
4
|
+
|
|
5
|
+
// No credentials, redirects or certificate exceptions. A 401 alone is ambiguous:
|
|
6
|
+
// require the gateway to echo our random probe nonce in its auth rejection.
|
|
7
|
+
export async function checkPublicEndpoint(endpoint, { timeoutMs = 8000, lookup = dns.lookup, request = https.request } = {}) {
|
|
8
|
+
const checkedAt = new Date().toISOString()
|
|
9
|
+
const result = (state, message, extra = {}) => ({ state, message, checkedAt, ...extra })
|
|
10
|
+
let url
|
|
11
|
+
try {
|
|
12
|
+
url = new URL(endpoint)
|
|
13
|
+
if (url.protocol !== 'wss:' || url.username || url.password || url.search || url.hash) throw new Error()
|
|
14
|
+
} catch { return result('invalid-url', '公网入口必须是无凭证的 wss:// 地址。') }
|
|
15
|
+
let timer
|
|
16
|
+
try {
|
|
17
|
+
await Promise.race([
|
|
18
|
+
lookup(url.hostname),
|
|
19
|
+
new Promise((_, reject) => { timer = setTimeout(() => reject(Object.assign(new Error(), { code: 'ETIMEDOUT' })), timeoutMs) }),
|
|
20
|
+
])
|
|
21
|
+
} catch (error) {
|
|
22
|
+
return result('dns-error', ['ENOTFOUND', 'ENODATA'].includes(error.code)
|
|
23
|
+
? '本机无法解析公网域名,不代表公共 DNS 记录一定不存在。请核对域名、DNS 缓存、网络代理及 Cloudflare 已代理的 CNAME 记录;若刚保存配置,请稍后重试。'
|
|
24
|
+
: 'DNS 查询失败或超时,请检查本机网络和 DNS 后重试。', { code: error.code || 'DNS_ERROR' })
|
|
25
|
+
} finally { clearTimeout(timer) }
|
|
26
|
+
return new Promise((resolve) => {
|
|
27
|
+
let done = false
|
|
28
|
+
let deadline
|
|
29
|
+
const finish = (value) => {
|
|
30
|
+
if (done) return
|
|
31
|
+
done = true
|
|
32
|
+
clearTimeout(deadline)
|
|
33
|
+
resolve(value)
|
|
34
|
+
}
|
|
35
|
+
let req
|
|
36
|
+
try {
|
|
37
|
+
const key = crypto.randomBytes(16).toString('base64')
|
|
38
|
+
const nonce = crypto.randomBytes(16).toString('hex')
|
|
39
|
+
req = request({ hostname: url.hostname, port: url.port || 443, path: url.pathname || '/', method: 'GET', rejectUnauthorized: true,
|
|
40
|
+
headers: { Upgrade: 'websocket', Connection: 'Upgrade', 'Sec-WebSocket-Key': key, 'Sec-WebSocket-Version': '13', 'Sec-WebSocket-Protocol': 'dsh-mobile-v1', 'X-DSH-Gateway-Probe': nonce } })
|
|
41
|
+
deadline = setTimeout(() => { finish(result('network-error', '公网 TLS/WebSocket 检测超时;请检查网络、代理和 Cloudflare 源站配置。', { code: 'ETIMEDOUT' })); req.destroy() }, timeoutMs)
|
|
42
|
+
req.on('response', (res) => {
|
|
43
|
+
const status = res.statusCode
|
|
44
|
+
finish(status === 401 && res.headers['x-dsh-gateway-probe'] === nonce ? result('reachable', '本机检测:TLS 校验通过,移动网关路由已确认且要求鉴权;不代表手机网络可达或会话同步成功。', { httpStatus: status })
|
|
45
|
+
: result('route-error', `公网返回 HTTP ${status},未确认移动网关路由。命名 Tunnel 的 Service 应为 HTTP,指向本机专用源站端口;同时检查 Cloudflare Access/安全规则。`, { httpStatus: status }))
|
|
46
|
+
res.destroy()
|
|
47
|
+
})
|
|
48
|
+
req.on('upgrade', (res, socket) => {
|
|
49
|
+
finish(result('auth-warning', '公网 WebSocket 未提供凭证即被接受,请检查设备鉴权设置。', { httpStatus: res.statusCode }))
|
|
50
|
+
socket.destroy()
|
|
51
|
+
})
|
|
52
|
+
req.on('error', (error) => {
|
|
53
|
+
const tlsError = /CERT|TLS|SSL|SELF_SIGNED|UNABLE_TO_VERIFY/.test(error.code || '')
|
|
54
|
+
finish(result(tlsError ? 'tls-error' : 'network-error', tlsError
|
|
55
|
+
? '公网 TLS 握手或证书校验失败;请检查域名证书、系统时间及网络代理,不要关闭证书校验。'
|
|
56
|
+
: '公网连接失败;请检查 DNS、网络、代理及隧道源站配置。', { code: error.code || 'CONNECT_ERROR' }))
|
|
57
|
+
})
|
|
58
|
+
req.end()
|
|
59
|
+
} catch { finish(result('network-error', '无法发起公网检测。')); req?.destroy() }
|
|
60
|
+
})
|
|
61
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-mobile-gateway",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.10.0",
|
|
4
|
+
"description": "Mobile gateway for DeepSeek Harness with secure device pairing, realtime session sync, plugin management, LAN and Cloudflare Tunnel access, and a responsive theme-aware control panel",
|
|
5
5
|
"main": "lib/index.mjs",
|
|
6
6
|
"files": [
|
|
7
7
|
"bin",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
},
|
|
27
27
|
"bin": "bin/setup-ip.mjs",
|
|
28
28
|
"scripts": {
|
|
29
|
-
"test": "node test/wire-json.test.mjs && node test/setup-ip.test.mjs && node test/host-adapter.test.mjs && node test/session-preset.test.mjs && node test/session-follower.test.mjs && node test/gateway.test.mjs && node test/auth.test.mjs && node test/lan.test.mjs && node test/cloudflared-binary.test.mjs && node test/cloudflare-tunnel.test.mjs && node test/gateway-tools.test.mjs && node test/multi-gateway.test.mjs"
|
|
29
|
+
"test": "node test/wire-json.test.mjs && node test/setup-ip.test.mjs && node test/host-adapter.test.mjs && node test/plugin-management.test.mjs && node test/session-preset.test.mjs && node test/session-follower.test.mjs && node test/gateway.test.mjs && node test/auth.test.mjs && node test/lan.test.mjs && node test/cloudflared-binary.test.mjs && node test/cloudflare-tunnel.test.mjs && node --test test/public-endpoint-check.test.mjs && node test/gateway-tools.test.mjs && node test/multi-gateway.test.mjs"
|
|
30
30
|
},
|
|
31
31
|
"exports": {
|
|
32
32
|
".": "./lib/index.mjs",
|