Files
packager/http-server.js
T

144 lines
3.3 KiB
JavaScript

'use strict';
const http = require('http');
const promisify = require('util').promisify;
const fs = require('fs');
const pathUtil = require('path');
const lstat = promisify(fs.lstat);
const readFile = promisify(fs.readFile);
// checking for path traversal requires trailing slash
const webRoot = pathUtil.join(__dirname, '/');
function startServer() {
const port = process.env.PORT || 8080;
const contentTypes = {
'.html': 'text/html',
'.css': 'text/css',
'.js': 'text/javascript',
'.svg': 'image/svg+xml',
'.ico': 'image/vnd.microsoft.icon',
'.wav': 'audio/wav',
'.woff': 'font/woff',
'.woff2': 'font/woff2',
'.ttf': 'font/ttf',
'.md': 'text/plain',
};
const defaultContentType = 'application/octet-stream';
class HttpError extends Error {
constructor(code, message) {
super(message);
this.code = code;
this.message = message;
}
}
class Http404 extends HttpError {
constructor() {
super(404, '404 Not Found');
}
}
async function findFile(root, path) {
const indexes = ['index.html', 'README.md'];
const extensions = ['', '.html', '.md'];
// prevent null bytes
if (path.indexOf('\0') !== -1) {
return null;
}
// remove escape codes
path = unescape(path);
// remove query string
if (path.indexOf('?') !== -1) {
path = path.substr(0, path.indexOf('?'));
}
path = pathUtil.join(root, path);
// check for path traversal
if (!path.startsWith(root)) {
return null;
}
const originalPath = path;
try {
if ((await lstat(path)).isDirectory()) {
let found = false;
for (const i of indexes) {
try {
path = pathUtil.join(originalPath, i);
if ((await lstat(path)).isFile()) {
found = true;
break;
}
} catch (e) {
// ignore
}
}
if (!found) return null;
}
} catch (e) {
let found = false;
for (const i of extensions) {
try {
path = originalPath + i;
if ((await lstat(path)).isFile()) {
found = true;
break;
}
} catch (e) {
// ignore
}
}
if (!found) return null;
}
return path;
}
async function handler(req, res) {
const path = await findFile(webRoot, req.url || '/');
if (path === null) throw new Http404();
const data = await readFile(path);
const extension = pathUtil.extname(path);
res.setHeader('Content-Length', data.length);
res.setHeader('Content-Type', contentTypes[extension] || defaultContentType);
res.setHeader('Cache-Control', 'no-store');
res.write(data);
};
const server = http.createServer(async (req, res) => {
try {
await handler(req, res);
} catch (e) {
if (e.code) {
res.statusCode = e.code;
res.write(e.message);
} else {
res.statusCode = 500;
res.write('internal server error');
console.error(e.stack || e);
}
}
res.end();
});
server.listen(port);
portConnectionInfo(port);
}
function portConnectionInfo(port) {
console.log('Visit any of these links in your browser:');
console.log(` http://localhost:${port}/`);
console.log(` http://127.0.0.1:${port}/`);
}
startServer();