import { createReadStream } from 'node:fs' import { stat } from 'node:fs/promises' import { createServer } from 'node:http' import { extname, join, normalize, resolve } from 'node:path' import { fileURLToPath } from 'node:url' const __dirname = fileURLToPath(new URL('.', import.meta.url)) const distDir = resolve(__dirname, 'dist') const port = Number(process.env.PORT || 7860) const host = process.env.HOST || '0.0.0.0' const mimeTypes = new Map([ ['.html', 'text/html; charset=utf-8'], ['.js', 'text/javascript; charset=utf-8'], ['.css', 'text/css; charset=utf-8'], ['.json', 'application/json; charset=utf-8'], ['.png', 'image/png'], ['.jpg', 'image/jpeg'], ['.jpeg', 'image/jpeg'], ['.ico', 'image/x-icon'], ['.svg', 'image/svg+xml'], ['.webp', 'image/webp'], ['.woff', 'font/woff'], ['.woff2', 'font/woff2'] ]) const hopByHopHeaders = new Set([ 'accept-encoding', 'connection', 'content-length', 'content-encoding', 'content-md5', 'host', 'keep-alive', 'proxy-authenticate', 'proxy-authorization', 'te', 'trailer', 'transfer-encoding', 'upgrade' ]) const server = createServer(async (request, response) => { try { const requestUrl = new URL(request.url || '/', `http://${request.headers.host || 'localhost'}`) if (requestUrl.pathname === '/api/proxy') { await proxyApiRequest(request, response, requestUrl) return } await serveStatic(requestUrl.pathname, response) } catch (error) { console.error(error) sendJson(response, 500, { error: 'Internal server error.' }) } }) server.listen(port, host, () => { console.log(`PixAI web server listening on http://${host}:${port}`) }) async function proxyApiRequest(request, response, requestUrl) { const targetValue = requestUrl.searchParams.get('target') if (!targetValue) { sendJson(response, 400, { error: 'Missing target URL.' }) return } let targetUrl try { targetUrl = new URL(targetValue) } catch { sendJson(response, 400, { error: 'Invalid target URL.' }) return } if (!['https:', 'http:'].includes(targetUrl.protocol)) { sendJson(response, 400, { error: 'Unsupported target protocol.' }) return } const headers = new Headers() for (const [key, value] of Object.entries(request.headers)) { if (hopByHopHeaders.has(key.toLowerCase())) continue if (Array.isArray(value)) headers.set(key, value.join(', ')) else if (value != null) headers.set(key, value) } headers.set('accept-encoding', 'identity') const upstream = await fetch(targetUrl, { method: request.method, headers, body: request.method === 'GET' || request.method === 'HEAD' ? undefined : request, duplex: 'half' }) response.statusCode = upstream.status for (const [key, value] of upstream.headers) { if (hopByHopHeaders.has(key.toLowerCase())) continue response.setHeader(key, value) } if (!upstream.body) { response.end() return } await upstream.body.pipeTo(new WritableStream({ write(chunk) { response.write(Buffer.from(chunk)) }, close() { response.end() }, abort(reason) { response.destroy(reason) } })) } async function serveStatic(pathname, response) { const decodedPath = decodeURIComponent(pathname) const safePath = normalize(decodedPath).replace(/^(\.\.[/\\])+/, '') const requestedPath = resolve(join(distDir, safePath)) const requestedStat = requestedPath.startsWith(distDir) ? await stat(requestedPath).catch(() => null) : null const filePath = requestedStat?.isFile() ? requestedPath : join(distDir, 'index.html') const fileStat = await stat(filePath).catch(() => null) if (!fileStat?.isFile()) { sendJson(response, 404, { error: 'Build output not found. Run pnpm build first.' }) return } response.statusCode = 200 response.setHeader('Content-Type', mimeTypes.get(extname(filePath)) || 'application/octet-stream') response.setHeader('Cache-Control', filePath.endsWith('index.html') ? 'no-cache' : 'public, max-age=31536000, immutable') createReadStream(filePath).pipe(response) } function sendJson(response, status, payload) { response.statusCode = status response.setHeader('Content-Type', 'application/json; charset=utf-8') response.end(JSON.stringify(payload)) }