mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-25 12:16:00 +08:00
184 lines
6.0 KiB
TypeScript
184 lines
6.0 KiB
TypeScript
import express from 'express';
|
|
|
|
import { browserUseService } from '@/modules/browser-use/browser-use.service.js';
|
|
import { VIEWER_COOKIE_NAME, VIEWER_TOKEN_TTL_MS } from '@/modules/browser-use/browser-use.viewer.js';
|
|
|
|
const router = express.Router();
|
|
|
|
function readParam(value: string | string[] | undefined): string {
|
|
return Array.isArray(value) ? value[0] || '' : value || '';
|
|
}
|
|
|
|
const SAFE_VIEWER_ROOT_FILES = new Set(['vnc.html', 'favicon.ico', 'manifest.json']);
|
|
const SAFE_VIEWER_ROOT_DIRS = new Set(['app', 'core', 'vendor', 'assets', 'images', 'utils']);
|
|
|
|
function isSafeViewerPath(viewerPath: string): boolean {
|
|
if (!viewerPath || viewerPath.startsWith('/') || viewerPath.includes('..') || viewerPath.includes('\\')) {
|
|
return false;
|
|
}
|
|
|
|
if (!/^[A-Za-z0-9][A-Za-z0-9._~/-]*$/.test(viewerPath)) {
|
|
return false;
|
|
}
|
|
|
|
if (SAFE_VIEWER_ROOT_FILES.has(viewerPath)) {
|
|
return true;
|
|
}
|
|
|
|
const [rootDir] = viewerPath.split('/');
|
|
return Boolean(rootDir && SAFE_VIEWER_ROOT_DIRS.has(rootDir));
|
|
}
|
|
|
|
function isSecureRequest(req: express.Request): boolean {
|
|
const forwardedProto = String(req.headers['x-forwarded-proto'] || '')
|
|
.split(',')[0]
|
|
.trim()
|
|
.toLowerCase();
|
|
return req.secure || forwardedProto === 'https';
|
|
}
|
|
|
|
function readQueryString(originalUrl: string): string {
|
|
const queryIndex = originalUrl.indexOf('?');
|
|
if (queryIndex < 0) {
|
|
return '';
|
|
}
|
|
const params = new URLSearchParams(originalUrl.slice(queryIndex + 1));
|
|
params.delete('viewerToken');
|
|
const nextQuery = params.toString();
|
|
return nextQuery ? `?${nextQuery}` : '';
|
|
}
|
|
|
|
router.get('/status', async (_req, res) => {
|
|
try {
|
|
res.json({ success: true, data: await browserUseService.getStatus() });
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to load Browser status.',
|
|
});
|
|
}
|
|
});
|
|
|
|
router.get('/settings', async (_req, res) => {
|
|
try {
|
|
res.json({ success: true, data: { settings: await browserUseService.getSettings() } });
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to load Browser settings.',
|
|
});
|
|
}
|
|
});
|
|
|
|
router.put('/settings', async (req, res) => {
|
|
try {
|
|
const settings = await browserUseService.updateSettings(req.body || {});
|
|
res.json({ success: true, data: { settings } });
|
|
} catch (error) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to save Browser settings.',
|
|
});
|
|
}
|
|
});
|
|
|
|
router.post('/runtime/install', async (_req, res) => {
|
|
try {
|
|
const result = await browserUseService.installRuntime();
|
|
res.status(result.success ? 200 : 500).json({
|
|
success: result.success,
|
|
data: result,
|
|
error: result.success ? undefined : result.message,
|
|
});
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to install Browser runtime.',
|
|
});
|
|
}
|
|
});
|
|
|
|
router.get('/sessions', async (_req, res) => {
|
|
try {
|
|
res.json({ success: true, data: { sessions: await browserUseService.listSessions() } });
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to list browser sessions.',
|
|
});
|
|
}
|
|
});
|
|
|
|
router.get('/sessions/:sessionId/viewer/*', async (req, res) => {
|
|
try {
|
|
const sessionId = readParam(req.params.sessionId);
|
|
const originalPath = req.originalUrl.split('?')[0] || '';
|
|
const viewerMarker = `/sessions/${sessionId}/viewer/`;
|
|
const markerIndex = originalPath.indexOf(viewerMarker);
|
|
const rawViewerPath = markerIndex >= 0 ? originalPath.slice(markerIndex + viewerMarker.length) : 'vnc.html';
|
|
const viewerPath = decodeURIComponent(rawViewerPath).replace(/^\/+/, '') || 'vnc.html';
|
|
if (!isSafeViewerPath(viewerPath)) {
|
|
res.status(400).json({ success: false, error: 'Invalid Browser viewer path.' });
|
|
return;
|
|
}
|
|
|
|
const viewerToken = readParam(req.query.viewerToken as string | string[] | undefined);
|
|
if (viewerPath === 'vnc.html' && browserUseService.validateViewerToken(sessionId, viewerToken)) {
|
|
res.cookie(VIEWER_COOKIE_NAME, viewerToken, {
|
|
httpOnly: true,
|
|
sameSite: 'lax',
|
|
secure: isSecureRequest(req),
|
|
maxAge: VIEWER_TOKEN_TTL_MS,
|
|
path: '/api/browser-use/sessions/' + encodeURIComponent(sessionId) + '/viewer',
|
|
});
|
|
}
|
|
const target = browserUseService.getViewerProxyTarget(sessionId);
|
|
const query = readQueryString(req.originalUrl);
|
|
const upstream = await fetch(`http://127.0.0.1:${target.websockifyPort}/${viewerPath}${query}`, {
|
|
headers: {
|
|
accept: String(req.headers.accept || '*/*'),
|
|
},
|
|
});
|
|
const contentType = upstream.headers.get('content-type');
|
|
if (contentType) {
|
|
res.setHeader('content-type', contentType);
|
|
}
|
|
const cacheControl = viewerPath === 'vnc.html' ? 'no-store' : 'public, max-age=3600';
|
|
res.setHeader('cache-control', cacheControl);
|
|
res.status(upstream.status);
|
|
const body = Buffer.from(await upstream.arrayBuffer());
|
|
res.send(body);
|
|
} catch (error) {
|
|
res.status(404).json({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Browser viewer is not available.',
|
|
});
|
|
}
|
|
});
|
|
|
|
router.post('/sessions/:sessionId/stop', async (req, res) => {
|
|
try {
|
|
const result = await browserUseService.stopSession(readParam(req.params.sessionId));
|
|
res.json({ success: true, data: result });
|
|
} catch (error) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to stop browser session.',
|
|
});
|
|
}
|
|
});
|
|
|
|
router.delete('/sessions/:sessionId', async (req, res) => {
|
|
try {
|
|
const result = await browserUseService.deleteSession(readParam(req.params.sessionId));
|
|
res.json({ success: true, data: result });
|
|
} catch (error) {
|
|
res.status(400).json({
|
|
success: false,
|
|
error: error instanceof Error ? error.message : 'Failed to delete browser session.',
|
|
});
|
|
}
|
|
});
|
|
|
|
export default router;
|