Spaces:
Running
Running
| import fs from 'node:fs/promises'; | |
| import path from 'node:path'; | |
| import { gzipSync } from 'node:zlib'; | |
| import { GENERATED_ROOT } from './image-generator.js'; | |
| import { CLIENT_DIST, HTTPS_ROOT_CA_PATH, compressibleExtensions, mimeTypes } from './app-config.js'; | |
| export function acceptsGzip(req) { | |
| return String(req.headers['accept-encoding'] || '') | |
| .split(',') | |
| .some((value) => value.trim().toLowerCase().startsWith('gzip')); | |
| } | |
| export function staticCacheControl(ext, filePath = '') { | |
| if (ext === '.html') { | |
| return 'no-store'; | |
| } | |
| const normalized = filePath.split(path.sep).join('/'); | |
| if (normalized.includes('/assets/')) { | |
| return 'public, max-age=31536000, immutable'; | |
| } | |
| return 'public, max-age=3600'; | |
| } | |
| export function sendStaticContent(req, res, status, content, headers, ext) { | |
| let body = content; | |
| const nextHeaders = { ...headers }; | |
| if (content.length >= 1024 && compressibleExtensions.has(ext) && acceptsGzip(req)) { | |
| body = gzipSync(content); | |
| nextHeaders['content-encoding'] = 'gzip'; | |
| nextHeaders.vary = nextHeaders.vary ? `${nextHeaders.vary}, Accept-Encoding` : 'Accept-Encoding'; | |
| } | |
| nextHeaders['content-length'] = body.length; | |
| res.writeHead(status, nextHeaders); | |
| res.end(body); | |
| } | |
| export async function serveFileFromRoot(req, res, rootDir, requestedPath, cacheControl) { | |
| const relativePath = requestedPath.replace(/^\/+/, ''); | |
| const resolvedRoot = path.resolve(rootDir); | |
| const candidate = path.normalize(path.join(resolvedRoot, relativePath)); | |
| const rootWithSep = resolvedRoot.endsWith(path.sep) ? resolvedRoot : `${resolvedRoot}${path.sep}`; | |
| if (candidate !== resolvedRoot && !candidate.startsWith(rootWithSep)) { | |
| res.writeHead(403); | |
| res.end('Forbidden'); | |
| return true; | |
| } | |
| try { | |
| const [realRoot, realCandidate] = await Promise.all([ | |
| fs.realpath(resolvedRoot), | |
| fs.realpath(candidate) | |
| ]); | |
| const realRootWithSep = realRoot.endsWith(path.sep) ? realRoot : `${realRoot}${path.sep}`; | |
| if (realCandidate !== realRoot && !realCandidate.startsWith(realRootWithSep)) { | |
| res.writeHead(403); | |
| res.end('Forbidden'); | |
| return true; | |
| } | |
| const stat = await fs.stat(realCandidate); | |
| if (!stat.isFile()) { | |
| res.writeHead(404); | |
| res.end('Not found'); | |
| return true; | |
| } | |
| const ext = path.extname(realCandidate); | |
| const content = await fs.readFile(realCandidate); | |
| sendStaticContent(req, res, 200, content, { | |
| 'content-type': mimeTypes.get(ext) || 'application/octet-stream', | |
| 'cache-control': cacheControl, | |
| 'x-content-type-options': 'nosniff' | |
| }, ext); | |
| return true; | |
| } catch { | |
| res.writeHead(404); | |
| res.end('Not found'); | |
| return true; | |
| } | |
| } | |
| export async function serveStatic(req, res, url) { | |
| let requestedPath = ''; | |
| try { | |
| requestedPath = decodeURIComponent(url.pathname); | |
| } catch { | |
| res.writeHead(400); | |
| res.end('Bad request'); | |
| return true; | |
| } | |
| if (requestedPath === '/codexmobile-root-ca.cer') { | |
| try { | |
| const stat = await fs.stat(HTTPS_ROOT_CA_PATH); | |
| const content = await fs.readFile(HTTPS_ROOT_CA_PATH); | |
| res.writeHead(200, { | |
| 'content-type': 'application/x-x509-ca-cert', | |
| 'content-length': stat.size, | |
| 'cache-control': 'no-store', | |
| 'content-disposition': 'attachment; filename="codexmobile-root-ca.cer"', | |
| 'x-content-type-options': 'nosniff' | |
| }); | |
| res.end(content); | |
| } catch { | |
| res.writeHead(404); | |
| res.end('Certificate not found'); | |
| } | |
| return; | |
| } | |
| if (requestedPath.startsWith('/generated/')) { | |
| await serveFileFromRoot( | |
| req, | |
| res, | |
| GENERATED_ROOT, | |
| requestedPath.slice('/generated/'.length), | |
| 'private, max-age=86400' | |
| ); | |
| return; | |
| } | |
| if (requestedPath === '/') { | |
| requestedPath = '/index.html'; | |
| } | |
| const candidate = path.normalize(path.join(CLIENT_DIST, requestedPath)); | |
| if (!candidate.startsWith(CLIENT_DIST)) { | |
| res.writeHead(403); | |
| res.end('Forbidden'); | |
| return; | |
| } | |
| try { | |
| const stat = await fs.stat(candidate); | |
| const filePath = stat.isDirectory() ? path.join(candidate, 'index.html') : candidate; | |
| const ext = path.extname(filePath); | |
| const content = await fs.readFile(filePath); | |
| sendStaticContent(req, res, 200, content, { | |
| 'content-type': mimeTypes.get(ext) || 'application/octet-stream', | |
| 'cache-control': staticCacheControl(ext, filePath), | |
| 'x-content-type-options': 'nosniff' | |
| }, ext); | |
| } catch { | |
| const indexPath = path.join(CLIENT_DIST, 'index.html'); | |
| try { | |
| const content = await fs.readFile(indexPath); | |
| sendStaticContent(req, res, 200, content, { | |
| 'content-type': 'text/html; charset=utf-8', | |
| 'cache-control': 'no-store', | |
| 'x-content-type-options': 'nosniff' | |
| }, '.html'); | |
| } catch { | |
| res.writeHead(200, { 'content-type': 'text/plain; charset=utf-8' }); | |
| res.end('CodexMobile server is running. Build the PWA with: npm run build'); | |
| } | |
| } | |
| } | |