Skip to content

Instantly share code, notes, and snippets.

@tzvetkoff
Last active February 17, 2022 22:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • 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(port, address, cert, key) {
this.port = port;
this.address = address;
this.cert = cert;
this.key = key;
const
_this = this,
ssl = !!cert && !!key,
serverOptions = ssl ? { cert: fs.readFileSync(cert), key: fs.readFileSync(key) } : {};
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.port, this.address);
}
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.toString();
message += ' '.slice(message.length);
message += ' ';
message += status;
message += ' ';
message += request.method;
message += ' ';
message += requestPath;
if (error) {
message += ' [ ';
message += error;
message += ' ]';
}
console.log(message);
}
}
new WebServer(
process.argv[2] || process.env.PORT || 1337,
process.argv[3] || process.env.HOST || '0.0.0.0',
process.argv[4] || process.env.SSL_CERT,
process.argv[5] || process.env.SSL_KEY,
).start();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment