@wenbin_wb/dsh-bridge 2.3.3 → 2.5.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/README.en.md +85 -8
- package/README.md +85 -8
- package/client/client.js +832 -57
- package/client/index.js +718 -61
- package/docs/telegram-usage.md +89 -0
- package/lib/auth/login-template.js +378 -0
- package/lib/auth/manager.js +425 -0
- package/lib/bridge-rpc-constants.js +6 -0
- package/lib/bridge-rpc.js +34 -1
- package/lib/feishu/gateway.js +166 -7
- package/lib/feishu/node.js +55 -10
- package/lib/index.js +258 -14
- package/lib/platform/base.js +13 -0
- package/lib/platform/conversation-bridge.js +32 -4
- package/lib/qq/gateway.js +94 -14
- package/lib/qq/node.js +49 -9
- package/lib/telegram/gateway.js +619 -0
- package/lib/telegram/index.js +210 -0
- package/lib/telegram/node.js +342 -0
- package/lib/wechat/gateway.js +54 -0
- package/lib/wechat/node.js +1 -0
- package/package.json +4 -3
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
// lib/auth/manager.js
|
|
2
|
+
// 远程访问安全认证管理器:密码加盐哈希、Session 管理、免密安全 Token、防暴力破解与全协议鉴权拦截
|
|
3
|
+
|
|
4
|
+
import { randomBytes, pbkdf2Sync, timingSafeEqual } from 'node:crypto'
|
|
5
|
+
|
|
6
|
+
const DEFAULT_SESSION_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000 // 30 天
|
|
7
|
+
const MAX_FAILED_ATTEMPTS = 5
|
|
8
|
+
const LOCKOUT_PERIOD_MS = 60 * 1000 // 连续失败封禁 60 秒
|
|
9
|
+
|
|
10
|
+
export class AuthManager {
|
|
11
|
+
/**
|
|
12
|
+
* @param {object} opts
|
|
13
|
+
* @param {object} [opts.config]
|
|
14
|
+
* @param {boolean} [opts.config.enabled=false]
|
|
15
|
+
* @param {'token_and_password'|'password_only'|'token_only'} [opts.config.mode='token_and_password']
|
|
16
|
+
* @param {string} [opts.config.passwordHash]
|
|
17
|
+
* @param {string} [opts.config.passwordSalt]
|
|
18
|
+
* @param {string} [opts.config.secretToken]
|
|
19
|
+
* @param {boolean} [opts.config.allowLoopback=true]
|
|
20
|
+
* @param {(patch: object) => Promise<void>|void} [opts.onPersist]
|
|
21
|
+
* @param {object} [opts.logger]
|
|
22
|
+
*/
|
|
23
|
+
constructor({ config = {}, onPersist, logger = console } = {}) {
|
|
24
|
+
this.logger = logger
|
|
25
|
+
this.onPersist = onPersist
|
|
26
|
+
|
|
27
|
+
this.enabled = Boolean(config.enabled)
|
|
28
|
+
this.mode = config.mode || 'token_and_password'
|
|
29
|
+
this.scope = config.scope || 'all' // 'all' | 'public_only' | 'lan_only'
|
|
30
|
+
this.adminPolicy = config.adminPolicy || 'password_unlock' // 'password_unlock' | 'local_only' | 'open'
|
|
31
|
+
this.passwordHash = config.passwordHash || ''
|
|
32
|
+
this.passwordSalt = config.passwordSalt || ''
|
|
33
|
+
this.adminPasswordHash = config.adminPasswordHash || ''
|
|
34
|
+
this.adminPasswordSalt = config.adminPasswordSalt || ''
|
|
35
|
+
this.secretToken = config.secretToken || this._generateToken()
|
|
36
|
+
this.allowLoopback = config.allowLoopback !== false
|
|
37
|
+
|
|
38
|
+
// Session 内存存储:sessionToken -> { createdAt, expiresAt }
|
|
39
|
+
this.sessions = new Map()
|
|
40
|
+
// 管理员解锁 Session 内存存储:adminToken -> { createdAt, expiresAt }
|
|
41
|
+
this.adminSessions = new Map()
|
|
42
|
+
// 防暴力破解:ip -> { failedCount, lockUntil }
|
|
43
|
+
this.rateLimits = new Map()
|
|
44
|
+
|
|
45
|
+
// 定期清理过期 Session (每小时)
|
|
46
|
+
this._cleanupTimer = setInterval(() => this._cleanupExpired(), 60 * 60 * 1000)
|
|
47
|
+
if (this._cleanupTimer.unref) this._cleanupTimer.unref()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
_generateToken() {
|
|
51
|
+
return 'dsh_' + randomBytes(18).toString('hex')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_hashPassword(password, salt) {
|
|
55
|
+
return pbkdf2Sync(String(password), salt, 10000, 32, 'sha256').toString('hex')
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
get hasPassword() {
|
|
59
|
+
return Boolean(this.passwordHash && this.passwordSalt)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
get hasAdminPassword() {
|
|
63
|
+
return Boolean(this.adminPasswordHash && this.adminPasswordSalt)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
getStatus() {
|
|
67
|
+
return {
|
|
68
|
+
enabled: this.enabled,
|
|
69
|
+
mode: this.mode,
|
|
70
|
+
scope: this.scope,
|
|
71
|
+
adminPolicy: this.adminPolicy,
|
|
72
|
+
hasPassword: this.hasPassword,
|
|
73
|
+
hasAdminPassword: this.hasAdminPassword,
|
|
74
|
+
secretToken: this.secretToken,
|
|
75
|
+
allowLoopback: this.allowLoopback,
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async setEnabled(enabled) {
|
|
80
|
+
this.enabled = Boolean(enabled)
|
|
81
|
+
this.sessions.clear()
|
|
82
|
+
this.adminSessions.clear()
|
|
83
|
+
await this._persist()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async setMode(mode) {
|
|
87
|
+
if (['token_and_password', 'password_only', 'token_only'].includes(mode)) {
|
|
88
|
+
this.mode = mode
|
|
89
|
+
// 切换认证模式时清空所有现有 Session,确保新策略立即对所有设备生效
|
|
90
|
+
this.sessions.clear()
|
|
91
|
+
this.adminSessions.clear()
|
|
92
|
+
await this._persist()
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async setScope(scope) {
|
|
97
|
+
if (['all', 'public_only', 'lan_only'].includes(scope)) {
|
|
98
|
+
this.scope = scope
|
|
99
|
+
this.sessions.clear()
|
|
100
|
+
this.adminSessions.clear()
|
|
101
|
+
await this._persist()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async setAdminPolicy(policy) {
|
|
106
|
+
if (['password_unlock', 'local_only', 'open'].includes(policy)) {
|
|
107
|
+
this.adminPolicy = policy
|
|
108
|
+
this.adminSessions.clear()
|
|
109
|
+
await this._persist()
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 设置外部访客访问密码
|
|
114
|
+
async setPassword(password) {
|
|
115
|
+
if (!password) {
|
|
116
|
+
this.passwordHash = ''
|
|
117
|
+
this.passwordSalt = ''
|
|
118
|
+
} else {
|
|
119
|
+
const salt = randomBytes(16).toString('hex')
|
|
120
|
+
const hash = this._hashPassword(password, salt)
|
|
121
|
+
this.passwordHash = hash
|
|
122
|
+
this.passwordSalt = salt
|
|
123
|
+
}
|
|
124
|
+
// 更改访问密码时使所有普通访客 Session 失效
|
|
125
|
+
this.sessions.clear()
|
|
126
|
+
await this._persist()
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 设置后台管理员密码
|
|
130
|
+
async setAdminPassword(password) {
|
|
131
|
+
if (!password) {
|
|
132
|
+
this.adminPasswordHash = ''
|
|
133
|
+
this.adminPasswordSalt = ''
|
|
134
|
+
} else {
|
|
135
|
+
const salt = randomBytes(16).toString('hex')
|
|
136
|
+
const hash = this._hashPassword(password, salt)
|
|
137
|
+
this.adminPasswordHash = hash
|
|
138
|
+
this.adminPasswordSalt = salt
|
|
139
|
+
}
|
|
140
|
+
// 更改管理密码时使所有远程管理员解锁 Session 失效
|
|
141
|
+
this.adminSessions.clear()
|
|
142
|
+
await this._persist()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async regenerateSecretToken() {
|
|
146
|
+
this.secretToken = this._generateToken()
|
|
147
|
+
this.sessions.clear()
|
|
148
|
+
await this._persist()
|
|
149
|
+
return this.secretToken
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// 管理员解锁控制
|
|
153
|
+
createAdminSession(maxAgeMs = 30 * 60 * 1000) {
|
|
154
|
+
const adminToken = randomBytes(24).toString('hex')
|
|
155
|
+
const now = Date.now()
|
|
156
|
+
this.adminSessions.set(adminToken, {
|
|
157
|
+
createdAt: now,
|
|
158
|
+
expiresAt: now + maxAgeMs,
|
|
159
|
+
})
|
|
160
|
+
return adminToken
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
validateAdminSession(adminToken) {
|
|
164
|
+
if (!adminToken) return false
|
|
165
|
+
const sess = this.adminSessions.get(adminToken)
|
|
166
|
+
if (!sess) return false
|
|
167
|
+
if (Date.now() > sess.expiresAt) {
|
|
168
|
+
this.adminSessions.delete(adminToken)
|
|
169
|
+
return false
|
|
170
|
+
}
|
|
171
|
+
return true
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
verifyAdminPassword(inputPassword, clientIp = '') {
|
|
175
|
+
if (this.isIpBlocked(clientIp)) {
|
|
176
|
+
return { success: false, error: '尝试次数过多,请稍后再试' }
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const targetHash = this.adminPasswordHash || this.passwordHash
|
|
180
|
+
const targetSalt = this.adminPasswordSalt || this.passwordSalt
|
|
181
|
+
|
|
182
|
+
if (!targetHash || !targetSalt) {
|
|
183
|
+
return { success: true }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const inputHash = this._hashPassword(inputPassword, targetSalt)
|
|
187
|
+
const bufA = Buffer.from(inputHash, 'hex')
|
|
188
|
+
const bufB = Buffer.from(targetHash, 'hex')
|
|
189
|
+
|
|
190
|
+
if (bufA.length === bufB.length && timingSafeEqual(bufA, bufB)) {
|
|
191
|
+
this.recordSuccess(clientIp)
|
|
192
|
+
return { success: true }
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
this.recordFailedAttempt(clientIp)
|
|
196
|
+
return { success: false, error: '管理员密码错误' }
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
unlockAdmin(password, clientIp = '') {
|
|
200
|
+
if (this.adminPolicy === 'local_only') {
|
|
201
|
+
return { ok: false, error: '当前已配置为仅限电脑本机管理设置' }
|
|
202
|
+
}
|
|
203
|
+
const hasAnyPassword = this.hasAdminPassword || this.hasPassword
|
|
204
|
+
if (!hasAnyPassword) {
|
|
205
|
+
const adminToken = this.createAdminSession()
|
|
206
|
+
return { ok: true, adminToken }
|
|
207
|
+
}
|
|
208
|
+
const verify = this.verifyAdminPassword(password, clientIp)
|
|
209
|
+
if (verify.success) {
|
|
210
|
+
const adminToken = this.createAdminSession()
|
|
211
|
+
return { ok: true, adminToken }
|
|
212
|
+
}
|
|
213
|
+
return { ok: false, error: verify.error || '管理员密码错误' }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async _persist() {
|
|
217
|
+
if (typeof this.onPersist === 'function') {
|
|
218
|
+
try {
|
|
219
|
+
await this.onPersist({
|
|
220
|
+
enabled: this.enabled,
|
|
221
|
+
mode: this.mode,
|
|
222
|
+
scope: this.scope,
|
|
223
|
+
adminPolicy: this.adminPolicy,
|
|
224
|
+
passwordHash: this.passwordHash,
|
|
225
|
+
passwordSalt: this.passwordSalt,
|
|
226
|
+
adminPasswordHash: this.adminPasswordHash,
|
|
227
|
+
adminPasswordSalt: this.adminPasswordSalt,
|
|
228
|
+
secretToken: this.secretToken,
|
|
229
|
+
allowLoopback: this.allowLoopback,
|
|
230
|
+
})
|
|
231
|
+
} catch (err) {
|
|
232
|
+
this.logger.error?.('[dsh-bridge auth] persist failed:', err?.message ?? err)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ---- 认证校验逻辑 ----
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* 校验客户端密码
|
|
241
|
+
*/
|
|
242
|
+
verifyPassword(inputPassword, clientIp = '') {
|
|
243
|
+
if (this.isIpBlocked(clientIp)) {
|
|
244
|
+
return { success: false, error: '尝试次数过多,请稍后再试' }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (!this.hasPassword) {
|
|
248
|
+
// 若处于仅密码模式但尚未设置密码,不允许直接空白登录
|
|
249
|
+
if (this.mode === 'password_only') {
|
|
250
|
+
return { success: false, error: '管理员尚未设置访问密码,请先在控制台中设置密码' }
|
|
251
|
+
}
|
|
252
|
+
return { success: true }
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const inputHash = this._hashPassword(inputPassword, this.passwordSalt)
|
|
256
|
+
const bufA = Buffer.from(inputHash, 'hex')
|
|
257
|
+
const bufB = Buffer.from(this.passwordHash, 'hex')
|
|
258
|
+
|
|
259
|
+
if (bufA.length === bufB.length && timingSafeEqual(bufA, bufB)) {
|
|
260
|
+
this.recordSuccess(clientIp)
|
|
261
|
+
return { success: true }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
this.recordFailedAttempt(clientIp)
|
|
265
|
+
return { success: false, error: '访问密码错误' }
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* 校验安全 Token
|
|
270
|
+
*/
|
|
271
|
+
validateSecretToken(token) {
|
|
272
|
+
if (!token || !this.secretToken) return false
|
|
273
|
+
const bufA = Buffer.from(String(token))
|
|
274
|
+
const bufB = Buffer.from(String(this.secretToken))
|
|
275
|
+
if (bufA.length !== bufB.length) return false
|
|
276
|
+
return timingSafeEqual(bufA, bufB)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* 创建 Session
|
|
281
|
+
*/
|
|
282
|
+
createSession(maxAgeMs = DEFAULT_SESSION_MAX_AGE_MS) {
|
|
283
|
+
const sessionToken = randomBytes(24).toString('hex')
|
|
284
|
+
const now = Date.now()
|
|
285
|
+
this.sessions.set(sessionToken, {
|
|
286
|
+
createdAt: now,
|
|
287
|
+
expiresAt: now + maxAgeMs,
|
|
288
|
+
})
|
|
289
|
+
return sessionToken
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* 校验 Session
|
|
294
|
+
*/
|
|
295
|
+
validateSession(sessionToken) {
|
|
296
|
+
if (!sessionToken) return false
|
|
297
|
+
const sess = this.sessions.get(sessionToken)
|
|
298
|
+
if (!sess) return false
|
|
299
|
+
if (Date.now() > sess.expiresAt) {
|
|
300
|
+
this.sessions.delete(sessionToken)
|
|
301
|
+
return false
|
|
302
|
+
}
|
|
303
|
+
return true
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* 销毁 Session
|
|
308
|
+
*/
|
|
309
|
+
revokeSession(sessionToken) {
|
|
310
|
+
if (sessionToken) this.sessions.delete(sessionToken)
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// ---- 防暴力破解 ----
|
|
314
|
+
|
|
315
|
+
isIpBlocked(ip) {
|
|
316
|
+
if (!ip) return false
|
|
317
|
+
const record = this.rateLimits.get(ip)
|
|
318
|
+
if (!record) return false
|
|
319
|
+
if (record.lockUntil && Date.now() < record.lockUntil) {
|
|
320
|
+
return true
|
|
321
|
+
}
|
|
322
|
+
if (record.lockUntil && Date.now() >= record.lockUntil) {
|
|
323
|
+
this.rateLimits.delete(ip)
|
|
324
|
+
return false
|
|
325
|
+
}
|
|
326
|
+
return false
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
recordFailedAttempt(ip) {
|
|
330
|
+
if (!ip) return
|
|
331
|
+
const now = Date.now()
|
|
332
|
+
const record = this.rateLimits.get(ip) || { failedCount: 0, lockUntil: 0 }
|
|
333
|
+
record.failedCount += 1
|
|
334
|
+
if (record.failedCount >= MAX_FAILED_ATTEMPTS) {
|
|
335
|
+
record.lockUntil = now + LOCKOUT_PERIOD_MS
|
|
336
|
+
this.logger.warn?.(`[dsh-bridge auth] IP ${ip} locked out for 60s due to repeated failed attempts`)
|
|
337
|
+
}
|
|
338
|
+
this.rateLimits.set(ip, record)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
recordSuccess(ip) {
|
|
342
|
+
if (ip) this.rateLimits.delete(ip)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ---- HTTP / WebSocket 请求总入口鉴权 ----
|
|
346
|
+
|
|
347
|
+
/**
|
|
348
|
+
* 检查请求是否已授权
|
|
349
|
+
* @param {import('node:http').IncomingMessage} req
|
|
350
|
+
* @returns {{ authenticated: boolean, fromToken?: boolean, sessionToken?: string, loopback?: boolean, bypass?: boolean }}
|
|
351
|
+
*/
|
|
352
|
+
verifyRequest(req) {
|
|
353
|
+
// 1. 未开启认证 -> 允许直通
|
|
354
|
+
if (!this.enabled) {
|
|
355
|
+
return { authenticated: true, bypass: true }
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// 2. 本地环回免认证(宿主机 127.0.0.1 访问不阻断)
|
|
359
|
+
if (this.allowLoopback) {
|
|
360
|
+
const remote = req.socket?.remoteAddress || ''
|
|
361
|
+
if (remote === '127.0.0.1' || remote === '::1' || remote === '::ffff:127.0.0.1') {
|
|
362
|
+
const host = String(req.headers?.host || '')
|
|
363
|
+
if (host.startsWith('127.0.0.1') || host.startsWith('localhost')) {
|
|
364
|
+
return { authenticated: true, loopback: true }
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// 3. 检查防护范围 (scope: 'all' | 'public_only' | 'lan_only')
|
|
370
|
+
const isPublicTunnel = Boolean(
|
|
371
|
+
req.headers?.['cf-connecting-ip'] ||
|
|
372
|
+
req.headers?.['cf-ray'] ||
|
|
373
|
+
req.headers?.['x-dsh-tunnel'] ||
|
|
374
|
+
(req.headers?.['x-forwarded-proto'] && req.headers?.['x-forwarded-host'])
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
if (this.scope === 'public_only' && !isPublicTunnel) {
|
|
378
|
+
return { authenticated: true, lanBypass: true }
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (this.scope === 'lan_only' && isPublicTunnel) {
|
|
382
|
+
return { authenticated: true, publicBypass: true }
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// 4. 从 Query 参数中解析 ?auth=token 或 ?token=token (用于扫码免密登录)
|
|
386
|
+
if (this.mode !== 'password_only') {
|
|
387
|
+
try {
|
|
388
|
+
const urlObj = new URL(req.url, 'http://localhost')
|
|
389
|
+
const queryToken = urlObj.searchParams.get('auth') || urlObj.searchParams.get('token')
|
|
390
|
+
if (queryToken && this.validateSecretToken(queryToken)) {
|
|
391
|
+
return { authenticated: true, fromToken: true, secretToken: queryToken }
|
|
392
|
+
}
|
|
393
|
+
} catch {}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// 5. 从 Cookie 中解析 Session Token: dsh_bridge_auth=<token>
|
|
397
|
+
const cookieHeader = req.headers?.cookie || ''
|
|
398
|
+
const match = /(?:^|;\s*)dsh_bridge_auth=([a-f0-9]+)/i.exec(cookieHeader)
|
|
399
|
+
if (match) {
|
|
400
|
+
const sessionToken = match[1]
|
|
401
|
+
if (this.validateSession(sessionToken)) {
|
|
402
|
+
return { authenticated: true, sessionToken }
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// 未授权
|
|
407
|
+
return { authenticated: false }
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
_cleanupExpired() {
|
|
411
|
+
const now = Date.now()
|
|
412
|
+
for (const [t, s] of this.sessions.entries()) {
|
|
413
|
+
if (now > s.expiresAt) this.sessions.delete(t)
|
|
414
|
+
}
|
|
415
|
+
for (const [ip, r] of this.rateLimits.entries()) {
|
|
416
|
+
if (r.lockUntil && now > r.lockUntil) this.rateLimits.delete(ip)
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
dispose() {
|
|
421
|
+
clearInterval(this._cleanupTimer)
|
|
422
|
+
this.sessions.clear()
|
|
423
|
+
this.rateLimits.clear()
|
|
424
|
+
}
|
|
425
|
+
}
|
|
@@ -12,6 +12,12 @@ export const BRIDGE_ENDPOINTS = {
|
|
|
12
12
|
saveCustomTunnelConfig: 'saveCustomTunnelConfig',
|
|
13
13
|
checkVersion: 'checkVersion',
|
|
14
14
|
upgradePlugin: 'upgradePlugin',
|
|
15
|
+
// 访问安全认证(密码保护 / 扫码免密 Token)
|
|
16
|
+
authGetStatus: 'authGetStatus',
|
|
17
|
+
authUpdateConfig: 'authUpdateConfig',
|
|
18
|
+
authRegenerateToken: 'authRegenerateToken',
|
|
19
|
+
authAdminUnlock: 'authAdminUnlock',
|
|
20
|
+
authAdminLock: 'authAdminLock',
|
|
15
21
|
// 平台管理器(多 IM 平台统一接口)
|
|
16
22
|
listPlatforms: 'listPlatforms',
|
|
17
23
|
platformLogin: 'platformLogin',
|
package/lib/bridge-rpc.js
CHANGED
|
@@ -54,7 +54,7 @@ async function wechatStatusValue(wechatService, logger) {
|
|
|
54
54
|
return { ...status, login: { ...status.login, qr, qrPayload: undefined, qrKind: undefined } };
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
export function installBridgeRpc(ctx, { service, wechat, platformManager, logger, saveCustomTunnelConfig }) {
|
|
57
|
+
export function installBridgeRpc(ctx, { service, authManager, wechat, platformManager, logger, saveCustomTunnelConfig }) {
|
|
58
58
|
if (!ctx?.connection?.rpc?.handle) {
|
|
59
59
|
logger.warn('dsh-bridge: Connection RPC unavailable — UI will not work');
|
|
60
60
|
return () => {};
|
|
@@ -71,6 +71,39 @@ export function installBridgeRpc(ctx, { service, wechat, platformManager, logger
|
|
|
71
71
|
return ok(status);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
// ---- 访问安全认证 ----
|
|
75
|
+
|
|
76
|
+
if (endpoint === BRIDGE_ENDPOINTS.authGetStatus) {
|
|
77
|
+
if (!authManager) return fail('bad-request', 'AuthManager 未初始化');
|
|
78
|
+
return ok(authManager.getStatus());
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (endpoint === BRIDGE_ENDPOINTS.authUpdateConfig) {
|
|
82
|
+
if (!authManager) return fail('bad-request', 'AuthManager 未初始化');
|
|
83
|
+
const { enabled, mode, scope, adminPolicy, password, adminPassword } = payload;
|
|
84
|
+
if (enabled != null) await authManager.setEnabled(enabled);
|
|
85
|
+
if (mode != null) await authManager.setMode(mode);
|
|
86
|
+
if (scope != null) await authManager.setScope(scope);
|
|
87
|
+
if (adminPolicy != null) await authManager.setAdminPolicy(adminPolicy);
|
|
88
|
+
if (password !== undefined) await authManager.setPassword(password);
|
|
89
|
+
if (adminPassword !== undefined) await authManager.setAdminPassword(adminPassword);
|
|
90
|
+
return ok(authManager.getStatus());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (endpoint === BRIDGE_ENDPOINTS.authRegenerateToken) {
|
|
94
|
+
if (!authManager) return fail('bad-request', 'AuthManager 未初始化');
|
|
95
|
+
await authManager.regenerateSecretToken();
|
|
96
|
+
return ok(authManager.getStatus());
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (endpoint === BRIDGE_ENDPOINTS.authAdminUnlock) {
|
|
100
|
+
if (!authManager) return fail('bad-request', 'AuthManager 未初始化');
|
|
101
|
+
const { password } = payload;
|
|
102
|
+
const res = authManager.unlockAdmin(password);
|
|
103
|
+
if (res.ok) return ok({ adminToken: res.adminToken });
|
|
104
|
+
return fail('bad-request', res.error || '管理员密码错误');
|
|
105
|
+
}
|
|
106
|
+
|
|
74
107
|
if (endpoint === BRIDGE_ENDPOINTS.saveCustomTunnelConfig) {
|
|
75
108
|
const { serverUrl = '', accessToken = '' } = payload;
|
|
76
109
|
await saveCustomTunnelConfig(serverUrl.trim(), accessToken.trim());
|