Add Beryl Agent and harness source
This commit is contained in:
@@ -0,0 +1,53 @@
|
||||
import http from 'node:http';
|
||||
import { randomBytes, timingSafeEqual } from 'node:crypto';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
|
||||
// Expose only built assets and the application API, with a temporary password.
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../apps/web/dist');
|
||||
const password = process.env.PREVIEW_PASSWORD || randomBytes(12).toString('base64url');
|
||||
const credential = Buffer.from('Basic ' + Buffer.from(`beryl:${password}`).toString('base64'));
|
||||
const types = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript', '.css': 'text/css', '.svg': 'image/svg+xml', '.png': 'image/png', '.woff2': 'font/woff2' };
|
||||
http.createServer(async (req, res) => {
|
||||
const supplied = Buffer.from(req.headers.authorization || '');
|
||||
if (supplied.length !== credential.length || !timingSafeEqual(supplied, credential)) {
|
||||
res.writeHead(401, { 'WWW-Authenticate': 'Basic realm="Beryl preview"', 'Cache-Control': 'no-store' });
|
||||
return res.end('Authentication required');
|
||||
}
|
||||
if (req.headers.origin && req.headers.origin !== `https://${req.headers.host}` && req.headers.origin !== `http://${req.headers.host}`) {
|
||||
res.writeHead(403); return res.end('Origin denied');
|
||||
}
|
||||
let pathname;
|
||||
try { pathname = decodeURIComponent(new URL(req.url, 'http://localhost').pathname); }
|
||||
catch { res.writeHead(400); return res.end(); }
|
||||
if (pathname.startsWith('/api/') || pathname === '/health') {
|
||||
const headers = { ...req.headers, host: 'localhost:3001' };
|
||||
delete headers.authorization;
|
||||
const upstream = http.request({ hostname: '127.0.0.1', port: 3001, path: req.url, method: req.method, headers }, response => {
|
||||
const safeHeaders = { ...response.headers, 'cache-control': 'no-store' };
|
||||
delete safeHeaders['access-control-allow-origin'];
|
||||
res.writeHead(response.statusCode, safeHeaders);
|
||||
response.pipe(res);
|
||||
});
|
||||
upstream.on('error', () => { if (!res.headersSent) res.writeHead(502); res.end('Backend unavailable'); });
|
||||
res.on('close', () => upstream.destroy());
|
||||
req.pipe(upstream);
|
||||
return;
|
||||
}
|
||||
if (!['GET', 'HEAD'].includes(req.method)) { res.writeHead(405); return res.end(); }
|
||||
const relative = pathname === '/' ? 'index.html' : pathname.slice(1);
|
||||
const file = path.resolve(root, relative);
|
||||
if (!file.startsWith(root + path.sep) || relative.split('/').some(p => p.startsWith('.'))) {
|
||||
res.writeHead(404); return res.end();
|
||||
}
|
||||
try {
|
||||
const data = await readFile(file);
|
||||
res.writeHead(200, { 'Content-Type': types[path.extname(file)] || 'application/octet-stream', 'X-Content-Type-Options': 'nosniff', 'Cache-Control': 'no-store' });
|
||||
res.end(req.method === 'HEAD' ? undefined : data);
|
||||
} catch { res.writeHead(404); res.end('Not found'); }
|
||||
}).listen(4173, '127.0.0.1', () => {
|
||||
console.log('Preview listening on 127.0.0.1:4173');
|
||||
console.log('Username: beryl');
|
||||
console.log('Password: ' + password);
|
||||
});
|
||||
Reference in New Issue
Block a user