@wenbin_wb/dsh-bridge 2.9.0 → 2.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/CHANGELOG.md +320 -295
- package/client/client.js +370 -124
- package/client/index.js +4244 -4073
- package/client/unlock-manager.js +142 -0
- package/lib/auth/manager.js +545 -532
- package/lib/bridge-rpc.js +455 -436
- package/lib/index.js +1831 -1765
- package/package.json +106 -106
package/lib/bridge-rpc.js
CHANGED
|
@@ -1,436 +1,455 @@
|
|
|
1
|
-
// DSH Bridge - RPC Interface (server side)
|
|
2
|
-
// Loopback-only RPC methods for browser UI
|
|
3
|
-
|
|
4
|
-
import QRCode from 'qrcode';
|
|
5
|
-
import { BRIDGE_RPC_CHANNEL, BRIDGE_ENDPOINTS } from './bridge-rpc-constants.js';
|
|
6
|
-
import { RateLimiter } from './security/rate-limiter.js';
|
|
7
|
-
|
|
8
|
-
export { BRIDGE_RPC_CHANNEL, BRIDGE_ENDPOINTS };
|
|
9
|
-
|
|
10
|
-
const rpcRateLimiter = new RateLimiter({ maxRequests: 30, windowMs: 60000 });
|
|
11
|
-
|
|
12
|
-
function ok(value) {
|
|
13
|
-
return { ok: true, value };
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function fail(code, message, details = {}) {
|
|
17
|
-
const allowedCodes = new Set([
|
|
18
|
-
'bad-request', 'cancelled', 'internal', 'settings-rejected', 'command-error'
|
|
19
|
-
]);
|
|
20
|
-
const safeCode = allowedCodes.has(code) ? code : 'bad-request';
|
|
21
|
-
return {
|
|
22
|
-
ok: false,
|
|
23
|
-
error: {
|
|
24
|
-
code: safeCode,
|
|
25
|
-
message,
|
|
26
|
-
details: { issues: [{ message }], ...details },
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// 把登录态里的二维码载荷渲染成浏览器可展示的 dataURL(带缓存,避免重复生成)
|
|
32
|
-
async function renderQr(loginState) {
|
|
33
|
-
if (!loginState?.qrPayload) return null;
|
|
34
|
-
const cacheKey = `${loginState.qrKind}:${loginState.qrPayload.slice(0, 80)}`;
|
|
35
|
-
if (renderQr.cache && renderQr.cache.key === cacheKey) return renderQr.cache.url;
|
|
36
|
-
let url;
|
|
37
|
-
const payload = loginState.qrPayload;
|
|
38
|
-
if (loginState.qrKind === 'img') {
|
|
39
|
-
url = /^data:/i.test(payload) ? payload : `data:image/png;base64,${payload}`;
|
|
40
|
-
} else {
|
|
41
|
-
try {
|
|
42
|
-
url = await QRCode.toDataURL(payload, {
|
|
43
|
-
width: 300, margin: 2, color: { dark: '#1F2421', light: '#FFFFFF' },
|
|
44
|
-
});
|
|
45
|
-
} catch { url = null; }
|
|
46
|
-
}
|
|
47
|
-
renderQr.cache = { key: cacheKey, url };
|
|
48
|
-
return url;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function checkAdminAuth(authManager, payload, { requireConfigured = false } = {}) {
|
|
52
|
-
if (!authManager) return null;
|
|
53
|
-
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
return
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
return
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
await service.
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const status = await service.
|
|
173
|
-
return ok(status);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
if (endpoint === BRIDGE_ENDPOINTS.
|
|
177
|
-
const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
|
|
178
|
-
if (adminErr) return adminErr;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
const
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
return ok(
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (endpoint === BRIDGE_ENDPOINTS.
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
const
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (!
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
const
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
const
|
|
358
|
-
if (
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
const
|
|
379
|
-
return
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
const
|
|
394
|
-
return
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
return
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
const
|
|
424
|
-
return
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
1
|
+
// DSH Bridge - RPC Interface (server side)
|
|
2
|
+
// Loopback-only RPC methods for browser UI
|
|
3
|
+
|
|
4
|
+
import QRCode from 'qrcode';
|
|
5
|
+
import { BRIDGE_RPC_CHANNEL, BRIDGE_ENDPOINTS } from './bridge-rpc-constants.js';
|
|
6
|
+
import { RateLimiter } from './security/rate-limiter.js';
|
|
7
|
+
|
|
8
|
+
export { BRIDGE_RPC_CHANNEL, BRIDGE_ENDPOINTS };
|
|
9
|
+
|
|
10
|
+
const rpcRateLimiter = new RateLimiter({ maxRequests: 30, windowMs: 60000 });
|
|
11
|
+
|
|
12
|
+
function ok(value) {
|
|
13
|
+
return { ok: true, value };
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function fail(code, message, details = {}) {
|
|
17
|
+
const allowedCodes = new Set([
|
|
18
|
+
'bad-request', 'cancelled', 'internal', 'settings-rejected', 'command-error'
|
|
19
|
+
]);
|
|
20
|
+
const safeCode = allowedCodes.has(code) ? code : 'bad-request';
|
|
21
|
+
return {
|
|
22
|
+
ok: false,
|
|
23
|
+
error: {
|
|
24
|
+
code: safeCode,
|
|
25
|
+
message,
|
|
26
|
+
details: { issues: [{ message }], ...details },
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 把登录态里的二维码载荷渲染成浏览器可展示的 dataURL(带缓存,避免重复生成)
|
|
32
|
+
async function renderQr(loginState) {
|
|
33
|
+
if (!loginState?.qrPayload) return null;
|
|
34
|
+
const cacheKey = `${loginState.qrKind}:${loginState.qrPayload.slice(0, 80)}`;
|
|
35
|
+
if (renderQr.cache && renderQr.cache.key === cacheKey) return renderQr.cache.url;
|
|
36
|
+
let url;
|
|
37
|
+
const payload = loginState.qrPayload;
|
|
38
|
+
if (loginState.qrKind === 'img') {
|
|
39
|
+
url = /^data:/i.test(payload) ? payload : `data:image/png;base64,${payload}`;
|
|
40
|
+
} else {
|
|
41
|
+
try {
|
|
42
|
+
url = await QRCode.toDataURL(payload, {
|
|
43
|
+
width: 300, margin: 2, color: { dark: '#1F2421', light: '#FFFFFF' },
|
|
44
|
+
});
|
|
45
|
+
} catch { url = null; }
|
|
46
|
+
}
|
|
47
|
+
renderQr.cache = { key: cacheKey, url };
|
|
48
|
+
return url;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function checkAdminAuth(authManager, payload, { requireConfigured = false } = {}) {
|
|
52
|
+
if (!authManager) return null;
|
|
53
|
+
// local_only 最严格:即使关闭管理保护,也绝不远程放行(本机经 loopback-token 天然持有 adminToken)
|
|
54
|
+
// 注意:RPC 层无法区分本机/远程(代理以回环转发),local_only 的防线是 unlockAdmin 拒绝
|
|
55
|
+
// 远程解锁——因此这里管理保护关闭时也不放行 local_only,保持"仅本机可管理"的语义。
|
|
56
|
+
if (authManager.adminPolicy === 'local_only') {
|
|
57
|
+
// local_only 下必须持有有效 adminToken(只有本机 loopback-token / 本机解锁能拿到)
|
|
58
|
+
if (payload?.adminToken && authManager.validateAdminSession(payload.adminToken)) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return fail('bad-request', '操作已被拦截:当前策略为仅限电脑本机管理');
|
|
62
|
+
}
|
|
63
|
+
// 管理保护独立开关:用户明确关闭后,管理操作免 adminToken(与访问认证 enabled 解耦)
|
|
64
|
+
if (authManager.adminProtection === false) return null;
|
|
65
|
+
if (authManager.adminPolicy === 'open') return null;
|
|
66
|
+
// 若系统尚未设置任何管理密码或访客密码,允许免密管理
|
|
67
|
+
const hasAnyPassword = authManager.hasAdminPassword || authManager.hasPassword;
|
|
68
|
+
// T2.9:高危操作(备份导出/导入、隧道配置与启动、目录浏览、添加工作区、升级、重启)
|
|
69
|
+
// 在系统从未设置任何密码时不再静默放行,强制先完成一次密码设置,
|
|
70
|
+
// 杜绝"未设密码 = 局域网/隧道内任何人都可导出全部凭证"的裸奔状态被直接利用
|
|
71
|
+
if (requireConfigured && !hasAnyPassword) {
|
|
72
|
+
return fail('bad-request', '该操作涉及敏感配置:请先在「安全认证」中设置访问密码或管理密码后再执行');
|
|
73
|
+
}
|
|
74
|
+
if (!hasAnyPassword) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
// 已设置密码时,必须提供经服务端校验有效的 adminToken(绝不依赖客户端自称的 isLocalhost)
|
|
78
|
+
if (payload?.adminToken && authManager.validateAdminSession(payload.adminToken)) {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
return fail('bad-request', '操作已被拦截:需要管理员权限,请先在控制台输入管理密码解锁');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function installBridgeRpc(ctx, { service, authManager, platformManager, logger, saveCustomTunnelConfig, exportBackup, importBackup }) {
|
|
85
|
+
if (!ctx?.connection?.rpc?.handle) {
|
|
86
|
+
logger.warn('dsh-bridge: Connection RPC unavailable — UI will not work');
|
|
87
|
+
return () => {};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return ctx.connection.rpc.handle(
|
|
91
|
+
BRIDGE_RPC_CHANNEL,
|
|
92
|
+
async (endpoint, payload = {}, signal) => {
|
|
93
|
+
if (signal?.aborted) return fail('cancelled', 'Request was cancelled');
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
if (endpoint === BRIDGE_ENDPOINTS.getStatus) {
|
|
97
|
+
const isAdmin = checkAdminAuth(authManager, payload) === null;
|
|
98
|
+
const status = await service.getStatus({ adminAuthValid: isAdmin });
|
|
99
|
+
return ok(status);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ---- 访问安全认证 ----
|
|
103
|
+
|
|
104
|
+
if (endpoint === BRIDGE_ENDPOINTS.authGetStatus) {
|
|
105
|
+
if (!authManager) return fail('bad-request', 'AuthManager 未初始化');
|
|
106
|
+
const isAdmin = checkAdminAuth(authManager, payload) === null;
|
|
107
|
+
return ok(authManager.getStatus({ masked: !isAdmin }));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (endpoint === BRIDGE_ENDPOINTS.authUpdateConfig) {
|
|
111
|
+
if (!authManager) return fail('bad-request', 'AuthManager 未初始化');
|
|
112
|
+
const { enabled, mode, scope, adminPolicy, adminProtection, password, adminPassword } = payload;
|
|
113
|
+
// 仅切换 enabled(访问认证开关)不需要管理权限:用户应能自由决定是否开放访问,
|
|
114
|
+
// 否则"关闭访问认证"这个动作本身会被管理保护锁死(死锁:关闭要先解锁,解锁要过认证)。
|
|
115
|
+
// 其余字段(模式/范围/策略/密码/管理保护)均涉及安全配置,仍需管理权限。
|
|
116
|
+
const sensitive = mode !== undefined || scope !== undefined || adminPolicy !== undefined
|
|
117
|
+
|| adminProtection !== undefined || password !== undefined || adminPassword !== undefined;
|
|
118
|
+
if (sensitive) {
|
|
119
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
120
|
+
if (adminErr) return adminErr;
|
|
121
|
+
}
|
|
122
|
+
if (enabled != null) await authManager.setEnabled(enabled);
|
|
123
|
+
if (mode != null) await authManager.setMode(mode);
|
|
124
|
+
if (scope != null) await authManager.setScope(scope);
|
|
125
|
+
if (adminPolicy != null) await authManager.setAdminPolicy(adminPolicy);
|
|
126
|
+
if (adminProtection != null) await authManager.setAdminProtection(adminProtection);
|
|
127
|
+
if (password !== undefined) await authManager.setPassword(password);
|
|
128
|
+
if (adminPassword !== undefined) await authManager.setAdminPassword(adminPassword);
|
|
129
|
+
return ok(authManager.getStatus({ masked: false }));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (endpoint === BRIDGE_ENDPOINTS.authRegenerateToken) {
|
|
133
|
+
if (!authManager) return fail('bad-request', 'AuthManager 未初始化');
|
|
134
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
135
|
+
if (adminErr) return adminErr;
|
|
136
|
+
|
|
137
|
+
await authManager.regenerateSecretToken();
|
|
138
|
+
return ok(authManager.getStatus({ masked: false }));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (endpoint === BRIDGE_ENDPOINTS.authAdminUnlock) {
|
|
142
|
+
if (!authManager) return fail('bad-request', 'AuthManager 未初始化');
|
|
143
|
+
const { password } = payload;
|
|
144
|
+
const res = await authManager.unlockAdmin(password);
|
|
145
|
+
if (res.ok) return ok({ adminToken: res.adminToken });
|
|
146
|
+
return fail('bad-request', res.error || '管理员密码错误');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (endpoint === BRIDGE_ENDPOINTS.authAdminLock) {
|
|
150
|
+
if (!authManager) return fail('bad-request', 'AuthManager 未初始化');
|
|
151
|
+
if (payload?.adminToken) authManager.revokeAdminSession(payload.adminToken);
|
|
152
|
+
return ok({ locked: true });
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (endpoint === BRIDGE_ENDPOINTS.saveCustomTunnelConfig) {
|
|
156
|
+
const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
|
|
157
|
+
if (adminErr) return adminErr;
|
|
158
|
+
|
|
159
|
+
// 未提供的字段保持 undefined 透传:服务端视为"保留现值"
|
|
160
|
+
const { serverUrl, accessToken } = payload;
|
|
161
|
+
await saveCustomTunnelConfig(serverUrl, accessToken);
|
|
162
|
+
const status = await service.getStatus();
|
|
163
|
+
return ok(status);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (endpoint === BRIDGE_ENDPOINTS.saveCloudflaredConfig) {
|
|
167
|
+
const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
|
|
168
|
+
if (adminErr) return adminErr;
|
|
169
|
+
|
|
170
|
+
const { token, hostname } = payload;
|
|
171
|
+
await service.saveCloudflaredConfig({ token, hostname });
|
|
172
|
+
const status = await service.getStatus();
|
|
173
|
+
return ok(status);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (endpoint === BRIDGE_ENDPOINTS.setTunnelAutoStart) {
|
|
177
|
+
const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
|
|
178
|
+
if (adminErr) return adminErr;
|
|
179
|
+
|
|
180
|
+
const { tunnel, autoStart } = payload;
|
|
181
|
+
await service.setTunnelAutoStart({ tunnel, autoStart });
|
|
182
|
+
const status = await service.getStatus();
|
|
183
|
+
return ok(status);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (endpoint === BRIDGE_ENDPOINTS.setLanIp) {
|
|
187
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
188
|
+
if (adminErr) return adminErr;
|
|
189
|
+
|
|
190
|
+
const { ip } = payload;
|
|
191
|
+
const status = await service.setLanIp({ ip });
|
|
192
|
+
return ok(status);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (endpoint === BRIDGE_ENDPOINTS.startCustomTunnel) {
|
|
196
|
+
const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
|
|
197
|
+
if (adminErr) return adminErr;
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
await service.startCustomTunnel();
|
|
201
|
+
const status = await service.getStatus();
|
|
202
|
+
return ok(status);
|
|
203
|
+
} catch (err) {
|
|
204
|
+
logger.error('Failed to start custom tunnel: %s', err.message);
|
|
205
|
+
return fail('bad-request', err.message);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (endpoint === BRIDGE_ENDPOINTS.stopCustomTunnel) {
|
|
210
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
211
|
+
if (adminErr) return adminErr;
|
|
212
|
+
|
|
213
|
+
service.stopCustomTunnel();
|
|
214
|
+
const status = await service.getStatus();
|
|
215
|
+
return ok(status);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (endpoint === BRIDGE_ENDPOINTS.startCloudflared) {
|
|
219
|
+
const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
|
|
220
|
+
if (adminErr) return adminErr;
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
await service.startCloudflared();
|
|
224
|
+
const status = await service.getStatus();
|
|
225
|
+
return ok(status);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
logger.error('Failed to start cloudflared: %s', err.message);
|
|
228
|
+
return fail('bad-request', err.message);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (endpoint === BRIDGE_ENDPOINTS.stopCloudflared) {
|
|
233
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
234
|
+
if (adminErr) return adminErr;
|
|
235
|
+
|
|
236
|
+
service.stopCloudflared();
|
|
237
|
+
const status = await service.getStatus();
|
|
238
|
+
return ok(status);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (endpoint === BRIDGE_ENDPOINTS.resetCloudflared) {
|
|
242
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
243
|
+
if (adminErr) return adminErr;
|
|
244
|
+
|
|
245
|
+
await service.resetCloudflared();
|
|
246
|
+
const status = await service.getStatus();
|
|
247
|
+
return ok(status);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (endpoint === BRIDGE_ENDPOINTS.checkVersion) {
|
|
251
|
+
const result = await service.checkVersion();
|
|
252
|
+
return ok(result);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (endpoint === BRIDGE_ENDPOINTS.upgradePlugin) {
|
|
256
|
+
const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
|
|
257
|
+
if (adminErr) return adminErr;
|
|
258
|
+
|
|
259
|
+
const result = await service.upgradePlugin(payload);
|
|
260
|
+
return ok(result);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (endpoint === BRIDGE_ENDPOINTS.restartDsh) {
|
|
264
|
+
const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
|
|
265
|
+
if (adminErr) return adminErr;
|
|
266
|
+
|
|
267
|
+
const result = await service.restartDsh();
|
|
268
|
+
return ok(result);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (endpoint === BRIDGE_ENDPOINTS.exportBackup) {
|
|
272
|
+
const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
|
|
273
|
+
if (adminErr) return adminErr;
|
|
274
|
+
|
|
275
|
+
if (!exportBackup) return fail('bad-request', '备份导出服务不可用');
|
|
276
|
+
const backup = await exportBackup();
|
|
277
|
+
return ok(backup);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (endpoint === BRIDGE_ENDPOINTS.importBackup) {
|
|
281
|
+
const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
|
|
282
|
+
if (adminErr) return adminErr;
|
|
283
|
+
|
|
284
|
+
if (!importBackup) return fail('bad-request', '备份导入服务不可用');
|
|
285
|
+
const result = await importBackup(payload?.backup);
|
|
286
|
+
const status = await service.getStatus({ adminAuthValid: true });
|
|
287
|
+
return ok({ result, status });
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (endpoint === BRIDGE_ENDPOINTS.diagnoseNetwork) {
|
|
291
|
+
const result = await service.diagnoseNetwork();
|
|
292
|
+
return ok(result);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (endpoint === BRIDGE_ENDPOINTS.getSystemMetrics) {
|
|
296
|
+
const metrics = service.getSystemMetrics();
|
|
297
|
+
return ok(metrics);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// ---- 远程工作区管理与目录浏览 ----
|
|
301
|
+
|
|
302
|
+
if (endpoint === BRIDGE_ENDPOINTS.listRemoteDirectories) {
|
|
303
|
+
const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
|
|
304
|
+
if (adminErr) return adminErr;
|
|
305
|
+
|
|
306
|
+
const clientKey = payload?.clientIp || payload?.adminToken || 'default';
|
|
307
|
+
const rateCheck = rpcRateLimiter.check(clientKey, 30);
|
|
308
|
+
if (!rateCheck.allowed) {
|
|
309
|
+
return fail('bad-request', `请求过于频繁,请等待 ${rateCheck.retryAfterSec} 秒后再试`);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const result = await service.listRemoteDirectories(payload?.path);
|
|
313
|
+
return ok(result);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
if (endpoint === BRIDGE_ENDPOINTS.addRemoteWorkspace) {
|
|
317
|
+
const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
|
|
318
|
+
if (adminErr) return adminErr;
|
|
319
|
+
|
|
320
|
+
const clientKey = payload?.clientIp || payload?.adminToken || 'default';
|
|
321
|
+
const rateCheck = rpcRateLimiter.check(clientKey, 20);
|
|
322
|
+
if (!rateCheck.allowed) {
|
|
323
|
+
return fail('bad-request', `添加工作区请求过于频繁,请等待 ${rateCheck.retryAfterSec} 秒后再试`);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const result = await service.addWorkspace(payload?.path);
|
|
327
|
+
return ok(result);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (endpoint === BRIDGE_ENDPOINTS.listWorkspaces) {
|
|
331
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
332
|
+
if (adminErr) return adminErr;
|
|
333
|
+
|
|
334
|
+
const result = await service.getWorkspaces();
|
|
335
|
+
return ok(result);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ---- 平台管理器(多 IM 平台)----
|
|
339
|
+
|
|
340
|
+
if (endpoint === BRIDGE_ENDPOINTS.listPlatforms) {
|
|
341
|
+
if (!platformManager) return ok({});
|
|
342
|
+
// 每个平台的 login.qrPayload 渲染为 dataURL 后返回
|
|
343
|
+
const raw = platformManager.getStatus();
|
|
344
|
+
const out = {};
|
|
345
|
+
for (const [id, status] of Object.entries(raw)) {
|
|
346
|
+
let qr = null;
|
|
347
|
+
try { qr = await renderQr(status.login).catch(() => null); } catch { /* ignore */ }
|
|
348
|
+
out[id] = { ...status, login: { ...(status.login ?? {}), qr, qrPayload: undefined, qrKind: undefined } };
|
|
349
|
+
}
|
|
350
|
+
return ok(out);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ---- 平台操作(统一接口)----
|
|
354
|
+
|
|
355
|
+
if (endpoint === BRIDGE_ENDPOINTS.platformLogin) {
|
|
356
|
+
if (!platformManager) return fail('bad-request', 'PlatformManager 未初始化');
|
|
357
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
358
|
+
if (adminErr) return adminErr;
|
|
359
|
+
|
|
360
|
+
const { platformId, qrType } = payload;
|
|
361
|
+
if (!platformId) return fail('bad-request', '缺少 platformId 参数');
|
|
362
|
+
const platform = platformManager.get(platformId);
|
|
363
|
+
if (!platform) return fail('bad-request', `平台未注册: ${platformId}`);
|
|
364
|
+
const result = await platform.login({ qrType });
|
|
365
|
+
if (!result.ok) return fail('bad-request', result.error ?? '登录启动失败');
|
|
366
|
+
const status = platform.getStatus();
|
|
367
|
+
const qr = await renderQr(status.login).catch(() => null);
|
|
368
|
+
return ok({ ...status, login: { ...status.login, qr, qrPayload: undefined, qrKind: undefined } });
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (endpoint === BRIDGE_ENDPOINTS.platformSetAllowFrom) {
|
|
372
|
+
if (!platformManager) return fail('bad-request', 'PlatformManager 未初始化');
|
|
373
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
374
|
+
if (adminErr) return adminErr;
|
|
375
|
+
|
|
376
|
+
const { platformId, allowFrom } = payload;
|
|
377
|
+
if (!platformId) return fail('bad-request', '缺少 platformId 参数');
|
|
378
|
+
const platform = platformManager.get(platformId);
|
|
379
|
+
if (!platform) return fail('bad-request', `平台未注册: ${platformId}`);
|
|
380
|
+
await platform.setAllowFrom(allowFrom);
|
|
381
|
+
const status = platform.getStatus();
|
|
382
|
+
const qr = await renderQr(status.login).catch(() => null);
|
|
383
|
+
return ok({ ...status, login: { ...status.login, qr, qrPayload: undefined, qrKind: undefined } });
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (endpoint === BRIDGE_ENDPOINTS.platformSetConfig) {
|
|
387
|
+
if (!platformManager) return fail('bad-request', 'PlatformManager 未初始化');
|
|
388
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
389
|
+
if (adminErr) return adminErr;
|
|
390
|
+
|
|
391
|
+
const { platformId, ...config } = payload;
|
|
392
|
+
if (!platformId) return fail('bad-request', '缺少 platformId 参数');
|
|
393
|
+
const platform = platformManager.get(platformId);
|
|
394
|
+
if (!platform) return fail('bad-request', `平台未注册: ${platformId}`);
|
|
395
|
+
await platform.setConfig(config);
|
|
396
|
+
const status = platform.getStatus();
|
|
397
|
+
const qr = await renderQr(status.login).catch(() => null);
|
|
398
|
+
return ok({ ...status, login: { ...status.login, qr, qrPayload: undefined, qrKind: undefined } });
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (endpoint === BRIDGE_ENDPOINTS.platformStop) {
|
|
402
|
+
if (!platformManager) return fail('bad-request', 'PlatformManager 未初始化');
|
|
403
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
404
|
+
if (adminErr) return adminErr;
|
|
405
|
+
|
|
406
|
+
const { platformId } = payload;
|
|
407
|
+
if (!platformId) return fail('bad-request', '缺少 platformId 参数');
|
|
408
|
+
const platform = platformManager.get(platformId);
|
|
409
|
+
if (!platform) return fail('bad-request', `平台未注册: ${platformId}`);
|
|
410
|
+
await platform.stop();
|
|
411
|
+
const status = platform.getStatus();
|
|
412
|
+
const qr = await renderQr(status.login).catch(() => null);
|
|
413
|
+
return ok({ ...status, login: { ...status.login, qr, qrPayload: undefined, qrKind: undefined } });
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (endpoint === BRIDGE_ENDPOINTS.platformStart) {
|
|
417
|
+
if (!platformManager) return fail('bad-request', 'PlatformManager 未初始化');
|
|
418
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
419
|
+
if (adminErr) return adminErr;
|
|
420
|
+
|
|
421
|
+
const { platformId } = payload;
|
|
422
|
+
if (!platformId) return fail('bad-request', '缺少 platformId 参数');
|
|
423
|
+
const platform = platformManager.get(platformId);
|
|
424
|
+
if (!platform) return fail('bad-request', `平台未注册: ${platformId}`);
|
|
425
|
+
await platform.start();
|
|
426
|
+
const status = platform.getStatus();
|
|
427
|
+
const qr = await renderQr(status.login).catch(() => null);
|
|
428
|
+
return ok({ ...status, login: { ...status.login, qr, qrPayload: undefined, qrKind: undefined } });
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
if (endpoint === BRIDGE_ENDPOINTS.platformUnbind) {
|
|
432
|
+
if (!platformManager) return fail('bad-request', 'PlatformManager 未初始化');
|
|
433
|
+
const adminErr = checkAdminAuth(authManager, payload);
|
|
434
|
+
if (adminErr) return adminErr;
|
|
435
|
+
|
|
436
|
+
const { platformId } = payload;
|
|
437
|
+
if (!platformId) return fail('bad-request', '缺少 platformId 参数');
|
|
438
|
+
const platform = platformManager.get(platformId);
|
|
439
|
+
if (!platform) return fail('bad-request', `平台未注册: ${platformId}`);
|
|
440
|
+
await platform.unbind();
|
|
441
|
+
const status = platform.getStatus();
|
|
442
|
+
const qr = await renderQr(status.login).catch(() => null);
|
|
443
|
+
return ok({ ...status, login: { ...status.login, qr, qrPayload: undefined, qrKind: undefined } });
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return fail('bad-request', `Unknown endpoint: ${endpoint}`);
|
|
447
|
+
} catch (err) {
|
|
448
|
+
logger.error('RPC endpoint %s failed: %s', endpoint, err.message);
|
|
449
|
+
return fail('bad-request', err.message);
|
|
450
|
+
}
|
|
451
|
+
},
|
|
452
|
+
{ authority: 'loopback' }
|
|
453
|
+
);
|
|
454
|
+
}
|
|
455
|
+
|