116 lines
3.6 KiB
JavaScript
116 lines
3.6 KiB
JavaScript
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import http from 'node:http'
|
|
import { pipeline } from 'node:stream/promises'
|
|
import { Readable } from 'node:stream'
|
|
|
|
const port = Math.max(1, Number(process.env.UI_SMOKE_HOST_PORT || 4173))
|
|
const proxyTarget = new URL(
|
|
process.env.FAMILIAROS_UI_PROXY_TARGET
|
|
|| process.env.SCRIPTORIUM_UI_PROXY_TARGET
|
|
|| 'http://127.0.0.1:3014'
|
|
)
|
|
const distDir = path.resolve(process.cwd(), 'dist')
|
|
|
|
const MIME_TYPES = {
|
|
'.css': 'text/css; charset=utf-8',
|
|
'.html': 'text/html; charset=utf-8',
|
|
'.ico': 'image/x-icon',
|
|
'.js': 'text/javascript; charset=utf-8',
|
|
'.json': 'application/json; charset=utf-8',
|
|
'.map': 'application/json; charset=utf-8',
|
|
'.png': 'image/png',
|
|
'.svg': 'image/svg+xml; charset=utf-8',
|
|
'.txt': 'text/plain; charset=utf-8',
|
|
'.woff': 'font/woff',
|
|
'.woff2': 'font/woff2',
|
|
}
|
|
|
|
function isProxyPath(pathname) {
|
|
return pathname === '/api'
|
|
|| pathname.startsWith('/api/')
|
|
|| pathname === '/auth'
|
|
|| pathname.startsWith('/auth/')
|
|
}
|
|
|
|
function resolveStaticPath(pathname) {
|
|
const normalized = pathname === '/' ? '/index.html' : pathname
|
|
const absolute = path.resolve(distDir, `.${normalized}`)
|
|
if (!absolute.startsWith(distDir)) return null
|
|
return absolute
|
|
}
|
|
|
|
async function proxyRequest(req, res, requestUrl) {
|
|
const upstream = new URL(`${requestUrl.pathname}${requestUrl.search}`, proxyTarget)
|
|
const headers = new Headers()
|
|
for (const [key, value] of Object.entries(req.headers)) {
|
|
if (value === undefined) continue
|
|
if (Array.isArray(value)) {
|
|
for (const part of value) headers.append(key, part)
|
|
} else {
|
|
headers.set(key, value)
|
|
}
|
|
}
|
|
headers.set('host', proxyTarget.host)
|
|
|
|
const response = await fetch(upstream, {
|
|
method: req.method || 'GET',
|
|
headers,
|
|
body: req.method === 'GET' || req.method === 'HEAD' ? undefined : req,
|
|
duplex: req.method === 'GET' || req.method === 'HEAD' ? undefined : 'half',
|
|
redirect: 'manual',
|
|
})
|
|
|
|
const responseHeaders = Object.fromEntries(response.headers.entries())
|
|
res.writeHead(response.status, responseHeaders)
|
|
if (!response.body || req.method === 'HEAD') {
|
|
res.end()
|
|
return
|
|
}
|
|
|
|
await pipeline(Readable.fromWeb(response.body), res)
|
|
}
|
|
|
|
async function serveStaticFile(res, filePath) {
|
|
const extension = path.extname(filePath).toLowerCase()
|
|
const contentType = MIME_TYPES[extension] || 'application/octet-stream'
|
|
const stream = fs.createReadStream(filePath)
|
|
res.writeHead(200, {
|
|
'Content-Type': contentType,
|
|
'Cache-Control': extension === '.html' ? 'no-cache' : 'public, max-age=31536000, immutable',
|
|
})
|
|
await pipeline(stream, res)
|
|
}
|
|
|
|
const server = http.createServer(async (req, res) => {
|
|
const requestUrl = new URL(req.url || '/', `http://${req.headers.host || `127.0.0.1:${port}`}`)
|
|
|
|
try {
|
|
if (isProxyPath(requestUrl.pathname)) {
|
|
await proxyRequest(req, res, requestUrl)
|
|
return
|
|
}
|
|
|
|
const candidate = resolveStaticPath(requestUrl.pathname)
|
|
if (candidate && fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
|
|
await serveStaticFile(res, candidate)
|
|
return
|
|
}
|
|
|
|
await serveStaticFile(res, path.join(distDir, 'index.html'))
|
|
} catch (error) {
|
|
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' })
|
|
res.end(error instanceof Error ? error.message : String(error))
|
|
}
|
|
})
|
|
|
|
server.listen(port, '127.0.0.1', () => {
|
|
process.stdout.write(`[live-auth-smoke-host] listening on http://127.0.0.1:${port} -> ${proxyTarget.origin}\n`)
|
|
})
|
|
|
|
function shutdown() {
|
|
server.close(() => process.exit(0))
|
|
}
|
|
|
|
process.on('SIGINT', shutdown)
|
|
process.on('SIGTERM', shutdown)
|