@feiyang666/dsh-mobile-remote 1.8.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/CHANGELOG.md +51 -0
- package/README.md +48 -11
- package/README.zh.md +53 -16
- package/RELEASE_NOTES.md +36 -74
- package/lib/client.js +9 -11
- package/lib/index.js +435 -50
- package/lib/lanproxy.js +228 -0
- package/lib/password.js +27 -0
- package/lib/patch.js +382 -244
- package/lib/patch.js.bak-0000 +244 -0
- package/package.json +4 -4
package/lib/lanproxy.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
// dsh-mobile-remote — 局域网旁挂听众(host 侧)。
|
|
2
|
+
//
|
|
3
|
+
// 为什么需要它(dsh 0.2.1-rc.3 起的现实):
|
|
4
|
+
// · webserver 的 host 只接受「一块具体网卡的 IP 字面量」,0.0.0.0 直接抛错退出;
|
|
5
|
+
// · 就算改绑到 192.168.x.x,本机 127.0.0.1 的监听也随之消失 —— 桌面端窗口、
|
|
6
|
+
// 官方桌面端(固定加载 http://127.0.0.1:<port>)会立刻白屏;
|
|
7
|
+
// · 官方桌面端用 `--port 0` 由系统分配端口,patch 里重述 port 会让端口漂移。
|
|
8
|
+
// 因此本模块采用「**本机监听完全不动,旁挂一个 LAN 听众**」的方案:
|
|
9
|
+
// · 在 `<局域网IP>:<当前端口>` 上再监听一份(同端口不同地址,Windows/Linux 都允许);
|
|
10
|
+
// · 把请求反向代理到 `127.0.0.1:<当前端口>`(TCP 层以上,HTTP 与 WebSocket 都通);
|
|
11
|
+
// · 代理时把 Host / Origin 改写成回环 authority —— dsh 的 Host/Origin 信任围栏
|
|
12
|
+
// 对回环一律放行,于是**局域网访问不需要任何 patch 改动**,也没有重启/CORS 风险;
|
|
13
|
+
// · 真实客户端 IP 通过 X-Forwarded-For 传给宿主(设备列表里仍然显示手机 IP)。
|
|
14
|
+
//
|
|
15
|
+
// 关闭时释放监听,本机访问自始至终不受影响。
|
|
16
|
+
|
|
17
|
+
import http from 'node:http'
|
|
18
|
+
import net from 'node:net'
|
|
19
|
+
|
|
20
|
+
/** 回环 authority(dsh 信任围栏自动放行的形式)。 */
|
|
21
|
+
function loopbackAuthority(port) {
|
|
22
|
+
return `127.0.0.1:${String(port)}`
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* 把 Origin 头的 authority 换成回环 authority(保留协议),
|
|
27
|
+
* 让围栏的「Origin 必须等于 Host」判断通过。
|
|
28
|
+
* @param {string} origin
|
|
29
|
+
* @param {string} authority
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
function rewriteOrigin(origin, authority) {
|
|
33
|
+
try {
|
|
34
|
+
const url = new URL(origin)
|
|
35
|
+
return `${url.protocol}//${authority}`
|
|
36
|
+
} catch {
|
|
37
|
+
return `http://${authority}`
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** 追加一条 X-Forwarded-For(保留上游已有值)。 */
|
|
42
|
+
function forwardedFor(headers, socket) {
|
|
43
|
+
const prev = headers['x-forwarded-for']
|
|
44
|
+
const ip = (socket && (socket.remoteAddress || '')) || ''
|
|
45
|
+
const clean = String(ip).replace(/^::ffff:/, '')
|
|
46
|
+
if (!clean) return prev
|
|
47
|
+
return prev ? `${prev}, ${clean}` : clean
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* 创建 LAN 旁挂听众。
|
|
52
|
+
* @param {{log?: (msg: string, err?: unknown) => void}} [options]
|
|
53
|
+
*/
|
|
54
|
+
export function createLanProxy(options = {}) {
|
|
55
|
+
const log = typeof options.log === 'function' ? options.log : () => {}
|
|
56
|
+
/** @type {Array<{server: import('node:http').Server, host: string}>} */
|
|
57
|
+
let listeners = []
|
|
58
|
+
/** 当前实际监听端口(等于宿主端口)。 */
|
|
59
|
+
let listenPort = 0
|
|
60
|
+
/** 转发目标端口(宿主端口;监听建立前就已确定,避免启动窗口内 503)。 */
|
|
61
|
+
let targetPort = 0
|
|
62
|
+
/** 当前对外提供的局域网地址列表。 */
|
|
63
|
+
let hosts = []
|
|
64
|
+
/** 每次 start/stop 递增,用于丢弃过期的启动结果。 */
|
|
65
|
+
let generation = 0
|
|
66
|
+
|
|
67
|
+
const state = () => ({
|
|
68
|
+
running: listeners.length > 0,
|
|
69
|
+
hosts: hosts.slice(),
|
|
70
|
+
port: listenPort,
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
function targetPortOf() {
|
|
74
|
+
const n = Number(targetPort)
|
|
75
|
+
return Number.isFinite(n) && n > 0 ? n : 0
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** 普通 HTTP 请求转发。 */
|
|
79
|
+
function handleRequest(req, res) {
|
|
80
|
+
const port = targetPortOf()
|
|
81
|
+
if (!port) {
|
|
82
|
+
res.writeHead(503, { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' })
|
|
83
|
+
res.end('dsh is starting, please retry in a moment.\n')
|
|
84
|
+
return
|
|
85
|
+
}
|
|
86
|
+
const authority = loopbackAuthority(port)
|
|
87
|
+
const headers = { ...req.headers }
|
|
88
|
+
const realHost = headers.host || ''
|
|
89
|
+
headers.host = authority
|
|
90
|
+
if (headers.origin) headers.origin = rewriteOrigin(headers.origin, authority)
|
|
91
|
+
headers['x-forwarded-for'] = forwardedFor(headers, req.socket)
|
|
92
|
+
headers['x-forwarded-proto'] = 'http'
|
|
93
|
+
if (realHost) headers['x-forwarded-host'] = realHost
|
|
94
|
+
const upstream = http.request({
|
|
95
|
+
host: '127.0.0.1',
|
|
96
|
+
port,
|
|
97
|
+
method: req.method,
|
|
98
|
+
path: req.url,
|
|
99
|
+
headers,
|
|
100
|
+
}, (up) => {
|
|
101
|
+
res.writeHead(up.statusCode || 502, up.headers)
|
|
102
|
+
up.pipe(res)
|
|
103
|
+
})
|
|
104
|
+
upstream.on('error', (error) => {
|
|
105
|
+
log('lan proxy upstream error', error)
|
|
106
|
+
if (!res.headersSent) {
|
|
107
|
+
res.writeHead(502, { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' })
|
|
108
|
+
}
|
|
109
|
+
try { res.end('dsh upstream unavailable\n') } catch { /* ignore */ }
|
|
110
|
+
})
|
|
111
|
+
req.on('error', () => { try { upstream.destroy() } catch { /* ignore */ } })
|
|
112
|
+
req.pipe(upstream)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** WebSocket / 其它 Upgrade 请求:按原始字节隧道转发。 */
|
|
116
|
+
function handleUpgrade(req, socket, head) {
|
|
117
|
+
const port = targetPortOf()
|
|
118
|
+
if (!port) { try { socket.destroy() } catch { /* ignore */ } return }
|
|
119
|
+
const authority = loopbackAuthority(port)
|
|
120
|
+
const headers = { ...req.headers }
|
|
121
|
+
const realHost = headers.host || ''
|
|
122
|
+
headers.host = authority
|
|
123
|
+
if (headers.origin) headers.origin = rewriteOrigin(headers.origin, authority)
|
|
124
|
+
headers['x-forwarded-for'] = forwardedFor(headers, req.socket)
|
|
125
|
+
headers['x-forwarded-proto'] = 'http'
|
|
126
|
+
if (realHost) headers['x-forwarded-host'] = realHost
|
|
127
|
+
|
|
128
|
+
const upstream = net.connect(port, '127.0.0.1', () => {
|
|
129
|
+
const lines = [`${req.method || 'GET'} ${req.url || '/'} HTTP/1.1`]
|
|
130
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
131
|
+
if (value === undefined) continue
|
|
132
|
+
if (Array.isArray(value)) for (const item of value) lines.push(`${key}: ${item}`)
|
|
133
|
+
else lines.push(`${key}: ${value}`)
|
|
134
|
+
}
|
|
135
|
+
upstream.write(`${lines.join('\r\n')}\r\n\r\n`)
|
|
136
|
+
if (head && head.length) upstream.write(head)
|
|
137
|
+
upstream.pipe(socket)
|
|
138
|
+
socket.pipe(upstream)
|
|
139
|
+
})
|
|
140
|
+
upstream.on('error', (error) => {
|
|
141
|
+
log('lan proxy upgrade error', error)
|
|
142
|
+
try { socket.destroy() } catch { /* ignore */ }
|
|
143
|
+
})
|
|
144
|
+
socket.on('error', () => { try { upstream.destroy() } catch { /* ignore */ } })
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** 在一个 host 上开监听,失败时抛出。 */
|
|
148
|
+
function listenOne(host, port) {
|
|
149
|
+
return new Promise((resolve, reject) => {
|
|
150
|
+
const server = http.createServer(handleRequest)
|
|
151
|
+
server.on('upgrade', handleUpgrade)
|
|
152
|
+
const onError = (error) => {
|
|
153
|
+
server.removeListener('listening', onListening)
|
|
154
|
+
try { server.close() } catch { /* ignore */ }
|
|
155
|
+
reject(error)
|
|
156
|
+
}
|
|
157
|
+
const onListening = () => {
|
|
158
|
+
server.removeListener('error', onError)
|
|
159
|
+
server.on('error', (error) => log(`lan proxy ${host} error`, error))
|
|
160
|
+
// 长连接(SSE / WebSocket)不能因为 keep-alive 超时被误杀
|
|
161
|
+
server.keepAliveTimeout = 65_000
|
|
162
|
+
server.headersTimeout = 70_000
|
|
163
|
+
resolve(server)
|
|
164
|
+
}
|
|
165
|
+
server.once('error', onError)
|
|
166
|
+
server.once('listening', onListening)
|
|
167
|
+
server.listen(port, host)
|
|
168
|
+
})
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 在给定局域网地址上开启监听。
|
|
173
|
+
* @param {string[]} lanHosts - 局域网 IPv4 列表(按优先级排序)
|
|
174
|
+
* @param {number} port - 宿主当前监听端口
|
|
175
|
+
* @returns {Promise<{running: boolean, hosts: string[], port: number, failures: Array<{host: string, error: string}>}>}
|
|
176
|
+
*/
|
|
177
|
+
async function start(lanHosts, port) {
|
|
178
|
+
const wanted = [...new Set((lanHosts || []).filter(Boolean))]
|
|
179
|
+
if (!wanted.length) {
|
|
180
|
+
return { running: false, hosts: [], port: 0, failures: [{ host: '', error: '没有可用的局域网 IPv4 地址' }] }
|
|
181
|
+
}
|
|
182
|
+
if (listeners.length > 0 && listenPort === port && hosts.join(',') === wanted.join(',')) {
|
|
183
|
+
return { running: true, hosts: hosts.slice(), port: listenPort, failures: [] }
|
|
184
|
+
}
|
|
185
|
+
await stop()
|
|
186
|
+
// stop() 已推进代次;此刻记录本轮的代次,后续任何 stop() 都会让它过期
|
|
187
|
+
const mark = generation
|
|
188
|
+
// 先确定转发目标:监听建立期间到达的请求也能正确转发
|
|
189
|
+
targetPort = port
|
|
190
|
+
const opened = []
|
|
191
|
+
const failures = []
|
|
192
|
+
for (const host of wanted) {
|
|
193
|
+
try {
|
|
194
|
+
opened.push({ server: await listenOne(host, port), host })
|
|
195
|
+
} catch (error) {
|
|
196
|
+
failures.push({ host, error: String((error && error.message) || error) })
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (mark !== generation) {
|
|
200
|
+
// 期间被 stop() 抢先:本次结果作废
|
|
201
|
+
for (const item of opened) { try { item.server.close() } catch { /* ignore */ } }
|
|
202
|
+
return state().running
|
|
203
|
+
? { ...state(), failures }
|
|
204
|
+
: { running: false, hosts: [], port: 0, failures }
|
|
205
|
+
}
|
|
206
|
+
listeners = opened
|
|
207
|
+
listenPort = opened.length > 0 ? port : 0
|
|
208
|
+
hosts = opened.map((item) => item.host)
|
|
209
|
+
if (!opened.length) log('lan proxy: no listener could bind', failures)
|
|
210
|
+
return { running: opened.length > 0, hosts: hosts.slice(), port: listenPort, failures }
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** 关闭全部监听。 */
|
|
214
|
+
async function stop() {
|
|
215
|
+
generation += 1
|
|
216
|
+
const closing = listeners
|
|
217
|
+
listeners = []
|
|
218
|
+
listenPort = 0
|
|
219
|
+
targetPort = 0
|
|
220
|
+
hosts = []
|
|
221
|
+
await Promise.all(closing.map(({ server }) => new Promise((resolve) => {
|
|
222
|
+
try { server.close(() => resolve()) } catch { resolve() }
|
|
223
|
+
})))
|
|
224
|
+
return state()
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return { start, stop, status: state }
|
|
228
|
+
}
|
package/lib/password.js
CHANGED
|
@@ -133,6 +133,33 @@ export function isEnabled() {
|
|
|
133
133
|
return hasPassword()
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
/* ------------------------------------------------------------------ */
|
|
137
|
+
/* 插件本地配置(与密码共用 config.json,未知字段原样保留) */
|
|
138
|
+
/* ------------------------------------------------------------------ */
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* 读取插件配置里的一个值(不存在返回 fallback)。
|
|
142
|
+
* @param {string} key
|
|
143
|
+
* @param {*} [fallback]
|
|
144
|
+
*/
|
|
145
|
+
export function getSetting(key, fallback = null) {
|
|
146
|
+
const cfg = readRawConfig()
|
|
147
|
+
return Object.prototype.hasOwnProperty.call(cfg, key) ? cfg[key] : fallback
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* 写入插件配置里的一个值(保留其它字段,例如密码哈希)。
|
|
152
|
+
* @param {string} key
|
|
153
|
+
* @param {*} value
|
|
154
|
+
*/
|
|
155
|
+
export function setSetting(key, value) {
|
|
156
|
+
const cfg = readRawConfig()
|
|
157
|
+
if (value === undefined) delete cfg[key]
|
|
158
|
+
else cfg[key] = value
|
|
159
|
+
writeRawConfig(cfg)
|
|
160
|
+
return value
|
|
161
|
+
}
|
|
162
|
+
|
|
136
163
|
/* ------------------------------------------------------------------ */
|
|
137
164
|
/* 认证会话(内存) */
|
|
138
165
|
/* ------------------------------------------------------------------ */
|