chore: remote forwarding integration
This commit is contained in:
+236
-2
@@ -10,7 +10,8 @@ import {
|
||||
db, upsertDevice, listDevices, listMessages, insertMessage, getMessage,
|
||||
updateMessageStatus, updateMessageBody, deleteMessage, conversationKey, incrUnread, clearUnread,
|
||||
getUnread, totalUnread, listPendingMessages, listAllPending, listSentFileUpdates,
|
||||
setDeviceIgnored, listIgnoredDevices, getDevice, deleteConversation
|
||||
setDeviceIgnored, listIgnoredDevices, getDevice, deleteConversation,
|
||||
recordAudit, listAudit, pruneAudit
|
||||
} from './db'
|
||||
import { Discovery, listNetInterfaces, type NetInterface } from './discovery'
|
||||
import { ChatServer } from './chat-server'
|
||||
@@ -19,7 +20,11 @@ import { FileServer, uploadFile, uploadBuffer } from './file-server'
|
||||
import { broadcastToRenderer, focusMainWindow } from './window'
|
||||
import { rebuildMenu } from './tray'
|
||||
import { notify, setBadgeCount } from './notify'
|
||||
import type { DeviceInfo, MessageEnvelope, MessageBody, ImageMessage, FileMessage, TextMessage, SystemMessage, ReplyRef } from './protocol'
|
||||
import { TerminalManager } from './remote/terminal'
|
||||
import { ForwardManager } from './remote/forward'
|
||||
import { UsbBridgeManager } from './remote/usb'
|
||||
import { enqueueApproval, listPendingApprovals, replyApproval } from './remote/approval'
|
||||
import type { DeviceInfo, MessageEnvelope, MessageBody, ImageMessage, FileMessage, TextMessage, SystemMessage, ReplyRef, TerminalFrame, ForwardFrame, UsbFrame, UsbDeviceInfo, UsbDirection, UsbAttachConfig } from './protocol'
|
||||
|
||||
interface NetCtx {
|
||||
self: DeviceInfo
|
||||
@@ -27,6 +32,9 @@ interface NetCtx {
|
||||
chatServer: ChatServer
|
||||
chatClient: ChatClient
|
||||
fileServer: FileServer
|
||||
terminal: TerminalManager
|
||||
forward: ForwardManager
|
||||
usb: UsbBridgeManager
|
||||
}
|
||||
|
||||
let ctx: NetCtx | null = null
|
||||
@@ -159,6 +167,40 @@ export function bindNetworkContext(c: NetCtx) {
|
||||
else nextStatus = 'delivered'
|
||||
updateMessageStatus(messageId, nextStatus)
|
||||
broadcastToRenderer('message:statusChanged', { messageId, status: nextStatus })
|
||||
return
|
||||
}
|
||||
if (frame.type === 'terminal') {
|
||||
ctx!.terminal.handleIncoming(peer, frame.payload as TerminalFrame)
|
||||
.catch((e) => console.warn('[terminal] handle error:', e?.message))
|
||||
return
|
||||
}
|
||||
if (frame.type === 'forward') {
|
||||
ctx!.forward.handleIncoming(peer, frame.payload as ForwardFrame)
|
||||
return
|
||||
}
|
||||
if (frame.type === 'usb') {
|
||||
ctx!.usb.handleIncoming(peer, frame.payload as UsbFrame)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
// chat-server 收到的所有 ws 帧 (来自其他设备的入站)
|
||||
// 这里分发 remote 帧给本地 manager (peer -> 我们) — 控制端发的请求落在这里
|
||||
c.chatServer.on('frame', (peer, frame) => {
|
||||
if (frame.type === 'terminal') {
|
||||
console.log(`[ipc] incoming terminal frame from ${peer.name}: ${frame.payload.type}`)
|
||||
ctx!.terminal.handleIncoming(peer, frame.payload as TerminalFrame)
|
||||
.catch((e) => console.warn('[terminal] handle error:', e?.message))
|
||||
return
|
||||
}
|
||||
if (frame.type === 'forward') {
|
||||
console.log(`[ipc] incoming forward frame from ${peer.name}: ${(frame.payload as any).type}`)
|
||||
ctx!.forward.handleIncoming(peer, frame.payload as ForwardFrame)
|
||||
return
|
||||
}
|
||||
if (frame.type === 'usb') {
|
||||
ctx!.usb.handleIncoming(peer, frame.payload as UsbFrame)
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
@@ -234,6 +276,57 @@ export function bindNetworkContext(c: NetCtx) {
|
||||
broadcastToRenderer('message:statusChanged', { messageId: r.message_id, status: 'read' })
|
||||
}
|
||||
})
|
||||
|
||||
// ===== Remote: terminal / forward / usb 事件桥接 =====
|
||||
c.terminal.on('output', (sessionId: string, bytes: Buffer, peerId: string) => {
|
||||
// 控制端从被控端收到 output -> 给 UI
|
||||
broadcastToRenderer('remote:terminal:output', { sessionId, peerId, data: bytes.toString('base64') })
|
||||
})
|
||||
c.terminal.on('sessionOpened', (sessionId: string, peerId: string, info: { shell: string; rows: number; cols: number }) => {
|
||||
broadcastToRenderer('remote:terminal:opened', { sessionId, peerId, ...info })
|
||||
})
|
||||
c.terminal.on('sessionClosed', (sessionId: string, peerId: string, reason?: string) => {
|
||||
broadcastToRenderer('remote:terminal:closed', { sessionId, peerId, reason })
|
||||
})
|
||||
c.terminal.on('approvalRequested', (req) => {
|
||||
broadcastToRenderer('remote:approval:requested', { ...req, kind: 'terminal' })
|
||||
})
|
||||
|
||||
c.forward.on('sessionOpened', (info) => {
|
||||
broadcastToRenderer('remote:forward:opened', info)
|
||||
})
|
||||
c.forward.on('sessionClosed', (info) => {
|
||||
broadcastToRenderer('remote:forward:closed', info)
|
||||
})
|
||||
c.forward.on('sessionError', (info) => {
|
||||
broadcastToRenderer('remote:forward:error', info)
|
||||
})
|
||||
c.forward.on('approvalRequested', (req) => {
|
||||
broadcastToRenderer('remote:approval:requested', { ...req, kind: 'forward' })
|
||||
})
|
||||
|
||||
c.usb.on('output', (sessionId: string, bytes: Buffer, peerId: string) => {
|
||||
broadcastToRenderer('remote:usb:output', { sessionId, peerId, data: bytes.toString('base64') })
|
||||
})
|
||||
c.usb.on('sessionOpened', (info) => {
|
||||
broadcastToRenderer('remote:usb:opened', info)
|
||||
})
|
||||
c.usb.on('sessionClosed', (info) => {
|
||||
broadcastToRenderer('remote:usb:closed', info)
|
||||
})
|
||||
c.usb.on('sessionError', (info) => {
|
||||
broadcastToRenderer('remote:usb:error', info)
|
||||
})
|
||||
c.usb.on('approvalRequested', (req) => {
|
||||
broadcastToRenderer('remote:approval:requested', { ...req, kind: 'usb' })
|
||||
})
|
||||
|
||||
// 对端 ws 断开 -> 清理该对端的所有 session
|
||||
c.chatClient.on('close', (peer) => {
|
||||
c.terminal.onPeerDisconnected(peer.deviceId)
|
||||
c.forward.onPeerDisconnected(peer.deviceId)
|
||||
c.usb.onPeerDisconnected(peer.deviceId)
|
||||
})
|
||||
}
|
||||
|
||||
function notifyIfNeeded(from: DeviceInfo, env: MessageEnvelope) {
|
||||
@@ -911,6 +1004,147 @@ export function registerIpc() {
|
||||
return false
|
||||
}
|
||||
})
|
||||
|
||||
// ===== 远程: terminal =====
|
||||
|
||||
ipcMain.handle('terminal:open', async (_e, args: { peerId: string; rows?: number; cols?: number; readOnly?: boolean }) => {
|
||||
if (!ctx) return { ok: false, reason: 'no ctx' }
|
||||
return ctx.terminal.openOnPeer(args.peerId, {
|
||||
rows: args.rows ?? 24,
|
||||
cols: args.cols ?? 80,
|
||||
readOnly: args.readOnly,
|
||||
})
|
||||
})
|
||||
|
||||
ipcMain.handle('terminal:input', (_e, args: { sessionId: string; dataBase64: string }) => {
|
||||
if (!ctx) return false
|
||||
if (!args?.sessionId || !args?.dataBase64) return false
|
||||
const buf = Buffer.from(args.dataBase64, 'base64')
|
||||
return ctx.terminal.sendInput(args.sessionId, buf)
|
||||
})
|
||||
|
||||
ipcMain.handle('terminal:resize', (_e, args: { sessionId: string; rows: number; cols: number }) => {
|
||||
if (!ctx) return false
|
||||
return ctx.terminal.resize(args.sessionId, args.rows, args.cols)
|
||||
})
|
||||
|
||||
ipcMain.handle('terminal:close', (_e, args: { sessionId: string; reason?: string }) => {
|
||||
if (!ctx) return false
|
||||
ctx.terminal.close(args.sessionId, args.reason)
|
||||
return true
|
||||
})
|
||||
|
||||
ipcMain.handle('terminal:listSessions', () => {
|
||||
if (!ctx) return { client: [], server: [] }
|
||||
return {
|
||||
client: ctx.terminal.listClientSessions(),
|
||||
server: ctx.terminal.listServerSessions(),
|
||||
}
|
||||
})
|
||||
|
||||
// ===== 远程: forward =====
|
||||
|
||||
ipcMain.handle('forward:open', async (_e, args: { peerId: string; direction?: 'self-out' | 'self-in'; listenPort: number; targetHost: string; targetPort: number; ttlSec?: number }) => {
|
||||
if (!ctx) return { ok: false, reason: 'no ctx' }
|
||||
return ctx.forward.openOnPeer(args.peerId, args)
|
||||
})
|
||||
|
||||
ipcMain.handle('forward:close', (_e, args: { sessionId: string }) => {
|
||||
if (!ctx) return false
|
||||
// 控制端: 关 clientSession
|
||||
ctx.forward.cleanupClientSession(args.sessionId, 'user')
|
||||
return true
|
||||
})
|
||||
|
||||
ipcMain.handle('forward:listSessions', () => {
|
||||
if (!ctx) return { client: [], server: [] }
|
||||
return {
|
||||
client: ctx.forward.listClientSessions(),
|
||||
server: ctx.forward.listServerSessions(),
|
||||
}
|
||||
})
|
||||
|
||||
// ===== 远程: usb (3 种模式: serial 字节流 / usb libusb 字节桥 / usbip 真透传) =====
|
||||
|
||||
ipcMain.handle('usb:list', async (_e, args: { peerId: string }) => {
|
||||
if (!ctx) return { ok: false, reason: 'no ctx' }
|
||||
return ctx.usb.listOnPeer(args.peerId)
|
||||
})
|
||||
|
||||
// 列本机 (串口 + libusb + linux usbip) — self-in 模式用
|
||||
ipcMain.handle('usb:listLocal', async () => {
|
||||
if (!ctx) return { ok: false, reason: 'no ctx' }
|
||||
try {
|
||||
const devices = await ctx.usb.listLocal()
|
||||
return { ok: true, devices }
|
||||
} catch (e: any) {
|
||||
return { ok: false, reason: String(e?.message || e), devices: [] }
|
||||
}
|
||||
})
|
||||
|
||||
// attach: 由 renderer 把 UsbAttachConfig 传过来
|
||||
ipcMain.handle('usb:attach', async (_e, args: { peerId: string; direction?: UsbDirection; busId: string; config: UsbAttachConfig }) => {
|
||||
if (!ctx) return { ok: false, reason: 'no ctx' }
|
||||
return ctx.usb.attach(args.peerId, { direction: args.direction, busId: args.busId, config: args.config })
|
||||
})
|
||||
|
||||
// 串口: 发送字节 (client 端)
|
||||
ipcMain.handle('usb:serialSend', (_e, args: { sessionId: string; dataBase64: string }) => {
|
||||
if (!ctx) return false
|
||||
const buf = Buffer.from(args.dataBase64, 'base64')
|
||||
return ctx.usb.sendSerialBytes(args.sessionId, buf)
|
||||
})
|
||||
|
||||
// USB 控制传输 (control OUT / control IN)
|
||||
ipcMain.handle('usb:ctrlOut', async (_e, args: { sessionId: string; setup: { requestType: number; request: number; value: number; index: number }; dataBase64?: string }) => {
|
||||
if (!ctx) return { ok: false, status: -1 }
|
||||
return ctx.usb.usbCtrlTransfer(args.sessionId, args.setup, args.dataBase64)
|
||||
})
|
||||
ipcMain.handle('usb:ctrlIn', async (_e, args: { sessionId: string; setup: { requestType: number; request: number; value: number; index: number }; length: number }) => {
|
||||
if (!ctx) return { ok: false, status: -1 }
|
||||
return ctx.usb.usbCtrlIn(args.sessionId, args.setup, args.length)
|
||||
})
|
||||
|
||||
// USB 批量传输 (bulk OUT / bulk IN)
|
||||
ipcMain.handle('usb:bulkOut', async (_e, args: { sessionId: string; endpoint: number; dataBase64: string }) => {
|
||||
if (!ctx) return { ok: false, status: -1 }
|
||||
return ctx.usb.usbBulkOut(args.sessionId, args.endpoint, args.dataBase64)
|
||||
})
|
||||
ipcMain.handle('usb:bulkIn', async (_e, args: { sessionId: string; endpoint: number; length: number; timeoutMs?: number }) => {
|
||||
if (!ctx) return { ok: false, status: -1 }
|
||||
return ctx.usb.usbBulkIn(args.sessionId, args.endpoint, args.length, args.timeoutMs)
|
||||
})
|
||||
|
||||
ipcMain.handle('usb:detach', async (_e, args: { sessionId: string }) => {
|
||||
if (!ctx) return { ok: false, reason: 'no ctx' }
|
||||
return ctx.usb.detach(args.sessionId, 'user')
|
||||
})
|
||||
|
||||
ipcMain.handle('usb:listSessions', () => {
|
||||
if (!ctx) return { client: [], server: [] }
|
||||
return {
|
||||
client: ctx.usb.listClientSessions(),
|
||||
server: ctx.usb.listServerSessions(),
|
||||
}
|
||||
})
|
||||
|
||||
// ===== 授权 / 审计 =====
|
||||
|
||||
ipcMain.handle('remote:approval:list', () => listPendingApprovals())
|
||||
|
||||
ipcMain.handle('remote:approval:reply', (_e, args: { requestId: string; ok: boolean; remember?: boolean }) => {
|
||||
return replyApproval(args.requestId, args.ok, args.remember)
|
||||
})
|
||||
|
||||
ipcMain.handle('audit:list', (_e, args: { limit?: number; since?: number; sessionId?: string } = {}) => {
|
||||
return listAudit(args || {})
|
||||
})
|
||||
|
||||
ipcMain.handle('audit:prune', (_e, olderThanMs?: number) => {
|
||||
const days = getSettings().auditRetentionDays
|
||||
const ms = olderThanMs ?? Math.max(1, days) * 24 * 3600 * 1000
|
||||
return pruneAudit(ms)
|
||||
})
|
||||
}
|
||||
|
||||
function rowToEnvelope(row: any): MessageEnvelope {
|
||||
|
||||
Reference in New Issue
Block a user