tiny-https-server
Version:
Tiny web server with HTTPS support, static file serving, subdomain support, middleware support, and service worker support.
561 lines (460 loc) • 20.4 kB
JavaScript
/** Copyright (c) Manuel Lõhmus (MIT License). */
var WebCluster = require('./index.js'),
configSets = require("config-sets"),
{ isPrimary } = require('node:cluster'),
{ platform } = require('os'),
{ request } = require('node:http');
configSets.isSaveChanges = false;
var cluster = WebCluster({
isDebug: true,
parallelism: 1,
host: 'localhost',
port: 3000,
primary_domain: {
service_worker_version: "1",
is_new_service_worker_reload_browser: true
},
subdomains: {
"test.localhost": {
document_root: "./public/test",
service_worker_version: "0"
}
},
logDir: '',
contact_email: 'admin@localhost.local'
},
function _initServer(server) {
/*debugger;*/
server.addRequest({ path: "/heavy" }, function (req, res, next) {
res.writeProcessing();
setImmediate(function () {
res.writeHead(200);
for (var i = 0; i < 999999; i++) { }
res.end("heavy request");
});
});
server.addRequest({ host: "test.localhost", path: "/crash" }, function (req, res) {
throw new Error("Simulated crash ...");
});
}
);
if (isPrimary) { setTimeout(test, 5000); }
function test() {
testRunner("TESTS for tiny-https-server", { skip: false }, (test) => {
test("httpRequest('http://localhost/heavy') ", { skip: false, timeout: 15000 }, (check, done) => {
httpRequest('http://localhost/heavy', function (err, res) {
if (err) { done(err); return; }
check('status', res.status).mustBe(200);
check('data', res.data).mustBe('heavy request');
done();
});
});
test("httpRequest('http://localhost/service_worker_version') ", { skip: false }, (check, done) => {
httpRequest('http://localhost/service_worker_version', function (err, res) {
if (err) { done(err); return; }
check('status', res.status).mustBe(200);
check('data', res.data).mustBe('1');
done();
});
});
test("httpRequest('http://test.localhost/service_worker_version') ", { skip: platform() !== 'linux' }, (check, done) => {
httpRequest('http://test.localhost/service_worker_version', function (err, res) {
if (err) { done(err); return; }
check('status', res.status).mustBe(200);
check('data', res.data).mustBe('0');
done();
});
});
test("httpRequest('http://localhost/service_worker') ", { skip: false }, (check, done) => {
httpRequest('http://localhost/service_worker', function (err, res) {
if (err) { done(err); return; }
check('status', res.status).mustBe(200);
check('data', res.data).mustInclude('self.addEventListener(\'install\'');
done();
});
});
test("httpRequest('http://localhost/.well-known/blacklist') ", { skip: false }, (check, done) => {
httpRequest('http://localhost/.well-known/blacklist', function (err, res) {
if (err) { done(err); return; }
check('status', res.status).mustBe(200);
check('data', res.data).mustInclude('{');
done();
});
});
test("httpRequest('http://localhost/.well-known/traffic-advice') ", { skip: false }, (check, done) => {
httpRequest('http://localhost/.well-known/traffic-advice', function (err, res) {
if (err) { done(err); return; }
check('status', res.status).mustBe(200);
check('data', res.data).mustInclude('"user_agent":');
done();
});
});
test("httpRequest('http://localhost/.well-known/security.txt') ", { skip: false }, (check, done) => {
httpRequest('http://localhost/.well-known/security.txt', function (err, res) {
if (err) { done(err); return; }
check('status', res.status).mustBe(200);
check('data', res.data).mustInclude('Contact:');
done();
});
});
test("httpRequest('http://localhost/favicon-192x192.png') ", { skip: false }, (check, done) => {
httpRequest('http://localhost/favicon-192x192.png', function (err, res) {
if (err) { done(err); return; }
check('status', res.status).mustBe(200);
check('data', res.data.length).mustBe(8734);
done();
});
});
test("httpRequest('http://localhost/favicon-512x512.png') ", { skip: false }, (check, done) => {
httpRequest('http://localhost/favicon-512x512.png', function (err, res) {
if (err) { done(err); return; }
check('status', res.status).mustBe(200);
check('data', res.data.length).mustBe(23655);
done();
});
});
test("httpRequest('http://localhost/favicon.ico') ", { skip: false }, (check, done) => {
httpRequest('http://localhost/favicon.ico', function (err, res) {
if (err) { done(err); return; }
check('status', res.status).mustBe(200);
check('data', res.data.length).mustBe(18457);
done();
});
});
test("httpRequest('http://localhost/index.html') ", { skip: false }, (check, done) => {
httpRequest('http://localhost/index.html', function (err, res) {
if (err) { done(err); return; }
check('status', res.status).mustBe(200);
check('data', res.data).mustInclude('<!doctype html>');
done();
});
});
test("httpRequest('http://localhost/manifest.webmanifest') ", { skip: false }, (check, done) => {
httpRequest('http://localhost/manifest.webmanifest', function (err, res) {
if (err) { done(err); return; }
check('status', res.status).mustBe(200);
check('data', res.data).mustInclude('"start_url":');
done();
});
});
test("httpRequest('http://localhost/robots.txt') ", { skip: false }, (check, done) => {
httpRequest('http://localhost/robots.txt', function (err, res) {
if (err) { done(err); return; }
check('status', res.status).mustBe(200);
check('data', res.data).mustInclude('User-agent:');
done();
});
});
test("httpRequest('http://localhost/sitemap.xml') ", { skip: false }, (check, done) => {
httpRequest('http://localhost/sitemap.xml', function (err, res) {
if (err) { done(err); return; }
check('status', res.status).mustBe(200);
check('data', res.data).mustInclude('<urlset');
done();
});
});
test("httpRequest('http://localhost/node_modules/tiny-https-server')", { skip: false }, (check, done) => {
httpRequest('http://localhost/node_modules/tiny-https-server', function (err, res) {
if (err) { done(err); return; }
check('status', res.status).mustBe(200);
check('data', res.data).mustInclude('serviceWorker');
done();
});
});
test("httpRequest('http://localhost/index.php') ", { skip: false, timeout: 15000 }, (check, done) => {
httpRequest('http://localhost/index.php', function (err, res) {
if (err) { done(err); return; }
check('status', res.status).mustBe(400);
check('data', res.data).mustInclude('');
done();
});
});
});
/**
* Request function to send a request to the server.
* @param {string|URL} url URL to request.
* @param {(err:string, response:Response)=>void} cb Callback function to handle the response.
*
* @typedef Response Response object.
* @property {string} url Requested URL.
* @property {number} status Response status code.
* @property {Object} headers Response headers.
* @property {string} data Response data.
*/
function httpRequest(url, cb) {
var req = request(url, { port: cluster.serverOptions.port }, function (res) {
res.data = '';
res.on('data', function (chunk) { res.data += chunk; });
res.on('end', function () {
cb(null, {
url,
status: res.statusCode,
headers: res.headers,
data: res.data
});
});
});
req.on('error', function (e) { cb(e.message); });
req.end();
}
}
/**
* Test runner. Function to run unit tests in the console.
* @author Manuel Lõhmus (MIT License)
* @version 1.1.5
* [2024-12-29] adde d functionality to select tests by ID in the command line arguments (e.g. --testIDs=1 2 3)
* @example `npm test '--'` or `node index.test.js`
* @example `npm test '--' --help` or `node index.test.js --help`
* @example `npm test '--' --testIDs=1 2 3` or `node index.test.js --testIDs=1 2 3`
* @param {string} runnerName Test runner name.
* @param {{skip:boolean}} options Test runner options.
* @param {(test:Test)=>void} cb Callback function to run the unit tests.
* @returns {boolean} If the tests are OK
* @example testRunner('Module name', { skip: false }, function (test) {...});
*
* @callback Test Unit test callback function
* @param {string} testName Test name.
* @param {{skip:boolean,timeout:number}} options Test options. (default: {skip:false,timeout:3000})))
* @param {(check:Check,done:Done)=>void} fn Test function. Function parameters: check, done. `check` is used to check the test result. `done` is used to end the test.
* @returns {void}
* @example test("Test name", {skip:false,timeout:3000}, function(check,done){...});
* @example test("Test name", function(check,done){...});
* @example test("Test name", {skip:checkableObject === undefined}, function(check,done){...});
*
* @callback Check Check function to check the test result.
* @param {string} label Value name. Opional.
* @param {any} value Value to check.
* @returns {Validator}
* @example check('name', value).mustBe(true);
* @example check('name', value).mustNotBe(false);
* @example check('name', value).mustBe(true).done();
* @example check('name', value).mustBe(true).mustNotBe(false).done();
*
* @callback Done Callback function to end the test.
* @param {Error} err Error message. If the error message is empty, the test is considered successful.
* @returns {void}
*
* @typedef Validator
* @property {Check} check Check function to check the test result.
* @property {(value:any)=>Validator} mustBe Check if the value is equal to the specified value.
* @property {(value:any)=>Validator} mustNotBe Check if the value is not equal to the specified value.
* @property {(value:any)=>Validator} mustInclude Check if the value is included to the specified value.
* @property {Done} done Callback function to end the test.
*/
function testRunner(runnerName, options, cb) {
var globalScope = this || globalThis;
globalScope?.process?.on('uncaughtException', function noop() { });
testRunner.testRunnerOK = true;
clearTimeout(testRunner.exitTimeoutID);
var stdout = {},
timeouts = {},
countStarted = 0,
countCompleted = 0,
testsStarted = false,
testRunnerOK = true,
strSKIP = "\t\t[\x1b[100m\x1b[97m SKIP \x1b[0m]",
strTestsERR = "[\x1b[41m\x1b[97m The tests failed! \x1b[0m]",
strTestsDONE = "[\x1b[42m\x1b[97m The tests are done! \x1b[0m]",
{ help, testID } = arg_options();
if (help !== undefined) {
console.log(`
npm test '--' [OPTION1=VALUE1] [OPTION2=VALUE2] ...
or
node index.test.js [OPTION1=VALUE1] [OPTION2=VALUE2] ...
The following options are supported:
--help Display this help
--testID Number of the test to run (e.g. node index.test.js --testID=1 --testID=2 --testID=3)
`);
if (globalScope?.process?.argv[1].endsWith(".js")) { exitPressKey(); }
else { globalScope?.process?.exit(0); }
return;
}
if (!Array.isArray(testID)) { testID = testID ? [testID] : []; }
//skip all tests
if (options?.skip) {
testsStarted = "SKIP";
if (runnerName) { log(0, "SKIP > ", runnerName, strSKIP); }
testCompleted();
return testRunnerOK;
}
if (runnerName) { log(0, "START > ", runnerName); }
cb(test);
testsStarted = true;
testCompleted();
return testRunnerOK;
function log() {
var line = "";
for (let i = 1; i < arguments.length; i++) {
line += arguments[i];
}
if (stdout[arguments[0]]) {
stdout[arguments[0]] += line + "\n";
}
else {
stdout[arguments[0]] = line + "\n";
}
}
function print_stdout() {
console.log();
console.log(
Object.keys(stdout).reduce((output, value, i) => output += stdout[i], '')
);
}
/**
* Unit test function.
* @type {Test}
*/
function test(testName, options, fn) {
var startTime, endTime,
id = ++countStarted,
testOK = true,
label = " " + id + ".\tTEST > " + testName + "\t",
strOK = "\t[\x1b[42m\x1b[97m OK \x1b[0m]",
strERR = "\t[\x1b[41m\x1b[97m FAILED \x1b[0m] -> ";
//skip
if (options?.skip || testID && testID.length && !testID.includes(id)) {
log(id, label, "\t", strSKIP);
testCompleted();
return;
}
//timeout
timeouts[id] = setTimeout(function () {
done("timeout");
}, options?.timeout || 3000);
startTime = performance.now();
try {
if (fn(check, done)) { done(); }
}
catch (err) { done(err); }
/**
* Callback function to end the test.
* @type {Done}
*/
function done(err = '') {
endTime = performance.now();
if (err) { testRunnerOK = testOK = false; }
if (err || testOK)
log(id, label, ": ", (endTime - startTime).toFixed(2), "ms\t", err ? strERR : strOK, err || "");
if (timeouts[id]) { testCompleted(); }
clearTimeout(timeouts[id]);
delete timeouts[id];
}
/**
* Check function to check the test result.
* @type {Check}
*/
function check(label, value) {
if (arguments.length === 1) { value = label; label = 'returned'; }
if (label === undefined) { label = 'returned'; }
/**
* Selection fuctions to check.
* @type {Validator}
*/
return {
check,
mustBe: function mustBe(mustBe) {
if (value !== mustBe) { done("\x1b[44m\x1b[97m " + label + " \x1b[0m '" + value + "' \x1b[44m\x1b[97m must be \x1b[0m '" + mustBe + "'"); }
return this;
},
mustNotBe: function mustNotBe(mustNotBe) {
if (value === mustNotBe) { done("\x1b[44m\x1b[97m " + label + " \x1b[0m '" + value + "' \x1b[44m\x1b[97m must not be \x1b[0m '" + mustNotBe + "'"); }
return this;
},
mustInclude: function mustInclude(mustInclude) {
if (!value?.includes || !value.includes(mustInclude)) { done("\x1b[44m\x1b[97m " + label + " \x1b[0m '" + value + "' \x1b[44m\x1b[97m must include \x1b[0m '" + mustInclude + "'"); }
return this;
},
done
};
}
}
function testCompleted() {
countCompleted++;
if (!testsStarted || countStarted >= countCompleted) { return; }
if (runnerName) {
if (testsStarted === "SKIP") {
print_stdout();
}
else if (!testRunnerOK) {
log(++countStarted, "END > " + runnerName + "\t" + strTestsERR);
print_stdout();
}
else {
log(++countStarted, "END > ", runnerName, "\t", strTestsDONE);
print_stdout();
}
globalScope?.process?.removeAllListeners('uncaughtException');
if (globalScope?.process?.argv[1].endsWith(".js")) {
exitPressKey();
}
else if (globalScope?.process) {
if (!testRunnerOK) { testRunner.testRunnerOK = false; }
testRunner.exitTimeoutID = setTimeout(function (exit) {
exit(testRunner.testRunnerOK ? 0 : 1);
}, 100, globalScope?.process?.exit);
}
}
}
function exitPressKey() {
globalScope?.process?.stdin.setRawMode(true);
globalScope?.process?.stdin.resume();
globalScope?.process?.stdin.on('data', globalScope?.process?.exit.bind(globalScope?.process, testRunnerOK ? 0 : 1));
console.log('Press any key to exit');
}
function arg_options() {
if ("undefined" === typeof globalScope?.process) { return {}; }
var isKey = false,
key = '',
values,
args = globalScope?.process?.argv
.slice(2)
.join('')
.split('')
.reduce(function (args, c) {
if (c === '-') {
if (isKey && key && !args[key]) { args[key] = ['true']; }
isKey = true;
key = '';
return args;
}
if (c === '=') {
isKey = false;
if (!args[key]) { args[key] = []; }
values = args[key];
values.push('');
return args;
}
if (isKey && /\s/.test(c)) {
return args;
}
if (isKey) {
key += c;
return args;
}
values[values.length - 1] += c;
return args;
}, {});
if (isKey && key && !args[key]) { args[key] = ['true']; }
Object.keys(args).forEach((k) => {
if (!args[k].length) {
args[k] = '';
return;
}
if (args[k].length === 1) {
args[k] = convertValue(args[k][0].trim());
return;
}
args[k] = args[k].map((s) => {
return convertValue(s.trim());
});
});
return args;
function convertValue(val) {
if (val === 'null') { return null; }
if (val === 'true') { return true; }
if (val === 'false') { return false; }
if (!isNaN(Number(val))) { return Number(val); }
return val;
}
}
}