| 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)) |
| } |
|
|