fix: address multiple code audit findings
- CORS: replace wildcard with explicit origin list from CORS_ORIGINS env - Auth: enforce strong defaults, JWT blacklist (RevokedToken model), login rate limiting - Auth: validate password length before bcrypt (72-byte limit) - Scheduler: single-threaded worker to mitigate SQLite write contention - Scheduler: graceful shutdown (wait=True) - Snapshots: add prune_snapshots() with configurable retention count - Storage: isolate localStorage keys via VITE_APP_KEY prefix - Config: add cors_origins, login_rate_limit, snapshot_retention_count settings
This commit is contained in:
@@ -18,6 +18,11 @@
|
||||
<el-icon><Back /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="isRemoteBrowser" content="复制远程选中文本">
|
||||
<el-button size="small" text @click="copyRemoteSelection">
|
||||
<el-icon><DocumentCopy /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip v-if="isRemoteBrowser" content="前进">
|
||||
<el-button size="small" text @click="sendRemoteCommand('forward')">
|
||||
<el-icon><Right /></el-icon>
|
||||
@@ -67,7 +72,7 @@
|
||||
class="remote-screen"
|
||||
alt=""
|
||||
draggable="false"
|
||||
@load="() => { iframeLoading = false }"
|
||||
@load="onRemoteImageLoad"
|
||||
@error="() => handleRemoteSessionFailure(undefined, '远程浏览器截图加载失败')"
|
||||
@pointerdown.stop.prevent="onRemotePointerDown"
|
||||
@pointermove.stop.prevent="onRemotePointerMove"
|
||||
@@ -126,7 +131,7 @@ import { ref, computed, onMounted, onBeforeUnmount, nextTick, watch, markRaw } f
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import {
|
||||
ArrowLeft, TopRight, Refresh, Loading, Back, Right, Warning,
|
||||
ArrowLeft, TopRight, Refresh, Loading, Back, Right, Warning, DocumentCopy,
|
||||
Link as LinkIcon, Monitor, SetUp, Reading, Cpu, DataLine,
|
||||
Grid, Connection, Ticket, Wallet, Key, Tools, Star, House,
|
||||
} from '@element-plus/icons-vue'
|
||||
@@ -164,7 +169,12 @@ const isReconnectingRemoteBrowser = ref(false)
|
||||
const remoteErrorState = ref<RemoteBrowserErrorState | null>(null)
|
||||
let startRemoteBrowserPromise: Promise<void> | null = null
|
||||
let screenshotObjectUrl = ''
|
||||
let previousScreenshotObjectUrl = ''
|
||||
let pendingScreenshotBlob: Blob | null = null
|
||||
let screenshotFrameRequest: number | undefined
|
||||
let mouseMoveTimer: number | undefined
|
||||
let wheelTimer: number | undefined
|
||||
let pendingWheel: { deltaX: number; deltaY: number; x: number; y: number } | null = null
|
||||
let caretHideTimer: number | undefined
|
||||
|
||||
// Caret / cursor overlay
|
||||
@@ -183,8 +193,11 @@ let wsReconnectTimer: number | undefined
|
||||
let wsReconnectAttempts = 0
|
||||
const WS_MAX_RECONNECT = 5
|
||||
const WS_RECONNECT_BASE_MS = 800
|
||||
const REMOTE_DRAG_MOVE_INTERVAL_MS = 16
|
||||
const REMOTE_HOVER_MOVE_INTERVAL_MS = 80
|
||||
const WS_BACKPRESSURE_BYTES = 256 * 1024
|
||||
const REMOTE_DRAG_MOVE_INTERVAL_MS = 32
|
||||
const REMOTE_HOVER_MOVE_INTERVAL_MS = 120
|
||||
const REMOTE_WHEEL_INTERVAL_MS = 45
|
||||
const HIGH_FREQUENCY_EVENTS = new Set(['mousemove', 'scroll'])
|
||||
|
||||
type RemoteBrowserErrorState = {
|
||||
title: string
|
||||
@@ -318,10 +331,7 @@ function connectRemoteWs() {
|
||||
|
||||
socket.onmessage = (evt) => {
|
||||
if (evt.data instanceof Blob) {
|
||||
// Binary frame = JPEG screenshot
|
||||
const newUrl = URL.createObjectURL(evt.data)
|
||||
setRemoteScreenshotUrl(newUrl)
|
||||
iframeLoading.value = false
|
||||
queueRemoteScreenshot(evt.data)
|
||||
return
|
||||
}
|
||||
// Text frame = JSON control message
|
||||
@@ -368,6 +378,11 @@ function stopRemoteWs() {
|
||||
window.clearTimeout(wsReconnectTimer)
|
||||
wsReconnectTimer = undefined
|
||||
}
|
||||
if (wheelTimer !== undefined) {
|
||||
window.clearTimeout(wheelTimer)
|
||||
wheelTimer = undefined
|
||||
}
|
||||
pendingWheel = null
|
||||
if (ws) {
|
||||
// Prevent onclose from triggering reconnect
|
||||
const old = ws
|
||||
@@ -380,11 +395,13 @@ function stopRemoteWs() {
|
||||
async function sendRemoteEvent(payload: BrowserEventPayload) {
|
||||
if (!props.active || isStartingRemoteBrowser.value) return
|
||||
if (!remoteSession.value) return
|
||||
const highFrequency = HIGH_FREQUENCY_EVENTS.has(payload.type)
|
||||
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||
if (highFrequency && ws.bufferedAmount > WS_BACKPRESSURE_BYTES) return
|
||||
ws.send(JSON.stringify(payload))
|
||||
return
|
||||
}
|
||||
// Fallback: HTTP POST (e.g. during reconnect window)
|
||||
if (highFrequency) return
|
||||
try {
|
||||
const res = await browserSessionsApi.event(remoteSession.value.id, payload)
|
||||
remoteSession.value = res.data
|
||||
@@ -397,6 +414,22 @@ function sendRemoteCommand(type: 'reload' | 'back' | 'forward') {
|
||||
sendRemoteEvent({ type })
|
||||
}
|
||||
|
||||
async function copyRemoteSelection() {
|
||||
if (!remoteSession.value) return
|
||||
try {
|
||||
const res = await browserSessionsApi.selection(remoteSession.value.id)
|
||||
const text = res.data.text.trim()
|
||||
if (!text) {
|
||||
ElMessage.warning('远程页面没有选中文本')
|
||||
return
|
||||
}
|
||||
await navigator.clipboard.writeText(text)
|
||||
ElMessage.success('已复制远程选中文本')
|
||||
} catch (e: any) {
|
||||
ElMessage.error(e.response?.data?.detail || '复制失败')
|
||||
}
|
||||
}
|
||||
|
||||
function remoteViewport() {
|
||||
const rect = remoteFrameRef.value?.getBoundingClientRect()
|
||||
return {
|
||||
@@ -513,7 +546,23 @@ function onRemotePointerCancel(event: PointerEvent) {
|
||||
function onRemoteWheel(event: WheelEvent) {
|
||||
const point = eventPoint(event)
|
||||
if (!point) return
|
||||
sendRemoteEvent({ type: 'scroll', delta_x: event.deltaX, delta_y: event.deltaY, ...point })
|
||||
if (!pendingWheel) {
|
||||
pendingWheel = { deltaX: 0, deltaY: 0, ...point }
|
||||
}
|
||||
pendingWheel.deltaX += event.deltaX
|
||||
pendingWheel.deltaY += event.deltaY
|
||||
pendingWheel.x = point.x
|
||||
pendingWheel.y = point.y
|
||||
if (wheelTimer !== undefined) return
|
||||
wheelTimer = window.setTimeout(flushRemoteWheel, REMOTE_WHEEL_INTERVAL_MS)
|
||||
}
|
||||
|
||||
function flushRemoteWheel() {
|
||||
wheelTimer = undefined
|
||||
const wheel = pendingWheel
|
||||
pendingWheel = null
|
||||
if (!wheel) return
|
||||
sendRemoteEvent({ type: 'scroll', delta_x: wheel.deltaX, delta_y: wheel.deltaY, x: wheel.x, y: wheel.y })
|
||||
}
|
||||
|
||||
function onRemoteKeydown(event: KeyboardEvent) {
|
||||
@@ -521,6 +570,12 @@ function onRemoteKeydown(event: KeyboardEvent) {
|
||||
// which we handle in onRemotePaste with the actual clipboard text.
|
||||
const isPaste = (event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'v'
|
||||
if (isPaste) return
|
||||
const isCopy = (event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 'c'
|
||||
if (isCopy) {
|
||||
event.preventDefault()
|
||||
copyRemoteSelection()
|
||||
return
|
||||
}
|
||||
|
||||
// Prevent browser from handling other keys (scrolling, shortcuts, etc.)
|
||||
event.preventDefault()
|
||||
@@ -568,7 +623,45 @@ async function closeRemoteSession() {
|
||||
await browserSessionsApi.close(id).catch(() => undefined)
|
||||
}
|
||||
|
||||
function queueRemoteScreenshot(blob: Blob) {
|
||||
pendingScreenshotBlob = blob
|
||||
if (screenshotFrameRequest !== undefined) return
|
||||
screenshotFrameRequest = window.requestAnimationFrame(renderPendingScreenshot)
|
||||
}
|
||||
|
||||
function renderPendingScreenshot() {
|
||||
screenshotFrameRequest = undefined
|
||||
const blob = pendingScreenshotBlob
|
||||
pendingScreenshotBlob = null
|
||||
if (!blob) return
|
||||
const nextUrl = URL.createObjectURL(blob)
|
||||
if (previousScreenshotObjectUrl) URL.revokeObjectURL(previousScreenshotObjectUrl)
|
||||
previousScreenshotObjectUrl = screenshotObjectUrl
|
||||
screenshotObjectUrl = nextUrl
|
||||
remoteScreenshotUrl.value = nextUrl
|
||||
}
|
||||
|
||||
function onRemoteImageLoad() {
|
||||
iframeLoading.value = false
|
||||
if (previousScreenshotObjectUrl) {
|
||||
URL.revokeObjectURL(previousScreenshotObjectUrl)
|
||||
previousScreenshotObjectUrl = ''
|
||||
}
|
||||
if (pendingScreenshotBlob && screenshotFrameRequest === undefined) {
|
||||
screenshotFrameRequest = window.requestAnimationFrame(renderPendingScreenshot)
|
||||
}
|
||||
}
|
||||
|
||||
function setRemoteScreenshotUrl(url: string) {
|
||||
if (screenshotFrameRequest !== undefined) {
|
||||
window.cancelAnimationFrame(screenshotFrameRequest)
|
||||
screenshotFrameRequest = undefined
|
||||
}
|
||||
pendingScreenshotBlob = null
|
||||
if (previousScreenshotObjectUrl) {
|
||||
URL.revokeObjectURL(previousScreenshotObjectUrl)
|
||||
previousScreenshotObjectUrl = ''
|
||||
}
|
||||
if (screenshotObjectUrl) {
|
||||
URL.revokeObjectURL(screenshotObjectUrl)
|
||||
screenshotObjectUrl = ''
|
||||
|
||||
Reference in New Issue
Block a user