feat(remote): serial/USB/USBIP remote forwarding with virtual serial port
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
// 远程操作授权闸门: 决定一个对端能不能开 terminal/forward/usb
|
||||
// 规则:
|
||||
// 1. settings.remoteEnabled === false -> 全部拒绝
|
||||
// 2. settings.remoteAllowPeers[peerId][kind] === true -> 自动放行
|
||||
// 3. 否则弹窗 (主进程打开一个 BrowserWindow 询问); 用户可勾选 "记住此对端"
|
||||
// 4. 同一对端 + 同一 kind 24h 内只弹一次 (除非显式 reset)
|
||||
|
||||
import { BrowserWindow } from 'electron'
|
||||
import { getSettings } from '../settings'
|
||||
|
||||
export type RemoteKind = 'terminal' | 'forward' | 'usb'
|
||||
|
||||
interface Pending {
|
||||
resolve: (ok: boolean, remember?: boolean) => void
|
||||
timer: NodeJS.Timeout
|
||||
}
|
||||
|
||||
const pending = new Map<string, Pending>()
|
||||
const approvedAt = new Map<string, number>()
|
||||
const APPROVE_TTL_MS = 24 * 3600_000
|
||||
|
||||
function cacheKey(peerId: string, kind: RemoteKind, nonce?: string) {
|
||||
// nonce 用于 usb.attach 这种需要每次重新确认的场景
|
||||
return nonce ? `${peerId}::${kind}::${nonce}` : `${peerId}::${kind}`
|
||||
}
|
||||
|
||||
export function isAutoAllowed(peerId: string, kind: RemoteKind): boolean {
|
||||
const s = getSettings()
|
||||
if (!s.remoteEnabled) return false
|
||||
const allow = s.remoteAllowPeers?.[peerId]?.[kind]
|
||||
return !!allow
|
||||
}
|
||||
|
||||
export function isRemembered(peerId: string, kind: RemoteKind): boolean {
|
||||
const k = cacheKey(peerId, kind)
|
||||
const ts = approvedAt.get(k)
|
||||
if (!ts) return false
|
||||
if (Date.now() - ts > APPROVE_TTL_MS) {
|
||||
approvedAt.delete(k)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function markApproved(peerId: string, kind: RemoteKind) {
|
||||
approvedAt.set(cacheKey(peerId, kind), Date.now())
|
||||
}
|
||||
|
||||
export function resetApproval(peerId: string, kind?: RemoteKind) {
|
||||
for (const k of approvedAt.keys()) {
|
||||
if (k.startsWith(`${peerId}::`)) {
|
||||
if (!kind || k.startsWith(`${peerId}::${kind}`)) approvedAt.delete(k)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 同步决策: 自动允许 / 缓存允许 -> 直接 true; 否则 false (UI 需要主动 ask)
|
||||
export function shouldAsk(peerId: string, kind: RemoteKind): boolean {
|
||||
if (isAutoAllowed(peerId, kind)) return false
|
||||
if (isRemembered(peerId, kind)) {
|
||||
markApproved(peerId, kind)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
const ASK_TIMEOUT_MS = 30_000
|
||||
|
||||
// 让 UI 弹一个授权对话框; resolve(true) 同意, resolve(false) 拒绝/超时
|
||||
// 这里用 BrowserWindow 直接构造, 渲染层可以通过 IPC 拿到队列并显示
|
||||
export interface ApprovalRequest {
|
||||
requestId: string
|
||||
peerId: string
|
||||
peerName: string
|
||||
kind: RemoteKind
|
||||
detail?: string
|
||||
ts: number
|
||||
}
|
||||
|
||||
const waitingQueue: ApprovalRequest[] = []
|
||||
const waitersByReq = new Map<string, Pending>()
|
||||
|
||||
export function enqueueApproval(req: ApprovalRequest): Promise<{ ok: boolean; remember?: boolean }> {
|
||||
return new Promise((resolve) => {
|
||||
const timer = setTimeout(() => {
|
||||
pending.delete(req.requestId)
|
||||
waitersByReq.delete(req.requestId)
|
||||
// 从队列移除
|
||||
const i = waitingQueue.findIndex(x => x.requestId === req.requestId)
|
||||
if (i >= 0) waitingQueue.splice(i, 1)
|
||||
resolve({ ok: false })
|
||||
}, ASK_TIMEOUT_MS)
|
||||
pending.set(req.requestId, { resolve: (ok, remember) => resolve({ ok, remember }), timer })
|
||||
waitersByReq.set(req.requestId, pending.get(req.requestId)!)
|
||||
waitingQueue.push(req)
|
||||
})
|
||||
}
|
||||
|
||||
export function listPendingApprovals(): ApprovalRequest[] {
|
||||
return [...waitingQueue]
|
||||
}
|
||||
|
||||
export function replyApproval(requestId: string, ok: boolean, remember?: boolean): boolean {
|
||||
const p = pending.get(requestId)
|
||||
if (!p) return false
|
||||
clearTimeout(p.timer)
|
||||
pending.delete(requestId)
|
||||
waitersByReq.delete(requestId)
|
||||
const i = waitingQueue.findIndex(x => x.requestId === requestId)
|
||||
if (i >= 0) waitingQueue.splice(i, 1)
|
||||
p.resolve(ok, remember)
|
||||
return true
|
||||
}
|
||||
|
||||
export function focusMain() {
|
||||
const w = BrowserWindow.getAllWindows()[0]
|
||||
if (!w) return
|
||||
if (w.isMinimized()) w.restore()
|
||||
w.show()
|
||||
w.focus()
|
||||
}
|
||||
Reference in New Issue
Block a user