|
| 1 | +import fs from 'fs'; |
| 2 | +import path from 'path'; |
| 3 | +import os from 'os'; |
| 4 | +import crypto from 'crypto'; |
| 5 | + |
| 6 | +export function createHttpHandler({ BASE_URL, expressApp, queries, sendJSON, serveFile, staticDir, messageQueues, wss, activeExecutions, getACPStatus, discoveredAgents, PKG_VERSION, RATE_LIMIT_MAX, rateLimitMap, convRoutes, messagesRoutes, sessionsRoutes, scriptsRoutes, runsRoutes, agentRoutes, oauthRoutes, agentActionsRoutes, authConfigRoutes, speechRoutes, utilRoutes, threadRoutes, handleGeminiOAuthCallback, handleCodexOAuthCallback, PORT }) { |
| 7 | + return async function httpHandler(req, res) { |
| 8 | + res.setHeader('Access-Control-Allow-Origin', '*'); |
| 9 | + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS'); |
| 10 | + res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); |
| 11 | + if (req.method === 'OPTIONS') { res.writeHead(200); res.end(); return; } |
| 12 | + if (req.headers.upgrade && req.headers.upgrade.toLowerCase() === 'websocket') return; |
| 13 | + |
| 14 | + const clientIp = req.headers['x-forwarded-for']?.split(',')[0]?.trim() || req.socket.remoteAddress; |
| 15 | + const hits = (rateLimitMap.get(clientIp) || 0) + 1; |
| 16 | + rateLimitMap.set(clientIp, hits); |
| 17 | + res.setHeader('X-RateLimit-Limit', RATE_LIMIT_MAX); |
| 18 | + res.setHeader('X-RateLimit-Remaining', Math.max(0, RATE_LIMIT_MAX - hits)); |
| 19 | + if (hits > RATE_LIMIT_MAX) { res.writeHead(429, { 'Retry-After': '60' }); res.end('Too Many Requests'); return; } |
| 20 | + |
| 21 | + const _pwd = process.env.PASSWORD; |
| 22 | + if (_pwd) { |
| 23 | + const _auth = req.headers['authorization'] || ''; |
| 24 | + let _ok = false; |
| 25 | + if (_auth.startsWith('Basic ')) { |
| 26 | + try { |
| 27 | + const _decoded = Buffer.from(_auth.slice(6), 'base64').toString('utf8'); |
| 28 | + const _ci = _decoded.indexOf(':'); |
| 29 | + if (_ci !== -1) { const _p = _decoded.slice(_ci + 1); try { _ok = _p.length === _pwd.length && crypto.timingSafeEqual(Buffer.from(_p), Buffer.from(_pwd)); } catch { _ok = false; } } |
| 30 | + } catch (_) {} |
| 31 | + } |
| 32 | + if (!_ok) { res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="agentgui"' }); res.end('Unauthorized'); return; } |
| 33 | + } |
| 34 | + |
| 35 | + const pathOnly = req.url.split('?')[0]; |
| 36 | + if (pathOnly.startsWith(BASE_URL + '/api/upload/') || pathOnly.startsWith(BASE_URL + '/files/')) return expressApp(req, res); |
| 37 | + |
| 38 | + if (req.url === '/favicon.ico' || req.url === BASE_URL + '/favicon.ico') { |
| 39 | + const svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><rect width="100" height="100" rx="20" fill="#3b82f6"/><text x="50" y="68" font-size="50" font-family="sans-serif" font-weight="bold" fill="white" text-anchor="middle">G</text></svg>'; |
| 40 | + res.writeHead(200, { 'Content-Type': 'image/svg+xml', 'Cache-Control': 'public, max-age=86400' }); |
| 41 | + res.end(svg); return; |
| 42 | + } |
| 43 | + |
| 44 | + if (req.url === '/') { res.writeHead(302, { Location: BASE_URL + '/' }); res.end(); return; } |
| 45 | + |
| 46 | + let routePath = req.url; |
| 47 | + if (req.url.startsWith(BASE_URL + '/')) { routePath = req.url.slice(BASE_URL.length); } |
| 48 | + else if (req.url === BASE_URL) { routePath = '/'; } |
| 49 | + else if (req.url.startsWith('/api/') || req.url.startsWith('/js/') || req.url.startsWith('/css/') || |
| 50 | + req.url.startsWith('/vendor/') || req.url.startsWith('/sync') || req.url === '/' || |
| 51 | + req.url.startsWith('/conversations/')) { routePath = req.url; } |
| 52 | + else { res.writeHead(404); res.end('Not found'); return; } |
| 53 | + |
| 54 | + routePath = routePath || '/'; |
| 55 | + |
| 56 | + try { |
| 57 | + const pathOnly = routePath.split('?')[0]; |
| 58 | + |
| 59 | + if (pathOnly === '/oauth2callback' && req.method === 'GET') { await handleGeminiOAuthCallback(req, res, PORT); return; } |
| 60 | + if (pathOnly === '/codex-oauth2callback' && req.method === 'GET') { await handleCodexOAuthCallback(req, res, PORT); return; } |
| 61 | + |
| 62 | + if (pathOnly === '/api/health' && req.method === 'GET') { |
| 63 | + let dbStatus = { ok: true }; |
| 64 | + try { queries._db.prepare('SELECT 1').get(); } catch (e) { dbStatus = { ok: false, error: e.message }; } |
| 65 | + const queueSizes = {}; |
| 66 | + for (const [k, v] of messageQueues) queueSizes[k] = v.length; |
| 67 | + sendJSON(req, res, 200, { status: 'ok', version: PKG_VERSION, uptime: process.uptime(), agents: discoveredAgents.length, activeExecutions: activeExecutions.size, wsClients: wss.clients.size, memory: process.memoryUsage(), acp: getACPStatus(), db: dbStatus, queueSizes }); |
| 68 | + return; |
| 69 | + } |
| 70 | + |
| 71 | + const convHandler = convRoutes._match(req.method, pathOnly); |
| 72 | + if (convHandler) { await convHandler(req, res); return; } |
| 73 | + const messagesHandler = messagesRoutes._match(req.method, pathOnly); |
| 74 | + if (messagesHandler) { await messagesHandler(req, res); return; } |
| 75 | + const sessionsHandler = sessionsRoutes._match(req.method, pathOnly); |
| 76 | + if (sessionsHandler) { await sessionsHandler(req, res); return; } |
| 77 | + const scriptsHandler = scriptsRoutes._match(req.method, pathOnly); |
| 78 | + if (scriptsHandler) { await scriptsHandler(req, res); return; } |
| 79 | + const runsHandler = runsRoutes._match(req.method, pathOnly); |
| 80 | + if (runsHandler) { await runsHandler(req, res); return; } |
| 81 | + const agentHandler = agentRoutes._match(req.method, pathOnly); |
| 82 | + if (agentHandler) { await agentHandler(req, res); return; } |
| 83 | + const oauthHandler = oauthRoutes._match(req.method, pathOnly); |
| 84 | + if (oauthHandler) { await oauthHandler(req, res); return; } |
| 85 | + const agentActionsHandler = agentActionsRoutes._match(req.method, pathOnly); |
| 86 | + if (agentActionsHandler) { await agentActionsHandler(req, res); return; } |
| 87 | + const authConfigHandler = authConfigRoutes._match(req.method, pathOnly); |
| 88 | + if (authConfigHandler) { await authConfigHandler(req, res); return; } |
| 89 | + const speechHandler = speechRoutes._match(req.method, pathOnly); |
| 90 | + if (speechHandler) { await speechHandler(req, res, pathOnly); return; } |
| 91 | + const utilHandler = utilRoutes._match(req.method, pathOnly); |
| 92 | + if (utilHandler) { await utilHandler(req, res); return; } |
| 93 | + const threadHandler = threadRoutes._match(req.method, pathOnly); |
| 94 | + if (threadHandler) { await threadHandler(req, res); return; } |
| 95 | + if (routePath.startsWith('/api/image/')) { |
| 96 | + const imagePath = routePath.slice('/api/image/'.length); |
| 97 | + const decodedPath = decodeURIComponent(imagePath); |
| 98 | + const expandedPath = decodedPath.startsWith('~') ? decodedPath.replace('~', os.homedir()) : decodedPath; |
| 99 | + const normalizedPath = path.normalize(expandedPath); |
| 100 | + const isWindows = os.platform() === 'win32'; |
| 101 | + const isAbsolute = isWindows ? /^[A-Za-z]:[\\\/]/.test(normalizedPath) : normalizedPath.startsWith('/'); |
| 102 | + if (!isAbsolute || normalizedPath.includes('..')) { res.writeHead(403); res.end('Forbidden'); return; } |
| 103 | + try { |
| 104 | + if (!fs.existsSync(normalizedPath)) { res.writeHead(404); res.end('Not found'); return; } |
| 105 | + const ext = path.extname(normalizedPath).toLowerCase(); |
| 106 | + const mimeTypes = { '.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml' }; |
| 107 | + const contentType = mimeTypes[ext] || 'application/octet-stream'; |
| 108 | + res.writeHead(200, { 'Content-Type': contentType, 'Cache-Control': 'no-cache' }); |
| 109 | + res.end(fs.readFileSync(normalizedPath)); |
| 110 | + } catch (err) { sendJSON(req, res, 400, { error: err.message }); } |
| 111 | + return; |
| 112 | + } |
| 113 | + |
| 114 | + if (pathOnly.match(/^\/conversations\/[^\/]+$/)) { serveFile(path.join(staticDir, 'index.html'), res, req); return; } |
| 115 | + |
| 116 | + let filePath = routePath === '/' ? '/index.html' : routePath; |
| 117 | + filePath = path.join(staticDir, filePath); |
| 118 | + const normalizedPath = path.normalize(filePath); |
| 119 | + if (!normalizedPath.startsWith(staticDir)) { res.writeHead(403); res.end('Forbidden'); return; } |
| 120 | + |
| 121 | + fs.stat(filePath, (err, stats) => { |
| 122 | + if (err) { res.writeHead(404); res.end('Not found'); return; } |
| 123 | + if (stats.isDirectory()) { |
| 124 | + filePath = path.join(filePath, 'index.html'); |
| 125 | + fs.stat(filePath, (err2) => { if (err2) { res.writeHead(404); res.end('Not found'); return; } serveFile(filePath, res, req); }); |
| 126 | + } else { serveFile(filePath, res, req); } |
| 127 | + }); |
| 128 | + } catch (e) { |
| 129 | + console.error('Server error:', e.message); |
| 130 | + sendJSON(req, res, 500, { error: e.message }); |
| 131 | + } |
| 132 | + }; |
| 133 | +} |
0 commit comments