Skip to content

Instantly share code, notes, and snippets.

@tzvetkoff
Last active July 17, 2024 17:37
Show Gist options
  • Save tzvetkoff/89684671dd9432f07254 to your computer and use it in GitHub Desktop.
Save tzvetkoff/89684671dd9432f07254 to your computer and use it in GitHub Desktop.
Simple NodeJS HTTPd
#!/usr/bin/env node
const
fs = require('fs'),
http = require('http'),
https = require('https')
path = require('path'),
url = require('url');
class WebServer {
constructor(options) {
this.options = options;
const
_this = this,
ssl = !!options.cert,
serverOptions = ssl ? { cert: fs.readFileSync(options.cert), key: options.key ? fs.readFileSync(options.key) : undefined } : {};
this.server = (ssl ? https : http).createServer(serverOptions, (request, response) => {
try {
_this.serve(request, response);
} catch (error) {
_this.logRequest(500, request, undefined, error);
_this.serveText(500, 'text/plain', '500 Internal Server Error\n\n' + error + '\n', request, response);
}
});
}
start() {
this.server.listen(this.options.port || 1337, this.options.host || '0.0.0.0');
}
serve(request, response) {
const
_this = this,
requestPath = unescape(url.parse(request.url).pathname),
filePath = path.join(process.cwd(), requestPath);
fs.stat(filePath, (error, st) => {
if (error) {
_this.logRequest(404, request, requestPath);
_this.serveText(404, 'text/plain', '404 File Not Found\n', request, response);
return;
}
if (st.isDirectory()) {
_this.serveDirectory(filePath, requestPath, request, response);
} else {
_this.serveFile(filePath, requestPath, request, response);
}
});
}
serveText(status, contentType, text, request, response) {
response.writeHead(status, { 'Content-Length': Buffer.byteLength(text, 'utf8'), 'Content-Type': contentType });
response.write(text);
response.end();
}
serveFile(filePath, requestPath, request, response) {
const _this = this;
fs.stat(filePath, (error, stat) => {
if (error) {
_this.logRequest(404, request, requestPath);
_this.serveText(404, 'text/plain', '404 File Not Found\n', request, response);
return;
}
_this.logRequest(200, request, requestPath);
response.writeHead(200, { 'Content-Length': stat.size, 'Content-Type': _this.inferMimeType(filePath) });
if (request.method === 'HEAD') {
response.end();
} else {
fs.createReadStream(filePath).pipe(response, { end: true });
}
});
}
serveDirectory(dirPath, requestPath, request, response) {
const _this = this;
fs.readdir(dirPath, (error, items) => {
if (error) {
_this.logRequest(500, request, requestPath, error);
_this.serveText(500, 'text/plain', '500 Internal Server Error\n\n' + error + '\n', request, response);
return;
}
if (items.indexOf('index.html') !== -1) {
_this.serveFile(dirPath + '/index.html', requestPath, request, response);
return;
}
let html = '<!DOCTYPE html>';
html += '<html>';
html += '<head>';
html += '<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />';
html += '<title>Index of ' + requestPath + '</title>';
html += '<style>';
html += '* { margin: 0; padding: 0; }';
html += 'body { font: normal 12px/15px "Monospaced", monospace; color: #333333; background: #dcdcdc; }';
html += 'header, footer, section { margin: 8px; padding: 8px; border: 1px solid #000000; background: #ffffff; }';
html += 'footer { text-align: right; font-weight: bold; }';
html += 'h1 { margin: 0; padding: 0; font: normal 12px/15px "Monospaced", monospace; }';
html += 'table { width: 100%; border-collapse: collapse; }';
html += 'tbody tr:nth-of-type(odd) { background: #f0f0f0; }';
html += 'tbody tr:hover { background: #d8d8d8; }';
html += 'th, td { padding: 1px 2px; }';
html += 'th { border: 0 none; }';
html += 'td { border: 1px solid #cccccc; white-space: nowrap; }';
html += 'a, a:visited { text-decoration: none; color: #0000ff; }';
html += 'a:hover { text-decoration: underline; color: #ff0000; }';
html += '</style>';
html += '</head>';
html += '<body>';
html += '<header><h1>Index of <strong>' + requestPath + '</strong></h1></header>';
html += '<section>';
html += '<table cellpadding="0" cellspasing="0">';
html += '<colgroup>';
html += '<col />';
html += '<col width="1" />';
html += '<col width="1" />';
html += '</colgroup>';
html += '<thead>';
html += '<tr>';
html += '<th align="left">Name</th>';
html += '<th align="left">LastMod</th>';
html += '<th align="right">Size</th>';
html += '</tr>';
html += '</thead>';
html += '<tbody>';
if (requestPath !== '/') {
html += '<tr>';
html += '<td><a href="..">..</a>/</td>';
html += '<td></td>';
html += '<td></td>';
html += '</tr>';
}
const
pathPrefix = requestPath
.replace(/\/$/, '')
.split('/')
.map((item) => encodeURIComponent(item))
.join('/'),
dirs = [],
files = [],
sortFunction = (lft, rgt) => lft[0].toLowerCase().localeCompare(rgt[0].toLowerCase()),
formatFileSizeFunction = (size) => {
const log = (Math.log(size) / Math.log(1024)) | 0;
return (size / Math.pow(1024, log)).toFixed(2) + (log ? 'kmgtpezy'[log - 1] + 'b' : 'b');
};
for (let i = 0, length = items.length; i < length; ++i) {
const
item = items[i],
path = dirPath + '/' + item,
stat = fs.statSync(path);
if (stat.isDirectory()) {
dirs.push([item, pathPrefix + '/' + encodeURIComponent(item) + '/', 'DIR', stat.mtime]);
} else {
files.push([item, pathPrefix + '/' + encodeURIComponent(item), formatFileSizeFunction(stat.size), stat.mtime]);
}
}
dirs.sort(sortFunction);
files.sort(sortFunction);
const combined = dirs.concat(files);
for (let i = 0, length = combined.length; i < length; ++i) {
const item = combined[i];
html += '<tr>';
html += '<td><a href="' + item[1] + '">' + item[0] + '</a>' + (item[2] === 'DIR' ? '/' : '') + '</td>';
html += '<td>' + item[3].toISOString() + '</td>';
html += '<td align="right">' + item[2] + '</td>';
html += '</tr>';
}
html += '</tbody>';
html += '</table>';
html += '</section>';
html += '<footer>';
html += '<em>&copy; ' + new Date().getFullYear() + ' Latchezar Tzvetkoff</em>';
html += '</footer>';
html += '</body>';
html += '</html>';
_this.logRequest(200, request, requestPath);
_this.serveText(200, 'text/html', html, request, response);
});
}
inferMimeType(filePath) {
const
extension = path.extname(filePath),
knownMimeTypes = [
[/\.(html|css)$/, 'text/$1'],
[/\.js$/, 'text/javascript'],
[/\.(png|gif)$/, 'image/$1'],
[/\.jpe?g$/, 'image/jpeg'],
[/\.svg$/, 'image/svg+xml'],
[/\.(txt|c|cpp|h|hpp|cxx|hxx|php|rb|ac|am|in|pl|pm|sql|md)$/, 'text/plain']
];
for (let i = 0, length = knownMimeTypes.length; i < length; ++i) {
const
pair = knownMimeTypes[i],
regex = pair[0],
type = pair[1];
if (regex.test(extension)) {
return extension.replace(regex, type);
}
}
return 'application/octet-stream';
}
logRequest(status, request, requestPath, error) {
let message = request.socket.remoteAddress;
message += ' '.slice(message.length);
message += ' ';
message += status;
message += ' ';
message += request.method;
message += ' ';
message += requestPath;
if (error) {
message += ' [ ';
message += error;
message += ' ]';
}
console.log(message);
}
}
var options = {
host: process.env.BIND_HOST || '0.0.0.0',
port: parseInt(process.env.BIND_PORT, 10) || 1337,
cert: process.env.TLS_CERT,
key: process.env.TLS_PRIVKEY,
};
for (var i = 2, arg = process.argv[i]; i < process.argv.length; arg = process.argv[++i]) {
switch (arg) {
case '-h':
case '--help':
console.log('Usage:');
console.log(' ' + process.argv[0] + ' ' + process.argv[1] + ' [options]');
console.log();
console.log('Options:');
console.log(' -h, --help Print this message and exit');
console.log(' -H H, --host=H Set bind host (env: BIND_HOST, default: 0.0.0.0)');
console.log(' -P P, --port=P Set bind port (env: BIND_PORT, default: 1337)');
console.log(' -C C, --tls-cert=C Set TLS certificate (env: TLS_CERT, default: undefined)');
console.log(' -K K, --tls-privkey=K Set TLS private key (env: TLS_PRIVKEY, default: undefined)');
process.exit(0);
case '-H':
case '--host':
options.host = process.argv[++i];
break;
case '-P':
case '--port':
options.port = process.argv[++i];
break;
case '-C':
case '--tls-cert':
options.cert = process.argv[++i];
break;
case '-K':
case '--tls-privkey':
options.key = process.argv[++i];
break;
default:
if (arg.indexOf('-H') == 0) {
options.host = arg.slice(2);
} else if (arg.indexOf('--host=') == 0) {
options.host = arg.slice(7);
} else if (arg.indexOf('-P') == 0) {
options.host = arg.slice(2);
} else if (arg.indexOf('--port=') == 0) {
options.host = arg.slice(7);
} else if (arg.indexOf('-C') == 0) {
options.host = arg.slice(2);
} else if (arg.indexOf('--tls-cert=') == 0) {
options.host = arg.slice(11);
} else if (arg.indexOf('-K') == 0) {
options.host = arg.slice(2);
} else if (arg.indexOf('--tls-privkey=') == 0) {
options.host = arg.slice(14);
} else {
console.error('unknown option/argument: %s', arg);
process.exit(1);
}
}
}
if ((options.cert && !options.key) || (options.key && !options.key)) {
console.error('options --cert and --key are required together');
process.exit(1);
}
new WebServer(options).start();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment