@wenbin_wb/dsh-bridge 2.8.7 → 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 +70 -0
- package/README.en.md +408 -572
- package/README.md +430 -570
- package/client/client.js +3983 -3657
- package/client/index.js +4244 -4809
- package/client/mobile-styles.js +802 -0
- package/client/unlock-manager.js +142 -0
- package/docs/fix-plan-202608.md +108 -0
- package/lib/auth/login-template.js +381 -381
- package/lib/auth/manager.js +97 -22
- package/lib/bridge-rpc.js +48 -104
- package/lib/cloudflared-manager.mjs +361 -345
- package/lib/compat.js +129 -0
- package/lib/feishu/index.js +225 -222
- package/lib/feishu/node.js +433 -409
- package/lib/index.js +271 -244
- package/lib/platform/base.js +147 -156
- package/lib/platform/commands.js +221 -0
- package/lib/platform/conversation-bridge.js +816 -1570
- package/lib/platform/dsh-storage.js +117 -0
- package/lib/platform/index.js +10 -10
- package/lib/platform/message-split.js +191 -0
- package/lib/platform/session-catalog.js +372 -0
- package/lib/platform/stream-slices.js +21 -0
- package/lib/qq/index.js +312 -309
- package/lib/qq/node.js +532 -533
- package/lib/telegram/index.js +215 -212
- package/lib/telegram/node.js +348 -350
- package/lib/tunnel-client.mjs +39 -15
- package/lib/wechat/gateway.js +973 -960
- package/lib/wechat/index.js +244 -241
- package/lib/wechat/media.js +285 -281
- package/lib/wechat/node.js +352 -350
- package/package.json +6 -2
package/lib/auth/manager.js
CHANGED
|
@@ -1,12 +1,23 @@
|
|
|
1
1
|
// lib/auth/manager.js
|
|
2
2
|
// 远程访问安全认证管理器:密码加盐哈希、Session 管理、免密安全 Token、防暴力破解与全协议鉴权拦截
|
|
3
3
|
|
|
4
|
-
import { randomBytes,
|
|
4
|
+
import { randomBytes, pbkdf2 as pbkdf2Callback, timingSafeEqual } from 'node:crypto'
|
|
5
|
+
import { promisify } from 'node:util'
|
|
6
|
+
|
|
7
|
+
const pbkdf2Async = promisify(pbkdf2Callback)
|
|
5
8
|
|
|
6
9
|
const DEFAULT_SESSION_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000 // 30 天
|
|
7
10
|
const MAX_FAILED_ATTEMPTS = 5
|
|
8
11
|
const LOCKOUT_PERIOD_MS = 60 * 1000 // 连续失败封禁 60 秒
|
|
9
12
|
|
|
13
|
+
// T4.1:PBKDF2-HMAC-SHA256 迭代次数升级到 OWASP 建议值。
|
|
14
|
+
// 新哈希带算法前缀(pbkdf2-sha256$<iterations>$<hex>);裸 hex 视为旧的 10000 次
|
|
15
|
+
// 格式,登录成功后透明重哈希升级(无需用户重设密码)。
|
|
16
|
+
const PBKDF2_ITERATIONS = 600000
|
|
17
|
+
const LEGACY_PBKDF2_ITERATIONS = 10000
|
|
18
|
+
const PBKDF2_KEYLEN = 32
|
|
19
|
+
const PBKDF2_PREFIX = 'pbkdf2-sha256$'
|
|
20
|
+
|
|
10
21
|
export class AuthManager {
|
|
11
22
|
/**
|
|
12
23
|
* @param {object} opts
|
|
@@ -17,6 +28,7 @@ export class AuthManager {
|
|
|
17
28
|
* @param {string} [opts.config.passwordSalt]
|
|
18
29
|
* @param {string} [opts.config.secretToken]
|
|
19
30
|
* @param {boolean} [opts.config.allowLoopback=true]
|
|
31
|
+
* @param {boolean} [opts.config.adminProtection=true] 管理保护独立开关:关闭后管理操作免 adminToken
|
|
20
32
|
* @param {(patch: object) => Promise<void>|void} [opts.onPersist]
|
|
21
33
|
* @param {object} [opts.logger]
|
|
22
34
|
*/
|
|
@@ -28,6 +40,8 @@ export class AuthManager {
|
|
|
28
40
|
this.mode = config.mode || 'token_and_password'
|
|
29
41
|
this.scope = config.scope || 'all' // 'all' | 'public_only' | 'lan_only'
|
|
30
42
|
this.adminPolicy = config.adminPolicy || 'password_unlock' // 'password_unlock' | 'local_only' | 'open'
|
|
43
|
+
// 管理保护独立于访问认证:默认开启(现有用户保持受保护),关闭后管理操作免 adminToken
|
|
44
|
+
this.adminProtection = config.adminProtection !== false
|
|
31
45
|
this.passwordHash = config.passwordHash || ''
|
|
32
46
|
this.passwordSalt = config.passwordSalt || ''
|
|
33
47
|
this.adminPasswordHash = config.adminPasswordHash || ''
|
|
@@ -53,8 +67,47 @@ export class AuthManager {
|
|
|
53
67
|
return 'dsh_' + randomBytes(18).toString('hex')
|
|
54
68
|
}
|
|
55
69
|
|
|
56
|
-
|
|
57
|
-
|
|
70
|
+
// 异步派生:600k 次迭代约数百毫秒,同步实现会卡住整个事件循环
|
|
71
|
+
async _hashPassword(password, salt, iterations = PBKDF2_ITERATIONS) {
|
|
72
|
+
const derived = await pbkdf2Async(String(password), salt, iterations, PBKDF2_KEYLEN, 'sha256')
|
|
73
|
+
return `${PBKDF2_PREFIX}${iterations}$${derived.toString('hex')}`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 校验密码与存储哈希;兼容裸 hex 旧格式(10000 次)。返回 legacy 标记供透明升级。
|
|
77
|
+
async _verifyHash(inputPassword, storedHash, storedSalt) {
|
|
78
|
+
if (!storedHash || !storedSalt) return { ok: false, legacy: false }
|
|
79
|
+
let iterations = LEGACY_PBKDF2_ITERATIONS
|
|
80
|
+
let expected = storedHash
|
|
81
|
+
if (storedHash.startsWith(PBKDF2_PREFIX)) {
|
|
82
|
+
const rest = storedHash.slice(PBKDF2_PREFIX.length)
|
|
83
|
+
const sep = rest.indexOf('$')
|
|
84
|
+
if (sep === -1) return { ok: false, legacy: false }
|
|
85
|
+
iterations = Number(rest.slice(0, sep))
|
|
86
|
+
expected = rest.slice(sep + 1)
|
|
87
|
+
if (!Number.isFinite(iterations) || iterations < 1) return { ok: false, legacy: false }
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const derived = await pbkdf2Async(String(inputPassword), storedSalt, iterations, PBKDF2_KEYLEN, 'sha256')
|
|
91
|
+
const expectedBuf = Buffer.from(expected, 'hex')
|
|
92
|
+
const ok = derived.length === expectedBuf.length && timingSafeEqual(derived, expectedBuf)
|
|
93
|
+
return { ok, legacy: ok && !storedHash.startsWith(PBKDF2_PREFIX) }
|
|
94
|
+
} catch {
|
|
95
|
+
return { ok: false, legacy: false }
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 旧格式哈希在登录成功后透明升级到当前格式(新盐 + 新迭代数)并持久化
|
|
100
|
+
async _upgradeHash(kind, password) {
|
|
101
|
+
const salt = randomBytes(16).toString('hex')
|
|
102
|
+
const hash = await this._hashPassword(password, salt)
|
|
103
|
+
if (kind === 'admin') {
|
|
104
|
+
this.adminPasswordHash = hash
|
|
105
|
+
this.adminPasswordSalt = salt
|
|
106
|
+
} else {
|
|
107
|
+
this.passwordHash = hash
|
|
108
|
+
this.passwordSalt = salt
|
|
109
|
+
}
|
|
110
|
+
await this._persist()
|
|
58
111
|
}
|
|
59
112
|
|
|
60
113
|
get hasPassword() {
|
|
@@ -78,6 +131,7 @@ export class AuthManager {
|
|
|
78
131
|
mode: this.mode,
|
|
79
132
|
scope: this.scope,
|
|
80
133
|
adminPolicy: this.adminPolicy,
|
|
134
|
+
adminProtection: this.adminProtection,
|
|
81
135
|
hasPassword: this.hasPassword,
|
|
82
136
|
hasAdminPassword: this.hasAdminPassword,
|
|
83
137
|
secretToken: token,
|
|
@@ -107,6 +161,7 @@ export class AuthManager {
|
|
|
107
161
|
mode: this.mode,
|
|
108
162
|
scope: this.scope,
|
|
109
163
|
adminPolicy: this.adminPolicy,
|
|
164
|
+
adminProtection: this.adminProtection,
|
|
110
165
|
hasPassword: this.hasPassword,
|
|
111
166
|
hasAdminPassword: this.hasAdminPassword,
|
|
112
167
|
allowLoopback: this.allowLoopback,
|
|
@@ -120,6 +175,13 @@ export class AuthManager {
|
|
|
120
175
|
await this._persist()
|
|
121
176
|
}
|
|
122
177
|
|
|
178
|
+
/** 管理保护独立开关:关闭后管理操作免 adminToken(与访问认证解耦) */
|
|
179
|
+
async setAdminProtection(protection) {
|
|
180
|
+
this.adminProtection = protection !== false
|
|
181
|
+
if (!this.adminProtection) this.adminSessions.clear()
|
|
182
|
+
await this._persist()
|
|
183
|
+
}
|
|
184
|
+
|
|
123
185
|
async setMode(mode) {
|
|
124
186
|
if (['token_and_password', 'password_only', 'token_only'].includes(mode)) {
|
|
125
187
|
this.mode = mode
|
|
@@ -154,8 +216,7 @@ export class AuthManager {
|
|
|
154
216
|
this.passwordSalt = ''
|
|
155
217
|
} else {
|
|
156
218
|
const salt = randomBytes(16).toString('hex')
|
|
157
|
-
|
|
158
|
-
this.passwordHash = hash
|
|
219
|
+
this.passwordHash = await this._hashPassword(password, salt)
|
|
159
220
|
this.passwordSalt = salt
|
|
160
221
|
}
|
|
161
222
|
// 更改访问密码时使所有普通访客 Session 失效
|
|
@@ -170,8 +231,7 @@ export class AuthManager {
|
|
|
170
231
|
this.adminPasswordSalt = ''
|
|
171
232
|
} else {
|
|
172
233
|
const salt = randomBytes(16).toString('hex')
|
|
173
|
-
|
|
174
|
-
this.adminPasswordHash = hash
|
|
234
|
+
this.adminPasswordHash = await this._hashPassword(password, salt)
|
|
175
235
|
this.adminPasswordSalt = salt
|
|
176
236
|
}
|
|
177
237
|
// 更改管理密码时使所有远程管理员解锁 Session 失效
|
|
@@ -212,7 +272,7 @@ export class AuthManager {
|
|
|
212
272
|
if (adminToken) this.adminSessions.delete(adminToken)
|
|
213
273
|
}
|
|
214
274
|
|
|
215
|
-
verifyAdminPassword(inputPassword, clientIp = '') {
|
|
275
|
+
async verifyAdminPassword(inputPassword, clientIp = '') {
|
|
216
276
|
if (this.isIpBlocked(clientIp)) {
|
|
217
277
|
return { success: false, error: '尝试次数过多,请稍后再试' }
|
|
218
278
|
}
|
|
@@ -224,11 +284,12 @@ export class AuthManager {
|
|
|
224
284
|
return { success: true }
|
|
225
285
|
}
|
|
226
286
|
|
|
227
|
-
const
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
287
|
+
const check = await this._verifyHash(inputPassword, targetHash, targetSalt)
|
|
288
|
+
if (check.ok) {
|
|
289
|
+
// 旧格式透明升级:管理位为空时实际校验的是访客密码,升级落在对应字段
|
|
290
|
+
if (check.legacy) {
|
|
291
|
+
await this._upgradeHash(this.adminPasswordHash ? 'admin' : 'password', inputPassword)
|
|
292
|
+
}
|
|
232
293
|
this.recordSuccess(clientIp)
|
|
233
294
|
return { success: true }
|
|
234
295
|
}
|
|
@@ -237,20 +298,32 @@ export class AuthManager {
|
|
|
237
298
|
return { success: false, error: '管理员密码错误' }
|
|
238
299
|
}
|
|
239
300
|
|
|
240
|
-
unlockAdmin(password, clientIp = '') {
|
|
301
|
+
async unlockAdmin(password, clientIp = '') {
|
|
241
302
|
if (this.adminPolicy === 'local_only') {
|
|
242
303
|
return { ok: false, error: '当前已配置为仅限电脑本机管理设置' }
|
|
243
304
|
}
|
|
305
|
+
// RPC 层拿不到真实 clientIp(代理以回环转发),因此用独立的全局失败计数防爆破:
|
|
306
|
+
// 攻击者 5 次失败即锁 60s,暴力破解不可行;代价是攻击者可短时干扰解锁(可接受的权衡)
|
|
307
|
+
if (this._adminUnlockLockUntil && Date.now() < this._adminUnlockLockUntil) {
|
|
308
|
+
return { ok: false, error: '尝试次数过多,请 60 秒后再试' }
|
|
309
|
+
}
|
|
244
310
|
const hasAnyPassword = this.hasAdminPassword || this.hasPassword
|
|
245
311
|
if (!hasAnyPassword) {
|
|
246
312
|
const adminToken = this.createAdminSession()
|
|
247
313
|
return { ok: true, adminToken }
|
|
248
314
|
}
|
|
249
|
-
const verify = this.verifyAdminPassword(password, clientIp)
|
|
315
|
+
const verify = await this.verifyAdminPassword(password, clientIp)
|
|
250
316
|
if (verify.success) {
|
|
317
|
+
this._adminUnlockFailures = 0
|
|
251
318
|
const adminToken = this.createAdminSession()
|
|
252
319
|
return { ok: true, adminToken }
|
|
253
320
|
}
|
|
321
|
+
this._adminUnlockFailures = (this._adminUnlockFailures || 0) + 1
|
|
322
|
+
if (this._adminUnlockFailures >= MAX_FAILED_ATTEMPTS) {
|
|
323
|
+
this._adminUnlockLockUntil = Date.now() + LOCKOUT_PERIOD_MS
|
|
324
|
+
this._adminUnlockFailures = 0
|
|
325
|
+
this.logger.warn?.('[dsh-bridge auth] admin unlock locked out for 60s due to repeated failed attempts')
|
|
326
|
+
}
|
|
254
327
|
return { ok: false, error: verify.error || '管理员密码错误' }
|
|
255
328
|
}
|
|
256
329
|
|
|
@@ -262,6 +335,7 @@ export class AuthManager {
|
|
|
262
335
|
mode: this.mode,
|
|
263
336
|
scope: this.scope,
|
|
264
337
|
adminPolicy: this.adminPolicy,
|
|
338
|
+
adminProtection: this.adminProtection,
|
|
265
339
|
passwordHash: this.passwordHash,
|
|
266
340
|
passwordSalt: this.passwordSalt,
|
|
267
341
|
adminPasswordHash: this.adminPasswordHash,
|
|
@@ -280,7 +354,7 @@ export class AuthManager {
|
|
|
280
354
|
/**
|
|
281
355
|
* 校验客户端密码
|
|
282
356
|
*/
|
|
283
|
-
verifyPassword(inputPassword, clientIp = '') {
|
|
357
|
+
async verifyPassword(inputPassword, clientIp = '') {
|
|
284
358
|
if (this.isIpBlocked(clientIp)) {
|
|
285
359
|
return { success: false, error: '尝试次数过多,请稍后再试' }
|
|
286
360
|
}
|
|
@@ -298,11 +372,12 @@ export class AuthManager {
|
|
|
298
372
|
return { success: true }
|
|
299
373
|
}
|
|
300
374
|
|
|
301
|
-
const
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
375
|
+
const check = await this._verifyHash(inputPassword, this.passwordHash, this.passwordSalt)
|
|
376
|
+
if (check.ok) {
|
|
377
|
+
// 旧格式(裸 hex,10000 次)透明升级到当前格式
|
|
378
|
+
if (check.legacy) {
|
|
379
|
+
await this._upgradeHash('password', inputPassword)
|
|
380
|
+
}
|
|
306
381
|
this.recordSuccess(clientIp)
|
|
307
382
|
return { success: true }
|
|
308
383
|
}
|
|
@@ -467,4 +542,4 @@ export class AuthManager {
|
|
|
467
542
|
this.sessions.clear()
|
|
468
543
|
this.rateLimits.clear()
|
|
469
544
|
}
|
|
470
|
-
}
|
|
545
|
+
}
|
package/lib/bridge-rpc.js
CHANGED
|
@@ -33,7 +33,7 @@ async function renderQr(loginState) {
|
|
|
33
33
|
if (!loginState?.qrPayload) return null;
|
|
34
34
|
const cacheKey = `${loginState.qrKind}:${loginState.qrPayload.slice(0, 80)}`;
|
|
35
35
|
if (renderQr.cache && renderQr.cache.key === cacheKey) return renderQr.cache.url;
|
|
36
|
-
let url
|
|
36
|
+
let url;
|
|
37
37
|
const payload = loginState.qrPayload;
|
|
38
38
|
if (loginState.qrKind === 'img') {
|
|
39
39
|
url = /^data:/i.test(payload) ? payload : `data:image/png;base64,${payload}`;
|
|
@@ -48,21 +48,29 @@ async function renderQr(loginState) {
|
|
|
48
48
|
return url;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
|
|
52
|
-
async function wechatStatusValue(wechatService, logger) {
|
|
53
|
-
const status = wechatService.getStatus();
|
|
54
|
-
const qr = await renderQr(status.login).catch((err) => {
|
|
55
|
-
logger.warn('dsh-bridge: render wechat qr failed: %s', err?.message ?? err);
|
|
56
|
-
return null;
|
|
57
|
-
});
|
|
58
|
-
return { ...status, login: { ...status.login, qr, qrPayload: undefined, qrKind: undefined } };
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function checkAdminAuth(authManager, payload) {
|
|
51
|
+
function checkAdminAuth(authManager, payload, { requireConfigured = false } = {}) {
|
|
62
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;
|
|
63
65
|
if (authManager.adminPolicy === 'open') return null;
|
|
64
66
|
// 若系统尚未设置任何管理密码或访客密码,允许免密管理
|
|
65
67
|
const hasAnyPassword = authManager.hasAdminPassword || authManager.hasPassword;
|
|
68
|
+
// T2.9:高危操作(备份导出/导入、隧道配置与启动、目录浏览、添加工作区、升级、重启)
|
|
69
|
+
// 在系统从未设置任何密码时不再静默放行,强制先完成一次密码设置,
|
|
70
|
+
// 杜绝"未设密码 = 局域网/隧道内任何人都可导出全部凭证"的裸奔状态被直接利用
|
|
71
|
+
if (requireConfigured && !hasAnyPassword) {
|
|
72
|
+
return fail('bad-request', '该操作涉及敏感配置:请先在「安全认证」中设置访问密码或管理密码后再执行');
|
|
73
|
+
}
|
|
66
74
|
if (!hasAnyPassword) {
|
|
67
75
|
return null;
|
|
68
76
|
}
|
|
@@ -73,7 +81,7 @@ function checkAdminAuth(authManager, payload) {
|
|
|
73
81
|
return fail('bad-request', '操作已被拦截:需要管理员权限,请先在控制台输入管理密码解锁');
|
|
74
82
|
}
|
|
75
83
|
|
|
76
|
-
export function installBridgeRpc(ctx, { service, authManager,
|
|
84
|
+
export function installBridgeRpc(ctx, { service, authManager, platformManager, logger, saveCustomTunnelConfig, exportBackup, importBackup }) {
|
|
77
85
|
if (!ctx?.connection?.rpc?.handle) {
|
|
78
86
|
logger.warn('dsh-bridge: Connection RPC unavailable — UI will not work');
|
|
79
87
|
return () => {};
|
|
@@ -101,14 +109,21 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
101
109
|
|
|
102
110
|
if (endpoint === BRIDGE_ENDPOINTS.authUpdateConfig) {
|
|
103
111
|
if (!authManager) return fail('bad-request', 'AuthManager 未初始化');
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
+
}
|
|
108
122
|
if (enabled != null) await authManager.setEnabled(enabled);
|
|
109
123
|
if (mode != null) await authManager.setMode(mode);
|
|
110
124
|
if (scope != null) await authManager.setScope(scope);
|
|
111
125
|
if (adminPolicy != null) await authManager.setAdminPolicy(adminPolicy);
|
|
126
|
+
if (adminProtection != null) await authManager.setAdminProtection(adminProtection);
|
|
112
127
|
if (password !== undefined) await authManager.setPassword(password);
|
|
113
128
|
if (adminPassword !== undefined) await authManager.setAdminPassword(adminPassword);
|
|
114
129
|
return ok(authManager.getStatus({ masked: false }));
|
|
@@ -126,7 +141,7 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
126
141
|
if (endpoint === BRIDGE_ENDPOINTS.authAdminUnlock) {
|
|
127
142
|
if (!authManager) return fail('bad-request', 'AuthManager 未初始化');
|
|
128
143
|
const { password } = payload;
|
|
129
|
-
const res = authManager.unlockAdmin(password);
|
|
144
|
+
const res = await authManager.unlockAdmin(password);
|
|
130
145
|
if (res.ok) return ok({ adminToken: res.adminToken });
|
|
131
146
|
return fail('bad-request', res.error || '管理员密码错误');
|
|
132
147
|
}
|
|
@@ -138,27 +153,28 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
138
153
|
}
|
|
139
154
|
|
|
140
155
|
if (endpoint === BRIDGE_ENDPOINTS.saveCustomTunnelConfig) {
|
|
141
|
-
const adminErr = checkAdminAuth(authManager, payload);
|
|
156
|
+
const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
|
|
142
157
|
if (adminErr) return adminErr;
|
|
143
158
|
|
|
144
|
-
|
|
145
|
-
|
|
159
|
+
// 未提供的字段保持 undefined 透传:服务端视为"保留现值"
|
|
160
|
+
const { serverUrl, accessToken } = payload;
|
|
161
|
+
await saveCustomTunnelConfig(serverUrl, accessToken);
|
|
146
162
|
const status = await service.getStatus();
|
|
147
163
|
return ok(status);
|
|
148
164
|
}
|
|
149
165
|
|
|
150
166
|
if (endpoint === BRIDGE_ENDPOINTS.saveCloudflaredConfig) {
|
|
151
|
-
const adminErr = checkAdminAuth(authManager, payload);
|
|
167
|
+
const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
|
|
152
168
|
if (adminErr) return adminErr;
|
|
153
169
|
|
|
154
|
-
const { token
|
|
170
|
+
const { token, hostname } = payload;
|
|
155
171
|
await service.saveCloudflaredConfig({ token, hostname });
|
|
156
172
|
const status = await service.getStatus();
|
|
157
173
|
return ok(status);
|
|
158
174
|
}
|
|
159
175
|
|
|
160
176
|
if (endpoint === BRIDGE_ENDPOINTS.setTunnelAutoStart) {
|
|
161
|
-
const adminErr = checkAdminAuth(authManager, payload);
|
|
177
|
+
const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
|
|
162
178
|
if (adminErr) return adminErr;
|
|
163
179
|
|
|
164
180
|
const { tunnel, autoStart } = payload;
|
|
@@ -177,7 +193,7 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
177
193
|
}
|
|
178
194
|
|
|
179
195
|
if (endpoint === BRIDGE_ENDPOINTS.startCustomTunnel) {
|
|
180
|
-
const adminErr = checkAdminAuth(authManager, payload);
|
|
196
|
+
const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
|
|
181
197
|
if (adminErr) return adminErr;
|
|
182
198
|
|
|
183
199
|
try {
|
|
@@ -200,7 +216,7 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
200
216
|
}
|
|
201
217
|
|
|
202
218
|
if (endpoint === BRIDGE_ENDPOINTS.startCloudflared) {
|
|
203
|
-
const adminErr = checkAdminAuth(authManager, payload);
|
|
219
|
+
const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
|
|
204
220
|
if (adminErr) return adminErr;
|
|
205
221
|
|
|
206
222
|
try {
|
|
@@ -237,7 +253,7 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
237
253
|
}
|
|
238
254
|
|
|
239
255
|
if (endpoint === BRIDGE_ENDPOINTS.upgradePlugin) {
|
|
240
|
-
const adminErr = checkAdminAuth(authManager, payload);
|
|
256
|
+
const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
|
|
241
257
|
if (adminErr) return adminErr;
|
|
242
258
|
|
|
243
259
|
const result = await service.upgradePlugin(payload);
|
|
@@ -245,7 +261,7 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
245
261
|
}
|
|
246
262
|
|
|
247
263
|
if (endpoint === BRIDGE_ENDPOINTS.restartDsh) {
|
|
248
|
-
const adminErr = checkAdminAuth(authManager, payload);
|
|
264
|
+
const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
|
|
249
265
|
if (adminErr) return adminErr;
|
|
250
266
|
|
|
251
267
|
const result = await service.restartDsh();
|
|
@@ -253,7 +269,7 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
253
269
|
}
|
|
254
270
|
|
|
255
271
|
if (endpoint === BRIDGE_ENDPOINTS.exportBackup) {
|
|
256
|
-
const adminErr = checkAdminAuth(authManager, payload);
|
|
272
|
+
const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
|
|
257
273
|
if (adminErr) return adminErr;
|
|
258
274
|
|
|
259
275
|
if (!exportBackup) return fail('bad-request', '备份导出服务不可用');
|
|
@@ -262,7 +278,7 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
262
278
|
}
|
|
263
279
|
|
|
264
280
|
if (endpoint === BRIDGE_ENDPOINTS.importBackup) {
|
|
265
|
-
const adminErr = checkAdminAuth(authManager, payload);
|
|
281
|
+
const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
|
|
266
282
|
if (adminErr) return adminErr;
|
|
267
283
|
|
|
268
284
|
if (!importBackup) return fail('bad-request', '备份导入服务不可用');
|
|
@@ -284,7 +300,7 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
284
300
|
// ---- 远程工作区管理与目录浏览 ----
|
|
285
301
|
|
|
286
302
|
if (endpoint === BRIDGE_ENDPOINTS.listRemoteDirectories) {
|
|
287
|
-
const adminErr = checkAdminAuth(authManager, payload);
|
|
303
|
+
const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
|
|
288
304
|
if (adminErr) return adminErr;
|
|
289
305
|
|
|
290
306
|
const clientKey = payload?.clientIp || payload?.adminToken || 'default';
|
|
@@ -298,7 +314,7 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
298
314
|
}
|
|
299
315
|
|
|
300
316
|
if (endpoint === BRIDGE_ENDPOINTS.addRemoteWorkspace) {
|
|
301
|
-
const adminErr = checkAdminAuth(authManager, payload);
|
|
317
|
+
const adminErr = checkAdminAuth(authManager, payload, { requireConfigured: true });
|
|
302
318
|
if (adminErr) return adminErr;
|
|
303
319
|
|
|
304
320
|
const clientKey = payload?.clientIp || payload?.adminToken || 'default';
|
|
@@ -427,78 +443,6 @@ export function installBridgeRpc(ctx, { service, authManager, wechat, platformMa
|
|
|
427
443
|
return ok({ ...status, login: { ...status.login, qr, qrPayload: undefined, qrKind: undefined } });
|
|
428
444
|
}
|
|
429
445
|
|
|
430
|
-
// ---- 微信 Bot(v1.x 向后兼容别名,deprecated)----
|
|
431
|
-
|
|
432
|
-
if (endpoint === BRIDGE_ENDPOINTS.wechatGetStatus) {
|
|
433
|
-
if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
|
|
434
|
-
const value = await wechatStatusValue(wechat, logger);
|
|
435
|
-
return ok(value);
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
if (endpoint === BRIDGE_ENDPOINTS.wechatLogin) {
|
|
439
|
-
if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
|
|
440
|
-
const adminErr = checkAdminAuth(authManager, payload);
|
|
441
|
-
if (adminErr) return adminErr;
|
|
442
|
-
|
|
443
|
-
const { qrType } = payload;
|
|
444
|
-
const result = await wechat.login({ qrType });
|
|
445
|
-
if (!result.ok) return fail('bad-request', result.error ?? '登录启动失败');
|
|
446
|
-
const value = await wechatStatusValue(wechat, logger);
|
|
447
|
-
return ok(value);
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
if (endpoint === BRIDGE_ENDPOINTS.wechatSetAllowFrom) {
|
|
451
|
-
if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
|
|
452
|
-
const adminErr = checkAdminAuth(authManager, payload);
|
|
453
|
-
if (adminErr) return adminErr;
|
|
454
|
-
|
|
455
|
-
await wechat.setAllowFrom(payload.allowFrom);
|
|
456
|
-
const value = await wechatStatusValue(wechat, logger);
|
|
457
|
-
return ok(value);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
if (endpoint === BRIDGE_ENDPOINTS.wechatSetConfig) {
|
|
461
|
-
if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
|
|
462
|
-
const adminErr = checkAdminAuth(authManager, payload);
|
|
463
|
-
if (adminErr) return adminErr;
|
|
464
|
-
|
|
465
|
-
await wechat.setConfig(payload);
|
|
466
|
-
const value = await wechatStatusValue(wechat, logger);
|
|
467
|
-
return ok(value);
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
if (endpoint === BRIDGE_ENDPOINTS.wechatStop) {
|
|
471
|
-
if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
|
|
472
|
-
const adminErr = checkAdminAuth(authManager, payload);
|
|
473
|
-
if (adminErr) return adminErr;
|
|
474
|
-
|
|
475
|
-
await wechat.stop();
|
|
476
|
-
const value = await wechatStatusValue(wechat, logger);
|
|
477
|
-
return ok(value);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
if (endpoint === BRIDGE_ENDPOINTS.wechatStart) {
|
|
481
|
-
if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
|
|
482
|
-
const adminErr = checkAdminAuth(authManager, payload);
|
|
483
|
-
if (adminErr) return adminErr;
|
|
484
|
-
|
|
485
|
-
await wechat.gateway.start().catch((err) => {
|
|
486
|
-
logger.error('wechat start enabled: %s', err?.message ?? err);
|
|
487
|
-
});
|
|
488
|
-
const value = await wechatStatusValue(wechat, logger);
|
|
489
|
-
return ok(value);
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
if (endpoint === BRIDGE_ENDPOINTS.wechatUnbind) {
|
|
493
|
-
if (!wechat) return fail('bad-request', '微信 Bot 未初始化');
|
|
494
|
-
const adminErr = checkAdminAuth(authManager, payload);
|
|
495
|
-
if (adminErr) return adminErr;
|
|
496
|
-
|
|
497
|
-
await wechat.unbind();
|
|
498
|
-
const value = await wechatStatusValue(wechat, logger);
|
|
499
|
-
return ok(value);
|
|
500
|
-
}
|
|
501
|
-
|
|
502
446
|
return fail('bad-request', `Unknown endpoint: ${endpoint}`);
|
|
503
447
|
} catch (err) {
|
|
504
448
|
logger.error('RPC endpoint %s failed: %s', endpoint, err.message);
|