144 lines
3.3 KiB
JavaScript
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();
|