Initial
This commit is contained in:
98
server.mjs
Normal file
98
server.mjs
Normal file
@@ -0,0 +1,98 @@
|
||||
import http from 'http';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import url from 'url';
|
||||
|
||||
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
|
||||
const ROOT = process.env.WEB_ROOT || path.resolve(__dirname, 'dist');
|
||||
const PORT = Number(process.env.PORT || 3000);
|
||||
|
||||
const MIME = {
|
||||
'.html': 'text/html; charset=utf-8',
|
||||
'.css': 'text/css; charset=utf-8',
|
||||
'.js': 'application/javascript; charset=utf-8',
|
||||
'.mjs': 'application/javascript; charset=utf-8',
|
||||
'.json': 'application/json; charset=utf-8',
|
||||
'.svg': 'image/svg+xml',
|
||||
'.png': 'image/png',
|
||||
'.jpg': 'image/jpeg',
|
||||
'.jpeg': 'image/jpeg',
|
||||
'.webp': 'image/webp',
|
||||
'.gif': 'image/gif',
|
||||
'.ico': 'image/x-icon',
|
||||
'.txt': 'text/plain; charset=utf-8',
|
||||
'.map': 'application/json; charset=utf-8',
|
||||
'.woff': 'font/woff',
|
||||
'.woff2': 'font/woff2',
|
||||
'.ttf': 'font/ttf',
|
||||
'.otf': 'font/otf',
|
||||
'.eot': 'application/vnd.ms-fontobject',
|
||||
'.mp4': 'video/mp4',
|
||||
'.webm': 'video/webm',
|
||||
'.wasm': 'application/wasm'
|
||||
};
|
||||
|
||||
function send(res, status, headers, stream) {
|
||||
res.writeHead(status, headers);
|
||||
if (stream) stream.pipe(res); else res.end();
|
||||
}
|
||||
|
||||
function isUnder(p, root) {
|
||||
const rel = path.relative(root, p);
|
||||
return !!rel && !rel.startsWith('..') && !path.isAbsolute(rel);
|
||||
}
|
||||
|
||||
const server = http.createServer((req, res) => {
|
||||
try {
|
||||
const parsed = url.parse(req.url || '/');
|
||||
let pathname = decodeURIComponent(parsed.pathname || '/');
|
||||
|
||||
// Hard normalize, prevent traversal
|
||||
pathname = path.normalize(pathname).replace(/^([/\\])*|\/+$/g, '/');
|
||||
let fp = path.join(ROOT, pathname);
|
||||
|
||||
// If path is a directory, serve index.html
|
||||
if (fs.existsSync(fp) && fs.statSync(fp).isDirectory()) {
|
||||
fp = path.join(fp, 'index.html');
|
||||
}
|
||||
|
||||
// If it doesn't exist and path didn't include .html, try appending index.html
|
||||
if (!fs.existsSync(fp) && !path.extname(fp)) {
|
||||
fp = path.join(fp, 'index.html');
|
||||
}
|
||||
|
||||
// Verify containment in ROOT
|
||||
if (!isUnder(fp, ROOT) && path.resolve(fp) !== path.resolve(ROOT, 'index.html')) {
|
||||
return send(res, 403, { 'content-type': 'text/plain; charset=utf-8' }, null);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(fp) || fs.statSync(fp).isDirectory()) {
|
||||
return send(res, 404, { 'content-type': 'text/plain; charset=utf-8' }, null);
|
||||
}
|
||||
|
||||
const ext = path.extname(fp).toLowerCase();
|
||||
const type = MIME[ext] || 'application/octet-stream';
|
||||
|
||||
const headers = { 'content-type': type };
|
||||
|
||||
// Caching: long cache for assets, no-cache for html
|
||||
if (ext === '.html') {
|
||||
headers['cache-control'] = 'no-cache';
|
||||
} else if (fp.includes('/_astro/') || fp.startsWith(path.join(ROOT, 'assets'))) {
|
||||
headers['cache-control'] = 'public, max-age=31536000, immutable';
|
||||
} else {
|
||||
headers['cache-control'] = 'public, max-age=3600';
|
||||
}
|
||||
|
||||
const stream = fs.createReadStream(fp);
|
||||
stream.on('open', () => send(res, 200, headers, stream));
|
||||
stream.on('error', () => send(res, 500, { 'content-type': 'text/plain; charset=utf-8' }, null));
|
||||
} catch (err) {
|
||||
send(res, 500, { 'content-type': 'text/plain; charset=utf-8' }, null);
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(PORT, '0.0.0.0', () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(`Static server listening on :${PORT}, root: ${ROOT}`);
|
||||
});
|
||||
Reference in New Issue
Block a user