@bs-core/shell
Version:
The Bamboo Shell
4,441 lines • 175 kB
JavaScript
import * as fs from 'node:fs';
import * as util from 'node:util';
import { performance } from 'node:perf_hooks';
import * as http from 'node:http';
import * as crypto from 'node:crypto';
import * as zlib from 'node:zlib';
import * as streams from 'node:stream/promises';
import * as stream from 'node:stream';
import { PassThrough } from 'node:stream';
import * as fsPromises from 'node:fs/promises';
import * as path from 'node:path';
import * as https from 'node:https';
import * as os from 'node:os';
import * as readline from 'node:readline';
/**
* Config manager module. Provides functions to retrieve config values from various sources like
* CLI, environment variables, and env file. Includes utility functions to handle config value
* lookup, type conversion, error handling etc.
*/
// imports here
// Config consts here
// The env var that contains the name of the .env file
const CFG_ENV_FILE = "ENV_FILE";
// The env var that contains the name of the cfg file
const CFG_CFG_FILE = "CFG_FILE";
// Private variables here
// Stores the parsed contents of the .env file as key-value pairs
let _envFileStore;
// Stores the parsed contents of the cfg file as an object
let _cfgFileStore;
// Stores the messages generated during configuration.
// NOTE: This is used because the configuration manager is used by Logger
// and it becomes a chicken/egg situation when initialising the Logger
let _messageStore;
/**
* Enumeration of supported configuration value types.
* Can be used when retrieving a config value to specify the expected type.
*/
var ConfigType;
(function (ConfigType) {
ConfigType["String"] = "String";
ConfigType["Number"] = "Number";
ConfigType["Boolean"] = "Boolean";
ConfigType["Object"] = "Object";
ConfigType["Array"] = "Array";
})(ConfigType || (ConfigType = {}));
/**
* Represents an error that occurred while retrieving a config value
*/
class ConfigError {
message;
constructor(message) {
this.message = message;
}
}
// Private methods here
/**
* Converts a string value to the specified configuration type.
*
* NOTE: This is not used for the config files so we dont check
* for Object or Array types.
*
* @param value - The string value to convert.
* @param type - The expected configuration type.
* @returns The converted value as a number, string, or boolean.
*/
function convertValue(value, type) {
//Check the type
switch (type) {
case ConfigType.Number:
return parseInt(value);
case ConfigType.Boolean:
// Only accept Y or TRUE (case insensitive) to mean true
if (value.toUpperCase() === "Y" || value.toUpperCase() === "TRUE") {
return true;
}
// Everything else is false
return false;
default:
// All that is left is String and this is already a string!
return value;
}
}
/**
* Checks the command line arguments for a configuration value matching the
* specified configuration key.
*
* @param config - The configuration key to look for in the command line arguments.
* @param type - The expected type of the configuration value.
* @param options - Additional options for configuring the behavior of the function.
* @returns The configuration value from the command line arguments, converted to the specified type, or `null` if the configuration value is not found.
*/
function checkCli(config, type, options) {
// Ignore the first 2 params (node bin and executable file)
let cliParams = process.argv.slice(2);
// The convention used for config params on the command line is:
// Convert to lowercase, replace '_' with '-' and prepend "--"
let cliParam = `--${config.toLowerCase().replaceAll("_", "-")}`;
// Command line flags are just prepended with a '-'
let cmdLineFlag = options.cmdLineFlag !== undefined ? `-${options.cmdLineFlag}` : "";
// If the param is assigned a value on the cli it has the format:
// --param=value
// otherwise the format is and it implies true:
// --parm or -flag
let regExp;
if (options.cmdLineFlag === undefined) {
// No flag specified so only look for the param and an assigned value
regExp = new RegExp(`^${cliParam}=(.+)$`);
}
else {
// Look for param and an assigned value or cmd line flag
regExp = new RegExp(`^${cliParam}=(.+)$|^${cliParam}$|^${cmdLineFlag}$`);
}
let value;
// Step through each cli params until you find a match
for (let i = 0; i < cliParams.length; i++) {
let match = cliParams[i].match(regExp);
let paramOrFlag;
if (match === null) {
// There was no match so look at the next param
continue;
}
// If a value was supplied then match[1] will contain a value
if (match[1] !== undefined) {
paramOrFlag = match[0];
value = match[1];
}
else {
paramOrFlag = match[0];
// The presence of the flag/param without a value implies a true value
value = "Y";
}
// Check if we can or should log that we found it
// NOTE: If we log it we want to indicate is was found on the CLI
if (!options.silent) {
_messageStore.add(`CLI parameter/flag (${paramOrFlag}) = (${options.redact ? "redacted" : value})`);
}
// We are done so break out of the loop
break;
}
// Return null if we have no value
if (value === undefined) {
return null;
}
return convertValue(value, type);
}
/**
* Retrieves a configuration value from an environment variable.
*
* @param config - The configuration key to retrieve from the environment.
* @param type - The expected type of the configuration value.
* @param options - Additional options for configuring the behavior of the function.
* @returns The configuration value converted to the specified type, or `null` if the environment variable is not set.
*/
function checkEnvVar(config, type, options) {
// NOTE: Always convert to upper case for env vars
let evar = config.toUpperCase();
let value = process.env[evar];
// Return null if we have no value
if (value === undefined) {
return null;
}
// If we are here then we found it, now lets check if we can or should
// log that we found it
// NOTE: If we log it we want to indicate is was found in an env var
if (!options.silent) {
_messageStore.add(`Env var (${evar}) = (${options.redact ? "redacted" : value})`);
}
return convertValue(value, type);
}
/**
* Retrieves a configuration value from an environment file store.
*
* @param config - The configuration key to retrieve from the environment file.
* @param type - The expected type of the configuration value.
* @param options - Additional options for configuring the behavior of the function.
* @returns The configuration value converted to the specified type, or `null` if the configuration is not found in the environment file.
*/
function checkEnvFile(config, type, options) {
// NOTE: Always convert to upper case when checking the env file store
let evar = config.toUpperCase();
let value = _envFileStore.get(evar);
// Return null if we have no value
if (value === undefined) {
return null;
}
// If we are here then we found it, now lets check if we can or should
// log that we found it
// NOTE: If we log it we want to indicate it was found in the env file
if (!options.silent) {
_messageStore.add(`Env var from env file (${evar}) = (${options.redact ? "redacted" : value})`);
}
return convertValue(value, type);
}
/**
* Retrieves a configuration value from a configuration file store.
*
* @param config - The configuration key to retrieve from the configuration file.
* @param options - Additional options for configuring the behavior of the function.
* @returns The configuration value, or `null` if the configuration is not found in the configuration file.
*/
function checkCfgFile(config, options) {
let value = _cfgFileStore.get(config);
// Return null if we have no value
if (value === undefined) {
return null;
}
// If we are here then we found it, now lets check if we can or should
// log that we found it
// NOTE: If we log it we want to indicate it was found in the cfg file
if (!options.silent) {
_messageStore.add(`Config from cfg file (${config}) = (${options.redact ? "redacted" : JSON.stringify(value)})`);
}
// No need for any convertions, just return the value
return value;
}
/**
* Retrieves a configuration value from various sources, with the following precedence:
* 1. Command-line arguments
* 2. Environment variables
* 3. Environment file (.env)
* 4. Configuration file
*
* If the configuration value is not found in any of these sources, a default value can be provided.
*
* @param config - The configuration key to retrieve.
* @param type - The expected type of the configuration value.
* @param defaultVal - The default value to use if the configuration is not found.
* @param configOptions - Additional options for configuring the behavior of the function.
* @returns The configuration value, or the default value if the configuration is not found.
* @throws {ConfigError} If the configuration is required and not found.
*/
function get(config, type, defaultVal, configOptions) {
// Set up the defaults if not provided
let options = {
silent: false,
redact: false,
...configOptions,
};
// Check the CLI first, i.e. CLI has higher precedence then env vars
// of the cfg file
let value = checkCli(config, type, options);
if (value !== null) {
return value;
}
// OK it's not in the CLI so lets check the env vars, env var has higher
// precedence then the .env file
value = checkEnvVar(config, type, options);
if (value !== null) {
return value;
}
// OK it's not in the env vars either so check the env file store
value = checkEnvFile(config, type, options);
if (value !== null) {
return value;
}
// OK it's not in the env file store either so check the cfg store
value = checkCfgFile(config, options);
if (value !== null) {
return value;
}
// If we are here then the value was not found - use default provided
// NOTE: The default SHOULD have the correct type so do not do a conversion
if (defaultVal === undefined) {
// If the default was not provided then the config WAS required. In this
// scenario we need to throw an error
throw new ConfigError(`Config parameter (${config}) not found!`);
}
// Lets check if we can or should log the default value
// NOTE: If we log it we want to indicate is the default value
if (!options.silent) {
_messageStore.add(`Default value used for (${config}) = (${options.redact ? "redacted" : defaultVal})`);
}
return defaultVal;
}
/**
* Reads the contents of the specified .env file and adds the key-value
* pairs to the _envFileStore.
*
* @param envFile - The path to the .env file to read.
* @throws {ConfigError} If an error occurs while reading the .env file.
*/
function parseEnvFile(envFile) {
let lines = [];
try {
_messageStore.add(`Reading config info from .env file (${envFile})`);
// Read env file and split it into lines ...
let contents = fs.readFileSync(envFile, "utf8");
// ... makes sure if works for DOS and linux files!
lines = contents.split(/\r?\n/);
}
catch (e) {
throw new ConfigError(`The following error occured when trying to open the .env file (${envFile}) - (${e})`);
}
// Iterate through each line
for (let line of lines) {
// If the line is commented out or blank then skip it
if (line.length === 0 || line.startsWith("#")) {
continue;
}
// Don't use split() here because the value may contain an "="
let index = line.indexOf("=");
// Check if there was an equal in the line - if not then skip this line
if (index === -1) {
continue;
}
// Get the key/value pair - make sure to trim them as well
let key = line.slice(0, index).trim();
let value = line.slice(index + 1).trim();
// Check if the value is delimited with single or double quotes
if ((value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))) {
// Strip them away
value = value.slice(1, value.length - 1);
}
// Stick it in the env file store
// NOTE: Make key upper case to match env vars conventions
_envFileStore.set(key.toUpperCase(), value);
_messageStore.add(`Added (${key.toUpperCase()}) to the env file store`);
}
}
/**
* Reads the contents of a cfg file and adds the key-value pairs to the
* configuration store.
*
* @param cfgFile - The path to the configuration file to read.
* @throws {ConfigError} If an error occurs while reading or parsing the configuration file.
*/
function readCfgFile(cfgFile) {
let contents;
try {
_messageStore.add(`Reading config info from cfg file (${cfgFile})`);
// Read the cfg file
contents = fs.readFileSync(cfgFile, "utf8");
}
catch (e) {
throw new ConfigError(`The following error occured when trying to open the cfg file (${cfgFile}) - (${e})`);
}
try {
_messageStore.add("Adding the cfg file contents to the cfg store");
_cfgFileStore = new Map(Object.entries(JSON.parse(contents)));
}
catch (e) {
throw new ConfigError(`The following error occured when trying to add (${contents}) to the cfg store - (${e})`);
}
}
/**
* Initializes the configuration manager by setting up the necessary stores and
* parsing any specified environment and configuration files.
*
* The function first initializes the `_envFileStore`, `_cfgFileStore`, and
* `_messageStore` stores. It then checks if a `.env` file has been specified
* in the configuration and, if so, calls the `parseEnvFile` function to parse
* the contents of the file and add the key-value pairs to the `_envFileStore`.
*
* Next, the function checks if a configuration file has been specified in the
* configuration and, if so, calls the `readCfgFile` function to read the
* contents of the file and add the key-value pairs to the `_cfgFileStore`.
*
* This function is typically called during the initialization of the
* application to ensure that the configuration manager is properly set up and
* ready to use.
*/
function init$1() {
// Initialise the stores
_envFileStore = new Map();
_cfgFileStore = new Map();
_messageStore = new Set();
// Check if the user has specified a .env file
let envFile = configMan.getStr(CFG_ENV_FILE, "");
if (envFile.length > 0) {
parseEnvFile(envFile);
}
else {
_messageStore.add("No .env file specified");
}
// Check if the user has specified a cfg file
// NOTE: The cfg file config CAN be specified in the .env file since it
// has already been parsed
let cfgFile = configMan.getStr(CFG_CFG_FILE, "");
if (cfgFile.length > 0) {
readCfgFile(cfgFile);
}
else {
_messageStore.add("No cfg file specified");
}
}
// Public methods here
/**
* Provides a set of functions for retrieving configuration values from various
* sources, including environment variables and configuration files.
*
* The `configMan` object is a frozen object that contains the following methods:
*
* - `getStr(config: string, defaultVal?: string, options?: ConfigOptions): string`
* - Retrieves a string configuration value with a default value if not set.
* - `getBool(config: string, defaultVal?: boolean, options?: ConfigOptions): boolean`
* - Retrieves a boolean configuration value with a default value if not set.
* - `getNum(config: string, defaultVal?: number, options?: ConfigOptions): number`
* - Retrieves a number configuration value with a default value if not set.
* - `getMessages(): IterableIterator<[string, string]>`
* - Retrieves an iterator over the key-value pairs of the message store.
* - `clearMessages(): void`
* - Clears all messages stored in the message store.`
*
* NOTE: Freezing the object prevents modifications to the exported API.
*/
const configMan = Object.freeze({
/**
* Retrieves a string configuration value with a default value if not set.
*
* @param config - The name of the config parameter to retrieve.
* @param defaultVal - A default value if the config is not set. NOTE: This must be of the correct type.
* @param configOptions - The config options.
* @returns The string configuration value, or the default value if not set.
*/
getStr: (config, defaultVal, options) => {
return get(config, ConfigType.String, defaultVal, options);
},
/**
* Retrieves a boolean configuration value with a default value if not set.
*
* @param config - The name of the config parameter to retrieve.
* @param defaultVal - A default value if the config is not set. NOTE: This must be of the correct type.
* @param configOptions - The config options.
* @returns The boolean configuration value, or the default value if not set.
*/
getBool: (config, defaultVal, options) => {
return get(config, ConfigType.Boolean, defaultVal, options);
},
/**
* Retrieves a number configuration value with a default value if not set.
*
* @param config - The name of the config parameter to retrieve.
* @param defaultVal - A default value if the config is not set. NOTE: This must be of the correct type.
* @param configOptions - The config options.
* @returns The number configuration value, or the default value if not set.
*/
getNum: (config, defaultVal, options) => {
return get(config, ConfigType.Number, defaultVal, options);
},
/**
* Retrieves an object configuration value with a default value if not set.
*
* @param config - The name of the config parameter to retrieve.
* @param defaultVal - A default value if the config is not set. NOTE: This must be of the correct type.
* @param configOptions - The config options.
* @returns The object configuration value, or the default value if not set.
*/
getObject: (config, defaultVal, options) => {
return get(config, ConfigType.Object, defaultVal, options);
},
/**
* Retrieves an array configuration value with a default value if not set.
*
* @param config - The name of the config parameter to retrieve.
* @param defaultVal - A default value if the config is not set. NOTE: This must be of the correct type.
* @param configOptions - The config options.
* @returns The array configuration value, or the default value if not set.
*/
getArray: (config, defaultVal, options) => {
return get(config, ConfigType.Array, defaultVal, options);
},
/**
* Retrieves an iterator over the key-value pairs of the message store.
*
* @returns An iterator over the key-value pairs of the message store.
*/
getMessages: () => {
return _messageStore.entries();
},
/**
* Clears all messages stored in the message store.
*/
clearMessages: () => {
_messageStore.clear();
},
});
// Time to kick this puppy!
init$1();
// imports here
// Config consts here
const CFG_LOG_LEVEL = "LOG_LEVEL";
const CFG_LOG_TIMESTAMP = "LOG_TIMESTAMP";
const CFG_LOG_TIMESTAMP_LOCALE = "LOG_TIMESTAMP_LOCALE";
const CFG_LOG_TIMESTAMP_TZ = "LOG_TIMESTAMP_TZ";
// Types here
var LogLevel;
(function (LogLevel) {
LogLevel[LogLevel["COMPLETE_SILENCE"] = 0] = "COMPLETE_SILENCE";
LogLevel[LogLevel["QUIET"] = 100] = "QUIET";
LogLevel[LogLevel["INFO"] = 200] = "INFO";
LogLevel[LogLevel["START_UP"] = 250] = "START_UP";
LogLevel[LogLevel["DEBUG"] = 300] = "DEBUG";
LogLevel[LogLevel["TRACE"] = 400] = "TRACE";
})(LogLevel || (LogLevel = {}));
// Logger class here
class Logger {
// Private properties here
_name;
_timestamp;
_timestampLocale;
_timestampTz;
_logLevel;
// Private methods here
/**
* Generates a timestamp string to prefix log messages.
* Returns an empty string if timestamps are disabled. Otherwise returns
* the formatted timestamp string.
*/
timestamp() {
// If we are not supposed to generate timestamps then return nothing
if (!this._timestamp) {
return "";
}
let now = new Date();
if (this._timestampLocale === "ISO") {
// Make sure to add a trailing space!
return `${now.toISOString()} `;
}
// Make sure to add a trailing space!
return `${now.toLocaleString(this._timestampLocale, {
timeZone: this._timestampTz,
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
hour12: false,
fractionalSecondDigits: 3,
})} `;
}
convertLevel(level) {
let logLevel;
switch (level.toUpperCase()) {
case "": // This is in case it is not set
logLevel = LogLevel.INFO;
break;
case "SILENT":
logLevel = LogLevel.COMPLETE_SILENCE;
break;
case "QUIET":
logLevel = LogLevel.QUIET;
break;
case "INFO":
logLevel = LogLevel.INFO;
break;
case "STARTUP":
logLevel = LogLevel.START_UP;
break;
case "DEBUG":
logLevel = LogLevel.DEBUG;
break;
case "TRACE":
logLevel = LogLevel.TRACE;
break;
default:
throw new Error(`Log Level (${level}) is unknown.`);
}
return logLevel;
}
// constructor here
constructor(name) {
this._name = name;
this._timestamp = configMan.getBool(CFG_LOG_TIMESTAMP, false);
this._timestampLocale = configMan.getStr(CFG_LOG_TIMESTAMP_LOCALE, "ISO");
this._timestampTz = configMan.getStr(CFG_LOG_TIMESTAMP_TZ, "UTC");
this._logLevel = this.convertLevel(configMan.getStr(CFG_LOG_LEVEL, ""));
// Now get the messages from the confgiMan for display
let messages = configMan.getMessages();
for (const message of messages) {
this.startupMsg("Logger", message[0]);
}
configMan.clearMessages();
}
fatal(...args) {
// fatals are always logged
let msg = util.format(`${this.timestamp()}FATAL: ${this._name}: ${args[0]}`, ...args.slice(1));
console.error(msg);
}
error(...args) {
// errors are always logged unless level = LOG_COMPLETE_SILENCE
if (this._logLevel > LogLevel.COMPLETE_SILENCE) {
let msg = util.format(`${this.timestamp()}ERROR: ${this._name}: ${args[0]}`, ...args.slice(1));
console.error(msg);
}
}
warn(...args) {
// warnings are always logged unless level = LOG_COMPLETE_SILENCE
if (this._logLevel > LogLevel.COMPLETE_SILENCE) {
let msg = util.format(`${this.timestamp()}WARN: ${this._name}: ${args[0]}`, ...args.slice(1));
console.warn(msg);
}
}
info(...args) {
if (this._logLevel >= LogLevel.INFO) {
let msg = util.format(`${this.timestamp()}INFO: ${this._name}: ${args[0]}`, ...args.slice(1));
console.info(msg);
}
}
startupMsg(...args) {
if (this._logLevel >= LogLevel.START_UP) {
let msg = util.format(`${this.timestamp()}STARTUP: ${this._name}: ${args[0]}`, ...args.slice(1));
console.info(msg);
}
}
shutdownMsg(...args) {
if (this._logLevel >= LogLevel.START_UP) {
let msg = util.format(`${this.timestamp()}SHUTDOWN: ${this._name}: ${args[0]}`, ...args.slice(1));
console.info(msg);
}
}
debug(...args) {
if (this._logLevel >= LogLevel.DEBUG) {
let msg = util.format(`${this.timestamp()}DEBUG: ${this._name}: ${args[0]}`, ...args.slice(1));
console.info(msg);
}
}
trace(...args) {
if (this._logLevel >= LogLevel.TRACE) {
let msg = util.format(`${this.timestamp()}TRACE: ${this._name}: ${args[0]}`, ...args.slice(1));
console.info(msg);
}
}
force(...args) {
// forces are always logged even if level == LOG_COMPLETE_SILENCE
let msg = util.format(`${this.timestamp()}FORCED: ${this._name}: ${args[0]}`, ...args.slice(1));
console.error(msg);
}
setLevel(level) {
this._logLevel = level;
}
}
// NOTE: To use this with endpoints using self signed certs add this env var
// NODE_TLS_REJECT_UNAUTHORIZED=0
// imports here
// Misc consts here
const LOG_TAG = "request";
// Module private variables here
const _logger$1 = new Logger(LOG_TAG);
// Error classes here
class ReqAborted {
timedOut;
message;
constructor(timedOut, message) {
this.timedOut = timedOut;
this.message = message;
}
}
class ReqError {
status;
message;
constructor(status, message) {
this.status = status;
this.message = message;
}
}
// Private methods here
async function callFetch(origin, path, options, body) {
// Build the url
let url = `${origin}${path}`;
// And add the query string if one has been provided
if (options.searchParams !== undefined) {
url += `?${new URLSearchParams(options.searchParams)}`;
}
let timeoutTimer;
// Create an AbortController if a timeout has been provided
if (options.timeout) {
const controller = new AbortController();
// NOTE: this will overwrite a signal if one has been provided
options.signal = controller.signal;
timeoutTimer = setTimeout(() => {
controller.abort();
}, options.timeout * 1000);
}
let results = await fetch(url, {
method: options.method,
headers: options.headers,
body,
keepalive: options.keepalive,
cache: options.cache,
credentials: options.credentials,
mode: options.mode,
redirect: options.redirect,
referrer: options.referrer,
referrerPolicy: options.referrerPolicy,
signal: options.signal,
}).catch((e) => {
// Check if the request was aborted
if (e.name === "AbortError") {
// If timeout was set then the req must have timed out
if (options.timeout) {
throw new ReqAborted(true, `Request timeout out after ${options.timeout} seconds`);
}
throw new ReqAborted(false, "Request aborted");
}
// Need to check if we started a timeout
if (timeoutTimer !== undefined) {
clearTimeout(timeoutTimer);
}
// We don't know what the error is so pass it back
throw e;
});
// Need to check if we started a timeout
if (timeoutTimer !== undefined) {
clearTimeout(timeoutTimer);
}
// We will throw an error if the response is not 2XX
if (!results.ok) {
let message = await results.text();
throw new ReqError(results.status, message.length === 0 ? results.statusText : message);
}
return results;
}
async function handleResponseData(results) {
// No point worrying if the body is JSON at first, because we know its text
const body = await results.text();
// If the body exists then check if it is JSON
if (body.length > 0) {
// Check if the content type is JSON
const contentType = results.headers.get("content-type");
if (contentType?.startsWith("application/json")) {
return JSON.parse(body);
}
}
// If we are here, the body wasnt JSON so just return the text
return body;
}
// Public methods here
let request = async (origin, path, reqOptions) => {
// We need to remember the start time
const startTime = performance.now();
_logger$1.trace("Request for origin (%s) path (%s)", origin, path);
// Set the default values
let options = {
method: "GET",
timeout: 0,
keepalive: true,
handleResponse: true,
cache: "no-store",
mode: "cors",
credentials: "include",
redirect: "follow",
referrerPolicy: "no-referrer",
...reqOptions,
};
// Make sure the headers is set to something for later
if (options.headers === undefined) {
options.headers = {};
}
// If a bearer token is provided then add a Bearer auth header
if (options.bearerToken !== undefined) {
options.headers.Authorization = `Bearer ${options.bearerToken}`;
}
// If the basic auth creds are provided add a Basic auth header
if (options.auth !== undefined) {
let token = Buffer.from(`${options.auth.username}:${options.auth.password}`).toString("base64");
options.headers.Authorization = `Basic ${token}`;
}
let payloadBody;
// Automatically stringify and set the header if this is a JSON payload
// BUT dont do it for GETs and DELETE since they can have no body
if (options.body !== undefined &&
options.method !== "GET" &&
options.method !== "DELETE") {
// Rem an array is an object to!
if (typeof options.body === "object") {
// Add the content-type if it hasn't been provided
if (options.headers?.["content-type"] === undefined) {
options.headers["content-type"] = "application/json; charset=utf-8";
}
payloadBody = JSON.stringify(options.body);
}
else {
payloadBody = options.body;
}
}
// Call fetch
let response = await callFetch(origin, path, options, payloadBody);
// Build the response
let res = {
statusCode: response.status,
headers: response.headers,
body: undefined, // set to undefined for now
responseTime: 0,
};
// Check if we should handle the response for the user
if (options.handleResponse) {
// Yes, so handle and set the body
res.body = await handleResponseData(response).catch((e) => {
const msg = `Error handling response data for (${origin}) (${path}) - (${e}))`;
throw new Error(msg);
});
}
else {
// No, so set the response
res.response = response;
}
// Don't forget to set the response time
res.responseTime = Math.round(performance.now() - startTime);
return res;
};
// Classes here
class HttpError {
status;
message;
constructor(status, message = "Achtung Baby!") {
this.status = status;
this.message = message;
}
}
class HttpRedirect {
statusCode;
location;
message;
constructor(statusCode = 302, location, message = "") {
this.statusCode = statusCode;
this.location = location;
this.message = message;
}
}
class ServerRequest extends http.IncomingMessage {
// Properties here
urlObj;
params;
middlewareProps;
sseServer;
json;
body;
matchedInfo;
dontCompressResponse;
// Constructor here
constructor(socket) {
super(socket);
// When this object is instantiated the body of the req has not yet been
// received so the details, such as the URL, will not be known until later
this.urlObj = new URL("http://localhost/");
this.params = {};
this.middlewareProps = {};
this.dontCompressResponse = false;
}
getCookie = (cookieName) => {
// Get the cookie header and spilt it up by cookies -
// NOTE: cookies are separated by semi colons
let cookies = this.headers.cookie?.split(";");
if (cookies === undefined) {
// Nothing to do so just return
return null;
}
// Loop through the cookies
for (let cookie of cookies) {
// Split the cookie up into a key value pair
// NOTE: key/value is separated by an equals sign and has leading spaces
let [name, value] = cookie.trim().split("=");
// Make sure it was a validly formatted cookie
if (value === undefined) {
// It is not a valid cookie so skip it
continue;
}
// Check if we found the cookie
if (name === cookieName) {
// Return the cookie value
return value;
}
}
return null;
};
setServerTimingHeader = (value) => {
this.headers["Server-Timing"] = value;
};
}
class ServerResponse extends http.ServerResponse {
// Properties here
_receiveTime;
_redirected;
_latencyMetricName;
_serverTimingsMetrics;
json;
body;
proxied;
// constructor here
constructor(req) {
super(req);
// NOTE: This will be created at the same time as ServerRequest
this._receiveTime = performance.now();
this._redirected = false;
this._latencyMetricName = "latency";
this._serverTimingsMetrics = [];
this.proxied = false;
}
// Getter methods here
get redirected() {
return this._redirected;
}
// Setter methods here
set latencyMetricName(name) {
this._latencyMetricName = name;
}
// Public functions here
redirect(location, statusCode = 302, message = "") {
this._redirected = true;
let htmlMessage = message.length > 0
? message
: `Redirected to <a href="${location}">here</a>`;
// Write a little something something for good measure
this.body = `
<html>
<body>
<p>${htmlMessage}</p>
</body>
</html>`;
this.setHeader("Content-Type", "text/html; charset=utf-8");
this.setHeader("Location", location);
this.statusCode = statusCode;
}
setCookies = (cookies) => {
let setCookiesValue = [];
// Check for exiting cookies and add them to the setCookiesValue array
let existing = this.getHeader("Set-Cookie");
if (typeof existing === "string") {
setCookiesValue.push(existing);
}
else if (Array.isArray(existing)) {
setCookiesValue = existing;
}
// Loop through each cookie and build the cookie values
for (let cookie of cookies) {
// Set the cookie value first
let value = `${cookie.name}=${cookie.value}`;
// if there is a maxAge then set it - NOTE: put ";" first
if (cookie.maxAge !== undefined) {
value += `; Max-Age=${cookie.maxAge}`;
}
// If there is a path then set it or use default path of "/" - NOTE: put ";" first
if (cookie.path !== undefined) {
value += `; Path=${cookie.path}`;
}
else {
value += `; Path=/`;
}
// If httpOnly is indicated then add it - NOTE: put ";" first
if (cookie.httpOnly === true) {
value += "; HttpOnly";
}
// If secure is indicated set then add it - NOTE: put ";" first
if (cookie.secure === true) {
value += "; Secure";
}
// If sameSite has been provided then add it - NOTE: put ";" first
if (cookie.sameSite !== undefined) {
value += `; SameSite=${cookie.sameSite}`;
}
// If domain has been provided then add it - NOTE: put ";" first
if (cookie.domain !== undefined) {
value += `; Domain=${cookie.domain}`;
}
// Save the cookie
setCookiesValue.push(value);
}
// Finally set the cookie/s in the response header
this.setHeader("Set-Cookie", setCookiesValue);
};
clearCookies = (cookies) => {
let httpCookies = [];
for (let cookie of cookies) {
// To clear a cookie - set value to empty string and max age to -1
httpCookies.push({ name: cookie, value: "", maxAge: -1 });
}
this.setCookies(httpCookies);
};
setServerTimingHeader = () => {
let serverTimingHeaders = [];
// Check if the req has a Server-Timing header. This is not normal but I
// want to something like a forwardAuth server to be able to add it's
// metrics to the response header
if (this?.req?.headers["server-timing"] !== undefined) {
const reqTimings = this.req.headers["server-timing"];
// Check if there are multiple headers
if (Array.isArray(reqTimings)) {
// If so then since this is the first just use it as the headers array
serverTimingHeaders = reqTimings;
}
else {
// If not then just add it to the array
serverTimingHeaders.push(reqTimings);
}
}
let serverTimingValue = "";
// Add each additional metric added to the res next so they are in
// the order they were added
for (let metric of this._serverTimingsMetrics) {
// Check if we have a string or a metric object
if (typeof metric === "string") {
// The string version is already formatted so just add to the array
serverTimingHeaders.push(metric);
continue;
}
// If we are here then we have a metric object so add the name
serverTimingValue += metric.name;
// Check if there is an optional duration
if (metric.duration !== undefined) {
serverTimingValue += `;dur=${metric.duration}`;
}
// Check if there is an optional description
if (metric.description !== undefined) {
serverTimingValue += `;desc="${metric.description}"`;
}
serverTimingValue += ", ";
}
// Finally add the total latency for the endpoint to the array
const latency = Math.round(performance.now() - this._receiveTime);
serverTimingValue += `${this._latencyMetricName};dur=${latency}`;
serverTimingHeaders.push(serverTimingValue);
// Of course don't forget to set the header!!
this.setHeader("Server-Timing", serverTimingHeaders);
};
addServerTimingMetric = (name, duration, description) => {
// This adds a metric to the Server-Timing header for this response
this._serverTimingsMetrics.push({ name, duration, description });
};
addServerTimingHeader = (header) => {
// This adds a complete Server-Timing header to this response
this._serverTimingsMetrics.push(header);
};
}
const contentTypes = {
// "123": "application/vnd.lotus-1-2-3",
// "1km": "application/vnd.1000minds.decision-model+xml",
// "3dml": "text/vnd.in3d.3dml",
// "3ds": "image/x-3ds",
// "3g2": "video/3gpp2",
// "3gp": "video/3gpp",
// "3gpp": "video/3gpp",
// "3mf": "model/3mf",
"7z": "application/x-7z-compressed",
// "disposition-notification": "message/disposition-notification",
// "n-gage": "application/vnd.nokia.n-gage.symbian.install",
// "sfd-hdstx": "application/vnd.hydrostatix.sof-data",
// "vbox-extpack": "application/x-virtualbox-vbox-extpack",
// aab: "application/x-authorware-bin",
// aac: "audio/x-aac",
// aam: "application/x-authorware-map",
// aas: "application/x-authorware-seg",
// abw: "application/x-abiword",
// ac: "application/vnd.nokia.n-gage.ac+xml",
// acc: "application/vnd.americandynamics.acc",
// ace: "application/x-ace-compressed",
// acu: "application/vnd.acucobol",
// acutc: "application/vnd.acucorp",
// adp: "audio/adpcm",
// adts: "audio/aac",
// aep: "application/vnd.audiograph",
// afm: "application/x-font-type1",
// afp: "application/vnd.ibm.modcap",
// age: "application/vnd.age",
// ahead: "application/vnd.ahead.space",
// ai: "application/postscript",
// aif: "audio/x-aiff",
// aifc: "audio/x-aiff",
// aiff: "audio/x-aiff",
// air: "application/vnd.adobe.air-application-installer-package+zip",
// ait: "application/vnd.dvb.ait",
// ami: "application/vnd.amiga.ami",
// aml: "application/automationml-aml+xml",
// amlx: "application/automationml-amlx+zip",
// amr: "audio/amr",
// apk: "application/vnd.android.package-archive",
// apng: "image/apng",
// appcache: "text/cache-manifest",
// appinstaller: "application/appinstaller",
// application: "application/x-ms-application",
// appx: "application/appx",
// appxbundle: "application/appxbundle",
// apr: "application/vnd.lotus-approach",
// arc: "application/x-freearc",
// arj: "application/x-arj",
// asc: "application/pgp-signature",
// asf: "video/x-ms-asf",
// asm: "text/x-asm",
// aso: "application/vnd.accpac.simply.aso",
// asx: "video/x-ms-asf",
// atc: "application/vnd.acucorp",
// atom: "application/atom+xml",
// atomcat: "application/atomcat+xml",
// atomdeleted: "application/atomdeleted+xml",
// atomsvc: "application/atomsvc+xml",
// atx: "application/vnd.antix.game-component",
// au: "audio/basic",
// avci: "image/avci",
// avcs: "image/avcs",
// avi: "video/x-msvideo",
// avif: "image/avif",
// aw: "application/applixware",
// azf: "application/vnd.airzip.filesecure.azf",
// azs: "application/vnd.airzip.filesecure.azs",
// azv: "image/vnd.airzip.accelerator.azv",
// azw: "application/vnd.amazon.ebook",
// b16: "image/vnd.pco.b16",
// bat: "application/x-msdownload",
// bcpio: "application/x-bcpio",
// bdf: "application/x-font-bdf",
// bdm: "application/vnd.syncml.dm+wbxml",
// bdoc: "application/x-bdoc",
// bed: "application/vnd.realvnc.bed",
// bh2: "application/vnd.fujitsu.oasysprs",
// bin: "application/octet-stream",
// blb: "application/x-blorb",
// blorb: "application/x-blorb",
// bmi: "application/vnd.bmi",
// bmml: "application/vnd.balsamiq.bmml+xml",
bmp: "image/x-ms-bmp",
// book: "application/vnd.framemaker",
// box: "application/vnd.previewsystems.box",
// boz: "application/x-bzip2",
// bpk: "application/octet-stream",
// bsp: "model/vnd.valve.source.compiled-map",
// btf: "image/prs.btif",
// btif: "image/prs.btif",
// buffer: "application/octet-stream",
// bz2: "application/x-bzip2",
// bz: "application/x-bzip",
// c11amc: "application/vnd.cluetrust.cartomobile-config",
// c11amz: "application/vnd.cluetrust.cartomobile-config-pkg",
// c4d: "application/vnd.clonk.c4group",
// c4f: "application/vnd.clonk.c4group",
// c4g: "application/vnd.clonk.c4group",
// c4p: "application/vnd.clonk.c4group",
// c4u: "application/vnd.clonk.c4group",
// c: "text/x-c",
// cab: "application/vnd.ms-cab-compressed",
// caf: "audio/x-caf",
// cap: "application/vnd.tcpdump.pcap",
// car: "application/vnd.curl.car",
// cat: "application/vnd.ms-pki.seccat",
// cb7: "application/x-cbr",
// cba: "application/x-cbr",
// cbr: "application/x-cbr",
// cbt: "application/x-cbr",
// cbz: "application/x-cbr",
// cc: "text/x-c",
// cco: "application/x-cocoa",
// cct: "application/x-director",
// ccxml: "application/ccxml+xml",
// cdbcmsg: "application/vnd.contact.cmsg",
// cdf: "application/x-netcdf",
// cdfx: "application/cdfx+xml",
// cdkey: "application/vnd.mediastation.cdkey",
// cdmia: "application/cdmi-capability",
// cdmic: "application/cdmi-container",
// cdmid: "application/cdmi-domain",
// cdmio: "application/cdmi-object",
// cdmiq: "application/cdmi-queue",
// cdx: "chemical/x-cdx",
// cdxml: "application/vnd.chemdraw+xml",
// cdy: "application/vnd.cinderella",
// cer: "application/pkix-cert",
// cfs: "application/x-cfs-compressed",
// cgm: "image/cgm",
// chat: "application/x-chat",
// chm: "application/vnd.ms-htmlhelp",
// chrt: "application/vnd.kde.kchart",
// cif: "chemical/x-cif",
// cii: "application/vnd.anser-web-certificate-issue-initiation",
// cil: "application/vnd.ms-artgalry",
// cjs: "application/node",
// cla: "application/vnd.claymore",
// class: "application/java-vm",
// cld: "model/vnd.cld",
// clkk: "application/vnd.crick.clicker.keyboard",
// clkp: "application/vnd.crick.clicker.palette",
// clkt: "application/vnd.crick.clicker.template",
// clkw: "application/vnd.crick.clicker.wordbank",
// clkx: "application/vnd.crick.clicker",
// clp: "application/x-msclip",
// cmc: "application/vnd.cosmocaller",
// cmdf: "chemical/x-cmdf",
// cml: "chemical/x-cml",
// cmp: "application/vnd.yellowriver-custom-menu",
// cmx: "image/x-cmx",
// cod: "application/vnd.rim.cod",
// coffee: "text/coffeescript",
// com: "application/x-msdownload",
// conf: "text/plain",
// cpio: "application/x-cpio",
// cpl: "application/cpl+xml",
// cpp: "text/x-c",
// cpt: "application/mac-compactpro",
// crd: "application/x-mscardfile",
// crl: "application/pkix-crl",
// crt: "application/x-x509-ca-cert",
// crx: "application/x-chrome-extension",
// cryptonote: "application/vnd.rig.cryptonote",
// csh: "application/x-csh",
// csl: "application/vnd.citationstyles.style+xml",
// csml: "chemical/x-csml",
// csp: "application/vnd.commonspace",
css: "text/css",
// cst: "application/x-director",
csv: "text/csv",
// cu: "application/cu-seeme",
// curl: "text/vnd.curl",
// cwl: "application/cwl",
// cww: "application/prs.cww",
// cxt: "application/x-director",
// cxx: "text/x-c",
// dae: "model/vnd.collada+xml",
// daf: "application/vnd.mobius.daf",
// dart: "application/vnd.dart",
// dataless: "application/vnd.fdsn.seed",
// davmount: "application/davmount+xml",
// dbf: "application/vnd.dbf",
// dbk: "application/docbook+xml",
// dcr: "application/x-director",
// dcurl: "text/vnd.curl.dcurl",
// dd2: "application/vnd.oma.dd2+xml",
// ddd: "application/vnd.fujixerox.ddd",
// ddf: "application/vnd.syncml.dmddf+xml",
// dds: "image/vnd.ms-dds",
// deb: "application/x-debian-package",
// def: "text/plain",
// deploy: "application/octet-stream",
// der: "application/x-x509-ca-cert",
// dfac: "application/vnd.dreamfactory",
// dgc: "application/x-dgc-compressed",
// dib: "image/bmp",
// dic: "text/x-c",
// dir: "application/x-director",
// dis: "application/vnd.mobius.dis",
// dist: "application/octet-stream",
// distz: "application/octet-stream",
// djv: "image/vnd.djvu",
// djvu: "image/vnd.djvu",
// dll: "application/x-msdownload",
// dmg: "application/x-apple-diskimage",
// dmp: "application/vnd.tcpdump.pcap",
// dms: "application/octet-stream",
// dna: "application/vnd.dna",
doc: "application/msword",
docm: "application/vnd.ms-word.document.macroenabled.12",
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
dot: "application/msword",
dotm: "application/vnd.ms-word.template.macroenabled.12",
dotx: "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
// dp: "application/vnd.osgi.dp",
// dpg: "application/vnd.dpgraph",
// dpx: "image/dpx",
// dra: "audio/vnd.dra",
// drle: "image/dicom-rle",
// dsc: "text/prs.lines.tag",
// dssc: "application/dssc+der",
// dtb: "application/x-dtbook+xml",
// dtd: "application/xml-dtd",
// dts: "audio/vnd.dts",
// dtshd: "audio/vnd.dts.hd",
// dump: "application/octet-stream",
// dvb: "video/vnd.dvb.file",
// dvi: "application/x-dvi",
// dwd: "application/atsc-dwd+xml",
// dwf: "model/vnd.dwf",
// dwg: "image/vnd.dwg",
// dxf: "image/vnd.dxf",
// dxp: "application/vnd.spotfire.dxp",
// dxr: "application/x-director",
// ear: "application/java-archive",
// ecelp4800: "audio/vnd.nuera.ecelp4800",
// ecelp7470: "audio/vnd.nuera.ecelp7470",
// ecelp9600: "audio/vnd.nuera.ecelp9600",
// ecma: "application/ecmascript",
// edm: "application/vnd.novadigm.edm",
// edx: "application/vnd.novadigm.edx",
// efif: "application/vnd.picsel",
// ei6: "application/vnd.pg.osasli",
// elc: "application/octet-stream",
// emf: "image/emf",
// eml: "message/rfc822",
// emma: "application/emma+xml",
// emotionml: "application/emotionml+xml",
// emz: "application/x-msmetafile",
// eol: "audio/vnd.digital-winds",
eot: "application/vnd.ms-fontobject",
// eps: "application/postscript",
// epub: "application/epub+zip",
// es3: "application/vnd.eszigno3+xml",
// esa: "application/vnd.osgi.subsystem",
// esf: "application/vnd.epson.esf",
// et3: "application/vnd.eszigno3+xml",
// etx: "text/x-setext",
// eva: "application/x-eva",
// evy: "application/x-envoy",
// exe: "application/x-msdownload",
// exi: "application/exi",
// exp: "application/express",
// exr: "image/aces",
// ext: "application/vnd.novadigm.ext",
// ez2: "application/vnd.ezpix-album",
// ez3: "application/vnd.ezpix-package",
// ez: "application/andrew-inset",
// f4v: "video/x-f4v",
// f77: "text/x-fortran",
// f90: "text/x-fortran",
// f: "text/x-fortran",
// fbs: "image/vnd.fastbidsheet",
// fcdt: "application/vnd.adobe.formscentral.fcdt",
// fcs: "application/vnd.isac.fcs",
// fdf: "application/vnd.fdf",
// fdt: "application/fdt+xml",
// fe_launch: "application/vnd.denovo.fcselayout-link",
// fg5: "application/vnd.fujitsu.oasysgp",
// fgd: "application/x-director",
// fh4: "image/x-freehand",
// fh5: "image/x-freehand",
// fh7: "image/x-freehand",
// fh: "image/x-freehand",
// fhc: "image/x-freehand",
// fig: "application/x-xfig",
// fits: "image/fits",
// flac: "audio/x-flac",
// fli: "video/x-fli",
// flo: "application/vnd.micrografx.flo",
// flv: "video/x-flv",
// flw: "application/vnd.kde.kivio",
// flx: "text/vnd.fmi.flexstor",
// fly: "text/vnd.fly",
// fm: "application/vnd.framemaker",
// fnc: "application/vnd.frogans.fnc",
// fo: "application/vnd.software602.filler.form+xml",
// for: "text/x-fortran",
// fpx: "image/vnd.fpx",
// frame: "application/vnd.framemaker",
// fsc: "application/vnd.fsc.weblaunch",
// fst: "image/vnd.fst",
// ftc: "application/vnd.fluxtime.clip",
// fti: "application/vnd.anser-web-funds-transfer-initiation",
// fvt: "video/vnd.fvt",
// fxp: "application/vnd.adobe.fxp",
// fxpl: "application/vnd.adobe.fxp",
// fzs: "application/vnd.fuzzysheet",
// g2w: "application/vnd.geoplan",
// g3: "image/g3fax",
// g3w: "application/vnd.geospace",
// gac: "application/vnd.groove-account",
// gam: "application/x-tads",
// gbr: "application/rpki-ghostbusters",
// gca: "application/x-gca-compressed",
// gdl: "model/vnd.gdl",
// gdoc: "application/vnd.google-apps.document",
// ged: "text/vnd.familysearch.gedcom",
// geo: "application/vnd.dynageo",
// geojson: "application/geo+json",
// gex: "application/vnd.geometry-explorer",
// ggb: "application/vnd.geogebra.file",
// ggt: "application/vnd.geogebra.tool",
// ghf: "application/vnd.groove-help",
gif: "image/gif",
// gim: "application/vnd.groove-identity-message",
// glb: "model/gltf-binary",
// gltf: "model/gltf+json",
// gml: "application/gml+xml",
// gmx: "application/vnd.gmx",
// gnumeric: "application/x-gnumeric",
// gph: "application/vnd.flographit",
// gpx: "application/gpx+xml",
// gqf: "application/vnd.grafeq",
// gqs: "application/vnd.grafeq",
// gram: "application/srgs",
// gramps: "application/x-gramps-xml",
// gre: "application/vnd.geometry-explorer",
// grv: "application/vnd.groove-injector",
// grxml: "application/srgs+xml",
// gsf: "application/x-font-ghostscript",
gsheet: "application/vnd.google-apps.spreadsheet",
gslides: "application/vnd.google-apps.presentation",
// gtar: "application/x-gtar",
// gtm: "application/vnd.groove-tool-message",
// gtw: "model/vnd.gtw",
// gv: "text/vnd.graphviz",
// gxf: "application/gxf",
// gxt: "application/vnd.geonext",
gz: "application/gzip",
// h261: "video/h261",
// h263: "video/h263",
// h264: "video/h264",
// h: "text/x-c",
// hal: "application/vnd.hal+xml",
// hbci: "application/vnd.hbci",
// hbs: "text/x-handlebars-template",
// hdd: "application/x-virtualbox-hdd",
// hdf: "application/x-hdf",
// heic: "image/heic",
// heics: "image/heic-sequence",
// heif: "image/heif",
// heifs: "image/heif-sequence",
// hej2: "image/hej2k",
// held: "application/atsc-held+xml",
// hh: "text/x-c",
// hjson: "application/hjson",
// hlp: "application/winhlp",
// hpgl: "application/vnd.hp-hpgl",
// hpid: "application/vnd.hp-hpid",
// hps: "application/vnd.hp-hps",
// hqx: "application/mac-binhex40",
// hsj2: "image/hsj2",
// htc: "text/x-component",
// htke: "application/vnd.kenameaapp",
htm: "text/html",
html: "text/html",
// hvd: "application/vnd.yamaha.hv-dic",
// hvp: "application/vnd.yamaha.hv-voice",
// hvs: "application/vnd.yamaha.hv-script",
// i2g: "application/vnd.intergeo",
// icc: "application/vnd.iccprofile",
// ice: "x-conference/x-cooltalk",
// icm: "application/vnd.iccprofile",
ico: "image/x-icon",
// ics: "text/calendar",
// ief: "image/ief",
// ifb: "text/calendar",
// ifm: "application/vnd.shana.informed.formdata",
// iges: "model/iges",
// igl: "application/vnd.igloader",
// igm: "application/vnd.insors.igm",
// igs: "model/iges",
// igx: "application/vnd.micrografx.igx",
// iif: "application/vnd.shana.informed.interchange",
img: "application/octet-stream",
// imp: "application/vnd.accpac.simply.imp",
// ims: "application/vnd.ms-ims",
// in: "text/plain",
// ini: "text/plain",
// ink: "application/inkml+xml",
// inkml: "application/inkml+xml",
// install: "application/x-install-instructions",
// iota: "application/vnd.astraea-software.iota",
// ipfix: "application/ipfix",
// ipk: "application/vnd.shana.informed.package",
// irm: "application/vnd.ibm.rights-management",
// irp: "application/vnd.irepository.package+xml",
// iso: "application/x-iso9660-image",
// itp: "application/vnd.shana.informed.formtemplate",
// its: "application/its+xml",
// ivp: "application/vnd.immervision-ivp",
// ivu: "application/vnd.immervision-ivu",
// jad: "text/vnd.sun.j2me.app-descriptor",
// jade: "text/jade",
// jam: "application/vnd.jam",
// jar: "application/java-archive",
// jardiff: "application/x-java-archive-diff",
// java: "text/x-java-source",
// jhc: "image/jphc",
// jisp: "application/vnd.jisp",
// jls: "image/jls",
// jlt: "application/vnd.hp-jlyt",
// jng: "image/x-jng",
// jnlp: "application/x-java-jnlp-file",
// joda: "application/vnd.joost.joda-archive",
jp2: "image/jp2",
jpe: "image/jpeg",
jpeg: "image/jpeg",
// jpf: "image/jpx",
jpg2: "image/jp2",
jpg: "image/jpeg",
// jpgm: "video/jpm",
// jpgv: "video/jpeg",
// jph: "image/jph",
// jpm: "video/jpm",
jpx: "image/jpx",
js: "text/javascript",
json5: "application/json5",
json: "application/json",
// jsonld: "application/ld+json",
// jsonml: "application/jsonml+json",
// jsx: "text/jsx",
// jt: "model/jt",
// jxr: "image/jxr",
// jxra: "image/jxra",
// jxrs: "image/jxrs",
// jxs: "image/jxs",
// jxsc: "image/jxsc",
// jxsi: "image/jxsi",
// jxss: "image/jxss",
// kar: "audio/midi",
// karbon: "application/vnd.kde.karbon",
// kdbx: "application/x-keepass2",
// key: "application/x-iwork-keynote-sffkey",
// kfo: "application/vnd.kde.kformula",
// kia: "application/vnd.kidspiration",
// kml: "application/vnd.google-earth.kml+xml",
// kmz: "application/vnd.google-earth.kmz",
// kne: "application/vnd.kinar",
// knp: "application/vnd.kinar",
// kon: "application/vnd.kde.kontour",
// kpr: "application/vnd.kde.kpresenter",
// kpt: "application/vnd.kde.kpresenter",
// kpxx: "application/vnd.ds-keypoint",
// ksp: "application/vnd.kde.kspread",
// ktr: "application/vnd.kahootz",
// ktx2: "image/ktx2",
// ktx: "image/ktx",
// ktz: "application/vnd.kahootz",
// kwd: "application/vnd.kde.kword",
// kwt: "application/vnd.kde.kword",
// lasxml: "application/vnd.las.las+xml",
// latex: "application/x-latex",
// lbd: "application/vnd.llamagraphics.life-balance.desktop",
// lbe: "application/vnd.llamagraphics.life-balance.exchange+xml",
// les: "application/vnd.hhe.lesson-player",
// less: "text/less",
// lgr: "application/lgr+xml",
// lha: "application/x-lzh-compressed",
// link66: "application/vnd.route66.link66+xml",
// list3820: "application/vnd.ibm.modcap",
// list: "text/plain",
// listafp: "application/vnd.ibm.modcap",
// litcoffee: "text/coffeescript",
// lnk: "application/x-ms-shortcut",
log: "text/plain",
// lostxml: "application/lost+xml",
// lrf: "application/octet-stream",
// lrm: "application/vnd.ms-lrm",
// ltf: "application/vnd.frogans.ltf",
// lua: "text/x-lua",
// luac: "application/x-lua-bytecode",
// lvp: "audio/vnd.lucent.voice",
// lwp: "application/vnd.lotus-wordpro",
// lzh: "application/x-lzh-compressed",
// m13: "application/x-msmediaview",
// m14: "application/x-msmediaview",
// m1v: "video/mpeg",
// m21: "application/mp21",
// m2a: "audio/mpeg",
// m2v: "video/mpeg",
// m3a: "audio/mpeg",
// m3u8: "application/vnd.apple.mpegurl",
// m3u: "audio/x-mpegurl",
// m4a: "audio/x-m4a",
// m4p: "application/mp4",
// m4s: "video/iso.segment",
// m4u: "video/vnd.mpegurl",
// m4v: "video/x-m4v",
// ma: "application/mathematica",
// mads: "application/mads+xml",
// maei: "application/mmt-aei+xml",
// mag: "application/vnd.ecowin.chart",
// maker: "application/vnd.framemaker",
// man: "text/troff",
// manifest: "text/cache-manifest",
// map: "application/json",
// mar: "application/octet-stream",
// markdown: "text/markdown",
// mathml: "application/mathml+xml",
// mb: "application/mathematica",
// mbk: "application/vnd.mobius.mbk",
// mbox: "application/mbox",
// mc1: "application/vnd.medcalcdata",
// mcd: "application/vnd.mcd",
// mcurl: "text/vnd.curl.mcurl",
md: "text/markdown",
// mdb: "application/x-msaccess",
// mdi: "image/vnd.ms-modi",
// mdx: "text/mdx",
// me: "text/troff",
// mesh: "model/mesh",
// meta4: "application/metalink4+xml",
// metalink: "application/metalink+xml",
// mets: "application/mets+xml",
// mfm: "application/vnd.mfmp",
// mft: "application/rpki-manifest",
// mgp: "application/vnd.osgeo.mapguide.package",
// mgz: "application/vnd.proteus.magazine",
// mid: "audio/midi",
// midi: "audio/midi",
// mie: "application/x-mie",
// mif: "application/vnd.mif",
// mime: "message/rfc822",
// mj2: "video/mj2",
// mjp2: "video/mj2",
// mjs: "text/javascript",
// mk3d: "video/x-matroska",
// mka: "audio/x-matroska",
// mkd: "text/x-markdown",
// mks: "video/x-matroska",
// mkv: "video/x-matroska",
// mlp: "application/vnd.dolby.mlp",
// mmd: "application/vnd.chipnuts.karaoke-mmd",
// mmf: "application/vnd.smaf",
// mml: "text/mathml",
// mmr: "image/vnd.fujixerox.edmics-mmr",
// mng: "video/x-mng",
// mny: "application/x-msmoney",
// mobi: "application/x-mobipocket-ebook",
// mods: "application/mods+xml",
// mov: "video/quicktime",
// movie: "video/x-sgi-movie",
// mp21: "application/mp21",
// mp2: "audio/mpeg",
// mp2a: "audio/mpeg",
// mp3: "audio/mpeg",
// mp4: "video/mp4",
// mp4a: "audio/mp4",
// mp4s: "application/mp4",
// mp4v: "video/mp4",
// mpc: "application/vnd.mophun.certificate",
// mpd: "application/dash+xml",
// mpe: "video/mpeg",
// mpeg: "video/mpeg",
// mpf: "application/media-policy-dataset+xml",
// mpg4: "video/mp4",
// mpg: "video/mpeg",
// mpga: "audio/mpeg",
// mpkg: "application/vnd.apple.installer+xml",
// mpm: "application/vnd.blueice.multipass",
// mpn: "application/vnd.mophun.application",
// mpp: "application/vnd.ms-project",
// mpt: "application/vnd.ms-project",
// mpy: "application/vnd.ibm.minipay",
// mqy: "application/vnd.mobius.mqy",
// mrc: "application/marc",
// mrcx: "application/marcxml+xml",
// ms: "text/troff",
// mscml: "application/mediaservercontrol+xml",
// mseed: "application/vnd.fdsn.mseed",
// mseq: "application/vnd.mseq",
// msf: "application/vnd.epson.msf",
// msg: "application/vnd.ms-outlook",
// msh: "model/mesh",
// msi: "application/x-msdownload",
// msix: "application/msix",
// msixbundle: "application/msixbundle",
// msl: "application/vnd.mobius.msl",
// msm: "application/octet-stream",
// msp: "application/octet-stream",
// msty: "application/vnd.muvee.style",
// mtl: "model/mtl",
// mts: "model/vnd.mts",
// mus: "application/vnd.musician",
// musd: "application/mmt-usd+xml",
// musicxml: "application/vnd.recordare.musicxml+xml",
// mvb: "application/x-msmediaview",
// mvt: "application/vnd.mapbox-vector-tile",
// mwf: "application/vnd.mfer",
// mxf: "application/mxf",
// mxl: "application/vnd.recordare.musicxml",
// mxmf: "audio/mobile-xmf",
// mxml: "application/xv+xml",
// mxs: "application/vnd.triscape.mxs",
// mxu: "video/vnd.mpegurl",
// n3: "text/n3",
// nb: "application/mathematica",
// nbp: "application/vnd.wolfram.player",
// nc: "application/x-netcdf",
// ncx: "application/x-dtbncx+xml",
// nfo: "text/x-nfo",
// ngdat: "application/vnd.nokia.n-gage.data",
// nitf: "application/vnd.nitf",
// nlu: "application/vnd.neurolanguage.nlu",
// nml: "application/vnd.enliven",
// nnd: "application/vnd.noblenet-directory",
// nns: "application/vnd.noblenet-sealer",
// nnw: "application/vnd.noblenet-web",
// npx: "image/vnd.net-fpx",
// nq: "application/n-quads",
// nsc: "application/x-conference",
// nsf: "application/vnd.lotus-notes",
// nt: "application/n-triples",
// ntf: "application/vnd.nitf",
// numbers: "application/x-iwork-numbers-sffnumbers",
// nzb: "application/x-nzb",
// oa2: "application/vnd.fujitsu.oasys2",
// oa3: "application/vnd.fujitsu.oasys3",
// oas: "application/vnd.fujitsu.oasys",
// obd: "application/x-msbinder",
// obgx: "application/vnd.openblox.game+xml",
// obj: "model/obj",
// oda: "application/oda",
// odb: "application/vnd.oasis.opendocument.database",
// odc: "application/vnd.oasis.opendocument.chart",
// odf: "application/vnd.oasis.opendocument.formula",
// odft: "application/vnd.oasis.opendocument.formula-template",
// odg: "application/vnd.oasis.opendocument.graphics",
// odi: "application/vnd.oasis.opendocument.image",
// odm: "application/vnd.oasis.opendocument.text-master",
// odp: "application/vnd.oasis.opendocument.presentation",
// ods: "application/vnd.oasis.opendocument.spreadsheet",
// odt: "application/vnd.oasis.opendocument.text",
// oga: "audio/ogg",
// ogex: "model/vnd.opengex",
// ogg: "audio/ogg",
// ogv: "video/ogg",
// ogx: "application/ogg",
// omdoc: "application/omdoc+xml",
// onepkg: "application/onenote",
// onetmp: "application/onenote",
// onetoc2: "application/onenote",
// onetoc: "application/onenote",
// opf: "application/oebps-package+xml",
// opml: "text/x-opml",
// oprc: "application/vnd.palm",
// opus: "audio/ogg",
// org: "text/x-org",
// osf: "application/vnd.yamaha.openscoreformat",
// osfpvg: "application/vnd.yamaha.openscoreformat.osfpvg+xml",
// osm: "application/vnd.openstreetmap.data+xml",
// otc: "application/vnd.oasis.opendocument.chart-template",
otf: "font/otf",
// otg: "application/vnd.oasis.opendocument.graphics-template",
// oth: "application/vnd.oasis.opendocument.text-web",
// oti: "application/vnd.oasis.opendocument.image-template",
// otp: "application/vnd.oasis.opendocument.presentation-template",
// ots: "application/vnd.oasis.opendocument.spreadsheet-template",
// ott: "application/vnd.oasis.opendocument.text-template",
// ova: "application/x-virtualbox-ova",
// ovf: "application/x-virtualbox-ovf",
// owl: "application/rdf+xml",
// oxps: "application/oxps",
// oxt: "application/vnd.openofficeorg.extension",
// p10: "application/pkcs10",
// p12: "application/x-pkcs12",
// p7b: "application/x-pkcs7-certificates",
// p7c: "application/pkcs7-mime",
// p7m: "application/pkcs7-mime",
// p7r: "application/x-pkcs7-certreqresp",
// p7s: "application/pkcs7-signature",
// p8: "application/pkcs8",
// p: "text/x-pascal",
// pac: "application/x-ns-proxy-autoconfig",
// pages: "application/x-iwork-pages-sffpages",
// pas: "text/x-pascal",
// paw: "application/vnd.pawaafile",
// pbd: "application/vnd.powerbuilder6",
// pbm: "image/x-portable-bitmap",
// pcap: "application/vnd.tcpdump.pcap",
// pcf: "application/x-font-pcf",
// pcl: "application/vnd.hp-pcl",
// pclxl: "application/vnd.hp-pclxl",
// pct: "image/x-pict",
// pcurl: "application/vnd.curl.pcurl",
// pcx: "image/x-pcx",
// pdb: "application/x-pilot",
// pde: "text/x-processing",
pdf: "application/pdf",
// pem: "application/x-x509-ca-cert",
// pfa: "application/x-font-type1",
// pfb: "application/x-font-type1",
// pfm: "application/x-font-type1",
// pfr: "application/font-tdpfr",
// pfx: "application/x-pkcs12",
// pgm: "image/x-portable-graymap",
// pgn: "application/x-chess-pgn",
// pgp: "application/pgp-encrypted",
// php: "application/x-httpd-php",
// pic: "image/x-pict",
// pkg: "application/octet-stream",
// pki: "application/pkixcmp",
// pkipath: "application/pkix-pkipath",
// pkpass: "application/vnd.apple.pkpass",
// pl: "application/x-perl",
// plb: "application/vnd.3gpp.pic-bw-large",
// plc: "application/vnd.mobius.plc",
// plf: "application/vnd.pocketlearn",
// pls: "application/pls+xml",
// pm: "application/x-perl",
// pml: "application/vnd.ctc-posml",
png: "image/png",
// pnm: "image/x-portable-anymap",
// portpkg: "application/vnd.macports.portpkg",
pot: "application/vnd.ms-powerpoint",
potm: "application/vnd.ms-powerpoint.template.macroenabled.12",
potx: "application/vnd.openxmlformats-officedocument.presentationml.template",
ppam: "application/vnd.ms-powerpoint.addin.macroenabled.12",
// ppd: "application/vnd.cups-ppd",
// ppm: "image/x-portable-pixmap",
pps: "application/vnd.ms-powerpoint",
ppsm: "application/vnd.ms-powerpoint.slideshow.macroenabled.12",
ppsx: "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
ppt: "application/vnd.ms-powerpoint",
pptm: "application/vnd.ms-powerpoint.presentation.macroenabled.12",
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
// pqa: "application/vnd.palm",
// prc: "model/prc",
// pre: "application/vnd.lotus-freelance",
// prf: "application/pics-rules",
// provx: "application/provenance+xml",
// ps: "application/postscript",
// psb: "application/vnd.3gpp.pic-bw-small",
psd: "image/vnd.adobe.photoshop",
// psf: "application/x-font-linux-psf",
// pskcxml: "application/pskc+xml",
// pti: "image/prs.pti",
// ptid: "application/vnd.pvi.ptid1",
// pub: "application/x-mspublisher",
// pvb: "application/vnd.3gpp.pic-bw-var",
// pwn: "application/vnd.3m.post-it-notes",
// pya: "audio/vnd.ms-playready.media.pya",
// pyo: "model/vnd.pytha.pyox",
// pyox: "model/vnd.pytha.pyox",
// pyv: "video/vnd.ms-playready.media.pyv",
// qam: "application/vnd.epson.quickanime",
// qbo: "application/vnd.intu.qbo",
// qfx: "application/vnd.intu.qfx",
// qps: "application/vnd.publishare-delta-tree",
// qt: "video/quicktime",
// qwd: "application/vnd.quark.quarkxpress",
// qwt: "application/vnd.quark.quarkxpress",
// qxb: "application/vnd.quark.quarkxpress",
// qxd: "application/vnd.quark.quarkxpress",
// qxl: "application/vnd.quark.quarkxpress",
// qxt: "application/vnd.quark.quarkxpress",
// ra: "audio/x-realaudio",
// ram: "audio/x-pn-realaudio",
// raml: "application/raml+yaml",
// rapd: "application/route-apd+xml",
// rar: "application/x-rar-compressed",
// ras: "image/x-cmu-raster",
// rcprofile: "application/vnd.ipunplugged.rcprofile",
// rdf: "application/rdf+xml",
// rdz: "application/vnd.data-vision.rdz",
// relo: "application/p2p-overlay+xml",
// rep: "application/vnd.businessobjects",
// res: "application/x-dtbresource+xml",
// rgb: "image/x-rgb",
// rif: "application/reginfo+xml",
// rip: "audio/vnd.rip",
// ris: "application/x-research-info-systems",
// rl: "application/resource-lists+xml",
// rlc: "image/vnd.fujixerox.edmics-rlc",
// rld: "application/resource-lists-diff+xml",
// rm: "application/vnd.rn-realmedia",
// rmi: "audio/midi",
// rmp: "audio/x-pn-realaudio-plugin",
// rms: "application/vnd.jcp.javame.midlet-rms",
// rmvb: "application/vnd.rn-realmedia-vbr",
// rnc: "application/relax-ng-compact-syntax",
// rng: "application/xml",
// roa: "application/rpki-roa",
// roff: "text/troff",
// rp9: "application/vnd.cloanto.rp9",
// rpm: "application/x-redhat-package-manager",
// rpss: "application/vnd.nokia.radio-presets",
// rpst: "application/vnd.nokia.radio-preset",
// rq: "application/sparql-query",
// rs: "application/rls-services+xml",
// rsat: "application/atsc-rsat+xml",
// rsd: "application/rsd+xml",
// rsheet: "application/urc-ressheet+xml",
// rss: "application/rss+xml",
rtf: "text/rtf",
// rtx: "text/richtext",
// run: "application/x-makeself",
// rusd: "application/route-usd+xml",
// s3m: "audio/s3m",
// s: "text/x-asm",
// saf: "application/vnd.yamaha.smaf-audio",
// sass: "text/x-sass",
// sbml: "application/sbml+xml",
// sc: "application/vnd.ibm.secure-container",
// scd: "application/x-msschedule",
// scm: "application/vnd.lotus-screencam",
// scq: "application/scvp-cv-request",
// scs: "application/scvp-cv-response",
scss: "text/x-scss",
// scurl: "text/vnd.curl.scurl",
// sda: "application/vnd.stardivision.draw",
// sdc: "application/vnd.stardivision.calc",
// sdd: "application/vnd.stardivision.impress",
// sdkd: "application/vnd.solent.sdkm+xml",
// sdkm: "application/vnd.solent.sdkm+xml",
// sdp: "application/sdp",
// sdw: "application/vnd.stardivision.writer",
// sea: "application/x-sea",
// see: "application/vnd.seemail",
// seed: "application/vnd.fdsn.seed",
// sema: "application/vnd.sema",
// semd: "application/vnd.semd",
// semf: "application/vnd.semf",
// senmlx: "application/senml+xml",
// sensmlx: "application/sensml+xml",
// ser: "application/java-serialized-object",
// setpay: "application/set-payment-initiation",
// setreg: "application/set-registration-initiation",
// sfs: "application/vnd.spotfire.sfs",
// sfv: "text/x-sfv",
// sgi: "image/sgi",
// sgl: "application/vnd.stardivision.writer-global",
// sgm: "text/sgml",
// sgml: "text/sgml",
// sh: "application/x-sh",
// shar: "application/x-shar",
// shex: "text/shex",
// shf: "application/shf+xml",
// shtml: "text/html",
// sid: "image/x-mrsid-image",
// sieve: "application/sieve",
// sig: "application/pgp-signature",
// sil: "audio/silk",
// silo: "model/mesh",
// sis: "application/vnd.symbian.install",
// sisx: "application/vnd.symbian.install",
// sit: "application/x-stuffit",
// sitx: "application/x-stuffitx",
// siv: "application/sieve",
// skd: "application/vnd.koan",
// skm: "application/vnd.koan",
// skp: "application/vnd.koan",
// skt: "application/vnd.koan",
sldm: "application/vnd.ms-powerpoint.slide.macroenabled.12",
sldx: "application/vnd.openxmlformats-officedocument.presentationml.slide",
// slim: "text/slim",
// slm: "text/slim",
// sls: "application/route-s-tsid+xml",
// slt: "application/vnd.epson.salt",
// sm: "application/vnd.stepmania.stepchart",
// smf: "application/vnd.stardivision.math",
// smi: "application/smil+xml",
// smil: "application/smil+xml",
// smv: "video/x-smv",
// smzip: "application/vnd.stepmania.package",
// snd: "audio/basic",
// snf: "application/x-font-snf",
// so: "application/octet-stream",
// spc: "application/x-pkcs7-certificates",
// spdx: "text/spdx",
// spf: "application/vnd.yamaha.smaf-phrase",
// spl: "application/x-futuresplash",
// spot: "text/vnd.in3d.spot",
// spp: "application/scvp-vp-response",
// spq: "application/scvp-vp-request",
// spx: "audio/ogg",
// sql: "application/x-sql",
// src: "application/x-wais-source",
// srt: "application/x-subrip",
// sru: "application/sru+xml",
// srx: "application/sparql-results+xml",
// ssdl: "application/ssdl+xml",
// sse: "application/vnd.kodak-descriptor",
// ssf: "application/vnd.epson.ssf",
// ssml: "application/ssml+xml",
// st: "application/vnd.sailingtracker.track",
// stc: "application/vnd.sun.xml.calc.template",
// std: "application/vnd.sun.xml.draw.template",
// stf: "application/vnd.wt.stf",
// sti: "application/vnd.sun.xml.impress.template",
// stk: "application/hyperstudio",
// stl: "model/stl",
// stpx: "model/step+xml",
// stpxz: "model/step-xml+zip",
// stpz: "model/step+zip",
// str: "application/vnd.pg.format",
// stw: "application/vnd.sun.xml.writer.template",
// styl: "text/stylus",
// stylus: "text/stylus",
// sub: "text/vnd.dvb.subtitle",
// sus: "application/vnd.sus-calendar",
// susp: "application/vnd.sus-calendar",
// sv4cpio: "application/x-sv4cpio",
// sv4crc: "application/x-sv4crc",
// svc: "application/vnd.dvb.service",
// svd: "application/vnd.svd",
svg: "image/svg+xml",
svgz: "image/svg+xml",
// swa: "application/x-director",
// swf: "application/x-shockwave-flash",
// swi: "application/vnd.aristanetworks.swi",
// swidtag: "application/swid+xml",
// sxc: "application/vnd.sun.xml.calc",
// sxd: "application/vnd.sun.xml.draw",
// sxg: "application/vnd.sun.xml.writer.global",
// sxi: "application/vnd.sun.xml.impress",
// sxm: "application/vnd.sun.xml.math",
// sxw: "application/vnd.sun.xml.writer",
// t38: "image/t38",
// t3: "application/x-t3vm-image",
// t: "text/troff",
// taglet: "application/vnd.mynfc",
// tao: "application/vnd.tao.intent-module-archive",
// tap: "image/vnd.tencent.tap",
tar: "application/x-tar",
// tcap: "application/vnd.3gpp2.tcap",
// tcl: "application/x-tcl",
// td: "application/urc-targetdesc+xml",
// teacher: "application/vnd.smart.teacher",
// tei: "application/tei+xml",
// teicorpus: "application/tei+xml",
// tex: "application/x-tex",
// texi: "application/x-texinfo",
// texinfo: "application/x-texinfo",
text: "text/plain",
// tfi: "application/thraud+xml",
// tfm: "application/x-tex-tfm",
// tfx: "image/tiff-fx",
// tga: "image/x-tga",
// thmx: "application/vnd.ms-officetheme",
tif: "image/tiff",
tiff: "image/tiff",
// tk: "application/x-tcl",
// tmo: "application/vnd.tmobile-livetv",
toml: "application/toml",
// torrent: "application/x-bittorrent",
// tpl: "application/vnd.groove-tool-template",
// tpt: "application/vnd.trid.tpt",
// tr: "text/troff",
// tra: "application/vnd.trueapp",
// trig: "application/trig",
// trm: "application/x-msterminal",
// ts: "video/mp2t",
// tsd: "application/timestamped-data",
// tsv: "text/tab-separated-values",
// ttc: "font/collection",
ttf: "font/ttf",
// ttl: "text/turtle",
// ttml: "application/ttml+xml",
// twd: "application/vnd.simtech-mindmapper",
// twds: "application/vnd.simtech-mindmapper",
// txd: "application/vnd.genomatix.tuxedo",
// txf: "application/vnd.mobius.txf",
txt: "text/plain",
// u32: "application/x-authorware-bin",
// u3d: "model/u3d",
// u8dsn: "message/global-delivery-status",
// u8hdr: "message/global-headers",
// u8mdn: "message/global-disposition-notification",
// u8msg: "message/global",
// ubj: "application/ubjson",
// udeb: "application/x-debian-package",
// ufd: "application/vnd.ufdl",
// ufdl: "application/vnd.ufdl",
// ulx: "application/x-glulx",
// umj: "application/vnd.umajin",
// unityweb: "application/vnd.unity",
// uo: "application/vnd.uoml+xml",
// uoml: "application/vnd.uoml+xml",
// uri: "text/uri-list",
// uris: "text/uri-list",
// urls: "text/uri-list",
// usda: "model/vnd.usda",
// usdz: "model/vnd.usdz+zip",
// ustar: "application/x-ustar",
// utz: "application/vnd.uiq.theme",
// uu: "text/x-uuencode",
// uva: "audio/vnd.dece.audio",
// uvd: "application/vnd.dece.data",
// uvf: "application/vnd.dece.data",
// uvg: "image/vnd.dece.graphic",
// uvh: "video/vnd.dece.hd",
// uvi: "image/vnd.dece.graphic",
// uvm: "video/vnd.dece.mobile",
// uvp: "video/vnd.dece.pd",
// uvs: "video/vnd.dece.sd",
// uvt: "application/vnd.dece.ttml+xml",
// uvu: "video/vnd.uvvu.mp4",
// uvv: "video/vnd.dece.video",
// uvva: "audio/vnd.dece.audio",
// uvvd: "application/vnd.dece.data",
// uvvf: "application/vnd.dece.data",
// uvvg: "image/vnd.dece.graphic",
// uvvh: "video/vnd.dece.hd",
// uvvi: "image/vnd.dece.graphic",
// uvvm: "video/vnd.dece.mobile",
// uvvp: "video/vnd.dece.pd",
// uvvs: "video/vnd.dece.sd",
// uvvt: "application/vnd.dece.ttml+xml",
// uvvu: "video/vnd.uvvu.mp4",
// uvvv: "video/vnd.dece.video",
// uvvx: "application/vnd.dece.unspecified",
// uvvz: "application/vnd.dece.zip",
// uvx: "application/vnd.dece.unspecified",
// uvz: "application/vnd.dece.zip",
// vbox: "application/x-virtualbox-vbox",
// vcard: "text/vcard",
// vcd: "application/x-cdlink",
// vcf: "text/x-vcard",
// vcg: "application/vnd.groove-vcard",
// vcs: "text/x-vcalendar",
// vcx: "application/vnd.vcx",
// vdi: "application/x-virtualbox-vdi",
// vds: "model/vnd.sap.vds",
// vhd: "application/x-virtualbox-vhd",
// vis: "application/vnd.visionary",
// viv: "video/vnd.vivo",
// vmdk: "application/x-virtualbox-vmdk",
// vob: "video/x-ms-vob",
// vor: "application/vnd.stardivision.writer",
// vox: "application/x-authorware-bin",
// vrml: "model/vrml",
// vsd: "application/vnd.visio",
// vsf: "application/vnd.vsf",
// vss: "application/vnd.visio",
// vst: "application/vnd.visio",
// vsw: "application/vnd.visio",
// vtf: "image/vnd.valve.source.texture",
// vtt: "text/vtt",
// vtu: "model/vnd.vtu",
// vxml: "application/voicexml+xml",
// w3d: "application/x-director",
// wad: "application/x-doom",
// wadl: "application/vnd.sun.wadl+xml",
// war: "application/java-archive",
wasm: "application/wasm",
// wav: "audio/x-wav",
// wax: "audio/x-ms-wax",
// wbmp: "image/vnd.wap.wbmp",
// wbs: "application/vnd.criticaltools.wbs+xml",
// wbxml: "application/vnd.wap.wbxml",
// wcm: "application/vnd.ms-works",
// wdb: "application/vnd.ms-works",
// wdp: "image/vnd.ms-photo",
// weba: "audio/webm",
// webapp: "application/x-web-app-manifest+json",
// webm: "video/webm",
// webmanifest: "application/manifest+json",
webp: "image/webp",
// wg: "application/vnd.pmi.widget",
// wgsl: "text/wgsl",
// wgt: "application/widget",
// wif: "application/watcherinfo+xml",
// wks: "application/vnd.ms-works",
// wm: "video/x-ms-wm",
// wma: "audio/x-ms-wma",
// wmd: "application/x-ms-wmd",
// wmf: "image/wmf",
// wml: "text/vnd.wap.wml",
// wmlc: "application/vnd.wap.wmlc",
// wmls: "text/vnd.wap.wmlscript",
// wmlsc: "application/vnd.wap.wmlscriptc",
// wmv: "video/x-ms-wmv",
// wmx: "video/x-ms-wmx",
// wmz: "application/x-msmetafile",
woff2: "font/woff2",
woff: "font/woff",
// wpd: "application/vnd.wordperfect",
// wpl: "application/vnd.ms-wpl",
// wps: "application/vnd.ms-works",
// wqd: "application/vnd.wqd",
// wri: "application/x-mswrite",
// wrl: "model/vrml",
// wsc: "message/vnd.wfa.wsc",
// wsdl: "application/wsdl+xml",
// wspolicy: "application/wspolicy+xml",
// wtb: "application/vnd.webturbo",
// wvx: "video/x-ms-wvx",
// x32: "application/x-authorware-bin",
// x3d: "model/x3d+xml",
// x3db: "model/x3d+fastinfoset",
// x3dbz: "model/x3d+binary",
// x3dv: "model/x3d-vrml",
// x3dvz: "model/x3d+vrml",
// x3dz: "model/x3d+xml",
// x_b: "model/vnd.parasolid.transmit.binary",
// x_t: "model/vnd.parasolid.transmit.text",
// xaml: "application/xaml+xml",
// xap: "application/x-silverlight-app",
// xar: "application/vnd.xara",
// xav: "application/xcap-att+xml",
// xbap: "application/x-ms-xbap",
// xbd: "application/vnd.fujixerox.docuworks.binder",
// xbm: "image/x-xbitmap",
// xca: "application/xcap-caps+xml",
// xcs: "application/calendar+xml",
// xdf: "application/xcap-diff+xml",
// xdm: "application/vnd.syncml.dm+xml",
// xdp: "application/vnd.adobe.xdp+xml",
// xdssc: "application/dssc+xml",
// xdw: "application/vnd.fujixerox.docuworks",
// xel: "application/xcap-el+xml",
// xenc: "application/xenc+xml",
// xer: "application/patch-ops-error+xml",
// xfdf: "application/xfdf",
// xfdl: "application/vnd.xfdl",
// xht: "application/xhtml+xml",
xhtm: "application/vnd.pwg-xhtml-print+xml",
xhtml: "application/xhtml+xml",
// xhvml: "application/xv+xml",
// xif: "image/vnd.xiff",
xla: "application/vnd.ms-excel",
xlam: "application/vnd.ms-excel.addin.macroenabled.12",
xlc: "application/vnd.ms-excel",
// xlf: "application/xliff+xml",
xlm: "application/vnd.ms-excel",
xls: "application/vnd.ms-excel",
xlsb: "application/vnd.ms-excel.sheet.binary.macroenabled.12",
xlsm: "application/vnd.ms-excel.sheet.macroenabled.12",
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
xlt: "application/vnd.ms-excel",
xltm: "application/vnd.ms-excel.template.macroenabled.12",
xltx: "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
xlw: "application/vnd.ms-excel",
// xm: "audio/xm",
xml: "text/xml",
// xns: "application/xcap-ns+xml",
// xo: "application/vnd.olpc-sugar",
// xop: "application/xop+xml",
// xpi: "application/x-xpinstall",
// xpl: "application/xproc+xml",
// xpm: "image/x-xpixmap",
// xpr: "application/vnd.is-xpr",
// xps: "application/vnd.ms-xpsdocument",
// xpw: "application/vnd.intercon.formnet",
// xpx: "application/vnd.intercon.formnet",
// xsd: "application/xml",
// xsf: "application/prs.xsf+xml",
// xsl: "application/xslt+xml",
// xslt: "application/xslt+xml",
// xsm: "application/vnd.syncml+xml",
// xspf: "application/xspf+xml",
// xul: "application/vnd.mozilla.xul+xml",
// xvm: "application/xv+xml",
// xvml: "application/xv+xml",
// xwd: "image/x-xwindowdump",
// xyz: "chemical/x-xyz",
// xz: "application/x-xz",
yaml: "text/yaml",
// yang: "application/yang",
// yin: "application/yin+xml",
yml: "text/yaml",
// ymp: "text/x-suse-ymp",
// z1: "application/x-zmachine",
// z2: "application/x-zmachine",
// z3: "application/x-zmachine",
// z4: "application/x-zmachine",
// z5: "application/x-zmachine",
// z6: "application/x-zmachine",
// z7: "application/x-zmachine",
// z8: "application/x-zmachine",
// zaz: "application/vnd.zzazz.deck+xml",
zip: "application/zip",
// zir: "application/vnd.zul",
// zirz: "application/vnd.zul",
// zmm: "application/vnd.handheld-entertainment+xml",
};
// SseServer class here
class SseServer {
_res;
_lastEventId;
_pingSeqNum;
constructor(req, res, opts) {
let retryInterval = opts.retryInterval ?? 0;
let pingInterval = opts.pingInterval ?? 0;
let pingEventName = opts.pingEventName ?? "ping";
this._res = res;
this._lastEventId = req.headers["last-event-id"];
this._pingSeqNum = 0;
// Set up the basics first
req.socket.setKeepAlive(true);
req.socket.setNoDelay(true);
req.socket.setTimeout(0);
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Connection", "keep-alive");
res.setHeader("Cache-Control", "no-cache");
res.statusCode = 200;
// Check if we should set a new delay interval
if (retryInterval > 0) {
this.setRetry(retryInterval);
}
// Check if we should setup a heartbeat ping
if (pingInterval > 0) {
// Setup a timer to send the heartbeat
let interval = setInterval(() => {
this.sendData(this._pingSeqNum, { event: pingEventName });
// Don't forget to increment the ping seq num
this._pingSeqNum += 1;
}, pingInterval * 1000);
// Make sure to stop the timer if the connection closes
res.addListener("close", () => {
clearInterval(interval);
});
}
}
get lastEventId() {
return this._lastEventId;
}
setRetry(delay) {
this._res.write(`retry: ${delay}\n\n`);
}
sendData(data, options) {
if (options?.event !== undefined) {
this._res.write(`event: ${options.event}\n`);
}
if (options?.id !== undefined) {
this._res.write(`id: ${options.id}\n`);
}
// Rem an array is an object!
if (typeof data === "object") {
this._res.write(`data: ${JSON.stringify(data)}\n\n`);
}
else {
this._res.write(`data: ${data}\n\n`);
}
}
close() {
this._res.end();
}
}
// Imports here
// Middleware functions here
const jsonMiddleware = () => {
return async (req, _, next) => {
// Before we do anything make sure there is a body!
let body;
if (Buffer.isBuffer(req.body)) {
body = req.body;
}
if (body === undefined || body.length === 0) {
// No body to parse so call next middleware and then return
await next();
return;
}
let jsonBody;
let parseOk = true;
let errMessage = "";
// Now check the content-type header to find out what sort of data we have
const contentTypeHeader = req.headers["content-type"];
if (contentTypeHeader !== undefined) {
let contentType = contentTypeHeader.split(";")[0];
switch (contentType) {
case "application/json":
try {
jsonBody = JSON.parse(body.toString());
}
catch (_) {
// Set the error message you want to return
errMessage = "Can not parse JSON body!";
parseOk = false;
}
break;
case "application/x-www-form-urlencoded":
let qry = new URLSearchParams(body.toString());
jsonBody = {};
for (let [key, value] of qry.entries()) {
jsonBody[key] = value;
}
break;
}
}
// If the parsing failed then return an error
if (!parseOk) {
throw new HttpError(400, errMessage);
}
req.json = jsonBody;
await next();
};
};
const bodyMiddleware = (options = {}) => {
let opts = {
maxBodySize: options.maxBodySize ?? 1024 * 1024,
};
return async (
// NOTE: No async here please since this is returning a Promise
req, _, next) => {
// Cehck if body has already been set
if (req.body !== undefined) {
// If so just continue down the middleware stack
await next();
return;
}
// Store each data "chunk" we receive this array
let chunks = [];
let bodySize = 0;
// Iterate of the req's AsyncIterator
for await (let chunk of req) {
bodySize += chunk.byteLength;
// Check if the body is larger then the user is allowing
if (bodySize >= opts.maxBodySize) {
let msg = `Body length greater than ${opts.maxBodySize} bytes`;
throw new HttpError(400, msg);
}
chunks.push(chunk);
}
req.body = Buffer.concat(chunks);
await next();
};
};
const corsMiddleware = (options = {}) => {
let opts = {
originsAllowed: options.originsAllowed ?? "*",
methodsAllowed: options.methodsAllowed ?? [],
headersAllowed: options.headersAllowed ?? [],
headersExposed: options.headersExposed ?? [],
credentialsAllowed: options.credentialsAllowed ?? false,
maxAge: options.maxAge ?? 60 * 60, // 1 hour
};
// NOTE: If credentialsAllowed is enabled then other headers cant be a "*"
if (opts.credentialsAllowed) {
if (opts.originsAllowed === "*") {
throw new Error("The originsAllowed MUST be specified when credentialsAllowed is true");
}
if (opts.methodsAllowed === "*") {
throw new Error("The methodsAllowed MUST be specified when credentialsAllowed is true");
}
if (opts.headersAllowed === "*") {
throw new Error("The headersAllowed MUST be specified when credentialsAllowed is true");
}
if (opts.headersExposed === "*") {
throw new Error("The headersExposed MUST be specified when credentialsAllowed is true");
}
}
return async (req, res, next) => {
let origin = req.headers["origin"];
// Check if this is a CORS preflight request
if (req.method === "OPTIONS") {
// The origin MUST be available or this is not valid
if (origin === undefined) {
throw new HttpError(400, "No origin header sent with the CORS request");
}
// Set Access-Control-Allow-Origin
if (opts.originsAllowed === "*" || opts.originsAllowed.includes(origin)) {
// Best to set this to the origin for this req and NOT allowed origins
res.setHeader("Access-Control-Allow-Origin", origin);
}
else {
throw new HttpError(400, `The origin ${origin} is not allowed`);
}
// Set Access-Control-Allow-Methods
// We know this header exists otherwise we couldn't have gotten here
let reqMethod = req.headers["access-control-request-method"];
if (opts.methodsAllowed === "*") {
res.setHeader("Access-Control-Allow-Methods", "*");
}
else if (opts.methodsAllowed.length === 0) {
// No methods being specified implies you should use the reqMethod
res.setHeader("Access-Control-Allow-Methods", reqMethod);
}
else if (opts.methodsAllowed.includes(reqMethod)) {
res.setHeader("Access-Control-Allow-Methods", opts.methodsAllowed.join(","));
}
else {
throw new HttpError(400, `The access-control-request-method ${reqMethod} is not allowed`);
}
// Set Access-Control-Allow-Headers
if (req.headers["access-control-request-headers"] !== undefined) {
if (opts.headersAllowed === "*") {
res.setHeader("Access-Control-Allow-Headers", "*");
}
else if (opts.headersAllowed.length) {
// Let the browser handle this one
res.setHeader("Access-Control-Allow-Headers", opts.headersAllowed.join(","));
}
}
// Set Access-Control-Expose-Headers
if (opts.headersExposed === "*") {
res.setHeader("Access-Control-Expose-Headers", "*");
}
else if (opts.headersExposed.length) {
res.setHeader("Access-Control-Expose-Headers", opts.headersExposed.join(","));
}
// Access-Control-Max-Age
res.setHeader("Access-Control-Max-Age", opts.maxAge);
// Access-Control-Allow-Credentials
if (opts.credentialsAllowed) {
res.setHeader("Access-Control-Allow-Credentials", "true");
}
// Finish up here and do not continue down the middleware stack
res.statusCode = 204;
res.end();
return;
}
// If we are here this was not a preflight request
// The origin needs to be available or we shouldn't set the CORS headers
if (origin !== undefined) {
if (opts.credentialsAllowed === true) {
res.setHeader("Access-Control-Allow-Credentials", "true");
}
if (opts.originsAllowed === "*" || opts.originsAllowed.includes(origin)) {
// Best to set this to the origin for this req and NOT allowed origins
res.setHeader("Access-Control-Allow-Origin", origin);
}
}
// If we are here then continue down the middleware stack
await next();
};
};
const expressWrapper = (middleware) => {
// Because we need to pass in the express middleware we will return the
// middleware, i.e. you need to call this function
return async (req, res, next) => {
middleware(req, res, (e) => {
if (e !== undefined) {
throw e;
}
});
await next();
};
};
const csrfChecksMiddleware = (options = {}) => {
let opts = {
methods: options.methods ?? ["POST", "PUT", "PATCH", "DELETE"],
checkType: options.checkType ?? "custom-req-header",
header: options.header ?? "x-csrf-header",
cookie: options.cookie ?? "x-csrf-cookie",
secret: options.secret ?? "",
hashAlgo: options.hashAlgo ?? "sha256",
signatureSeparator: options.signatureSeparator ?? ".",
};
// Need to make sure the header we check for is always lower case
opts.header = opts.header.toLowerCase();
// If this is "naive-double-submit-cookie" check the cookie is supplied
if (opts.checkType === "signed-double-submit-cookie") {
if (opts.secret.length === 0) {
throw new Error("Must set secret to use the 'signed-double-submit-cookie' CSRF check middleware");
}
}
let custReqHeader = (req) => {
// The custom-req-header check just ensures that the specified
// header exists - the value is not important
if (req.headers[opts.header] === undefined) {
return false;
}
return true;
};
let naiveDoubleSubmitCookie = (req) => {
// The naive-double-submit-cookie check ensures the value of the
// specified cookie matches the value of the specified header
let cookie = req.getCookie(opts.cookie);
// Note if cookie doesn't exist value is null and if headers doesn't exist
// it is undefined
if (req.headers[opts.header] !== cookie) {
return false;
}
return true;
};
let signedDoubleSubmitCookie = (req) => {
// The signed-double-submit-cookie check ensures the value of the
// specified cookie matches the value of the specified header
let cookie = req.getCookie(opts.cookie);
// Note if cookie doesn't exist value is null and if headers doesn't exist
// it is undefined
if (req.headers[opts.header] !== cookie) {
return false;
}
let [token, hash] = cookie.split(opts.signatureSeparator);
if (hash !==
crypto.createHmac(opts.hashAlgo, opts.secret).update(token).digest("hex")) {
return false;
}
return true;
};
// Because we need to pass in the options we will return the
// middleware, i.e. you need to call this function
return async (req, res, next) => {
// Make sure the method is one of the ones we want to check
if (opts.methods.includes(req.method)) {
let passed = false;
if (opts.checkType === "custom-req-header") {
passed = custReqHeader(req);
}
else if (opts.checkType === "naive-double-submit-cookie") {
passed = naiveDoubleSubmitCookie(req);
}
else {
passed = signedDoubleSubmitCookie(req);
}
// If the CSRF check failed then DO NOT continue down the stack
if (passed === false) {
res.statusCode = 401;
res.write("The request failed the CSRF check");
return;
}
}
await next();
};
};
const getSecurityHeaders = (options = {}) => {
let opts = {
headers: options.headers ?? [],
useDefaultHeaders: options.useDefaultHeaders ?? true,
};
// These are the default headers to use
let defaultHeaders = [
{ name: "X-Frame-Options", value: "SAMEORIGIN" },
{ name: "X-XSS-Protection", value: "0" },
{ name: "X-Content-Type-Options", value: "nosniff" },
{ name: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
{
name: "Strict-Transport-Security",
value: "max-age=63072000; includeSubDomains; preload",
},
{ name: "X-DNS-Prefetch-Control", value: "off" },
{
name: "Content-Security-Policy",
value: "default-src 'self';base-uri 'self';font-src 'self' https: data:;form-action 'self';frame-ancestors 'self';img-src 'self' data:;object-src 'none';script-src 'self';script-src-attr 'none';style-src 'self' https: 'unsafe-inline';upgrade-insecure-requests",
},
];
// These are the headers we will use
let securityHeaders = [];
// Set all of the user supplied headers first
for (let header of opts.headers) {
securityHeaders.push({ name: header.name, value: header.value });
}
// Check if we should use the default headers
if (opts.useDefaultHeaders) {
// Looks like it - so add the default headers
for (let header of defaultHeaders) {
// Check if the user has already supplied the header (use lower case
// to be safe)
let found = opts.headers.find((el) => el.name.toLowerCase() === header.name.toLowerCase());
if (found !== undefined) {
continue;
}
securityHeaders.push({ name: header.name, value: header.value });
}
}
return securityHeaders;
};
const securityHeadersMiddleware = (options = {}) => {
// Get the security headers
let securityHeaders = getSecurityHeaders(options);
// Because we need to pass in the options we will return the
// middleware, i.e. you need to call this function
return async (_, res, next) => {
// Set all of the sec headers
for (let header of securityHeaders) {
res.setHeader(header.name, header.value);
}
await next();
};
};
const dontCompressResponse = () => {
return async (req, _, next) => {
// Flag the response should not be compressed
req.dontCompressResponse = true;
await next();
};
};
const setLatencyMetricName = (name) => {
return async (_, res, next) => {
// Set the latency metric name
res.latencyMetricName = name;
await next();
};
};
/**
* Tokenize input string.
*/
function lexer(str) {
var tokens = [];
var i = 0;
while (i < str.length) {
var char = str[i];
if (char === "*" || char === "+" || char === "?") {
tokens.push({ type: "MODIFIER", index: i, value: str[i++] });
continue;
}
if (char === "\\") {
tokens.push({ type: "ESCAPED_CHAR", index: i++, value: str[i++] });
continue;
}
if (char === "{") {
tokens.push({ type: "OPEN", index: i, value: str[i++] });
continue;
}
if (char === "}") {
tokens.push({ type: "CLOSE", index: i, value: str[i++] });
continue;
}
if (char === ":") {
var name = "";
var j = i + 1;
while (j < str.length) {
var code = str.charCodeAt(j);
if (
// `0-9`
(code >= 48 && code <= 57) ||
// `A-Z`
(code >= 65 && code <= 90) ||
// `a-z`
(code >= 97 && code <= 122) ||
// `_`
code === 95) {
name += str[j++];
continue;
}
break;
}
if (!name)
throw new TypeError("Missing parameter name at ".concat(i));
tokens.push({ type: "NAME", index: i, value: name });
i = j;
continue;
}
if (char === "(") {
var count = 1;
var pattern = "";
var j = i + 1;
if (str[j] === "?") {
throw new TypeError("Pattern cannot start with \"?\" at ".concat(j));
}
while (j < str.length) {
if (str[j] === "\\") {
pattern += str[j++] + str[j++];
continue;
}
if (str[j] === ")") {
count--;
if (count === 0) {
j++;
break;
}
}
else if (str[j] === "(") {
count++;
if (str[j + 1] !== "?") {
throw new TypeError("Capturing groups are not allowed at ".concat(j));
}
}
pattern += str[j++];
}
if (count)
throw new TypeError("Unbalanced pattern at ".concat(i));
if (!pattern)
throw new TypeError("Missing pattern at ".concat(i));
tokens.push({ type: "PATTERN", index: i, value: pattern });
i = j;
continue;
}
tokens.push({ type: "CHAR", index: i, value: str[i++] });
}
tokens.push({ type: "END", index: i, value: "" });
return tokens;
}
/**
* Parse a string for the raw tokens.
*/
function parse(str, options) {
if (options === undefined) { options = {}; }
var tokens = lexer(str);
var _a = options.prefixes, prefixes = _a === undefined ? "./" : _a, _b = options.delimiter, delimiter = _b === undefined ? "/#?" : _b;
var result = [];
var key = 0;
var i = 0;
var path = "";
var tryConsume = function (type) {
if (i < tokens.length && tokens[i].type === type)
return tokens[i++].value;
};
var mustConsume = function (type) {
var value = tryConsume(type);
if (value !== undefined)
return value;
var _a = tokens[i], nextType = _a.type, index = _a.index;
throw new TypeError("Unexpected ".concat(nextType, " at ").concat(index, ", expected ").concat(type));
};
var consumeText = function () {
var result = "";
var value;
while ((value = tryConsume("CHAR") || tryConsume("ESCAPED_CHAR"))) {
result += value;
}
return result;
};
var isSafe = function (value) {
for (var _i = 0, delimiter_1 = delimiter; _i < delimiter_1.length; _i++) {
var char = delimiter_1[_i];
if (value.indexOf(char) > -1)
return true;
}
return false;
};
var safePattern = function (prefix) {
var prev = result[result.length - 1];
var prevText = prefix || (prev && typeof prev === "string" ? prev : "");
if (prev && !prevText) {
throw new TypeError("Must have text between two parameters, missing text after \"".concat(prev.name, "\""));
}
if (!prevText || isSafe(prevText))
return "[^".concat(escapeString(delimiter), "]+?");
return "(?:(?!".concat(escapeString(prevText), ")[^").concat(escapeString(delimiter), "])+?");
};
while (i < tokens.length) {
var char = tryConsume("CHAR");
var name = tryConsume("NAME");
var pattern = tryConsume("PATTERN");
if (name || pattern) {
var prefix = char || "";
if (prefixes.indexOf(prefix) === -1) {
path += prefix;
prefix = "";
}
if (path) {
result.push(path);
path = "";
}
result.push({
name: name || key++,
prefix: prefix,
suffix: "",
pattern: pattern || safePattern(prefix),
modifier: tryConsume("MODIFIER") || "",
});
continue;
}
var value = char || tryConsume("ESCAPED_CHAR");
if (value) {
path += value;
continue;
}
if (path) {
result.push(path);
path = "";
}
var open = tryConsume("OPEN");
if (open) {
var prefix = consumeText();
var name_1 = tryConsume("NAME") || "";
var pattern_1 = tryConsume("PATTERN") || "";
var suffix = consumeText();
mustConsume("CLOSE");
result.push({
name: name_1 || (pattern_1 ? key++ : ""),
pattern: name_1 && !pattern_1 ? safePattern(prefix) : pattern_1,
prefix: prefix,
suffix: suffix,
modifier: tryConsume("MODIFIER") || "",
});
continue;
}
mustConsume("END");
}
return result;
}
/**
* Create path match function from `path-to-regexp` spec.
*/
function match(str, options) {
var keys = [];
var re = pathToRegexp(str, keys, options);
return regexpToFunction(re, keys, options);
}
/**
* Create a path match function from `path-to-regexp` output.
*/
function regexpToFunction(re, keys, options) {
if (options === undefined) { options = {}; }
var _a = options.decode, decode = _a === undefined ? function (x) { return x; } : _a;
return function (pathname) {
var m = re.exec(pathname);
if (!m)
return false;
var path = m[0], index = m.index;
var params = Object.create(null);
var _loop_1 = function (i) {
if (m[i] === undefined)
return "continue";
var key = keys[i - 1];
if (key.modifier === "*" || key.modifier === "+") {
params[key.name] = m[i].split(key.prefix + key.suffix).map(function (value) {
return decode(value, key);
});
}
else {
params[key.name] = decode(m[i], key);
}
};
for (var i = 1; i < m.length; i++) {
_loop_1(i);
}
return { path: path, index: index, params: params };
};
}
/**
* Escape a regular expression string.
*/
function escapeString(str) {
return str.replace(/([.+*?=^!:${}()[\]|/\\])/g, "\\$1");
}
/**
* Get the flags for a regexp from the options.
*/
function flags(options) {
return options && options.sensitive ? "" : "i";
}
/**
* Pull out keys from a regexp.
*/
function regexpToRegexp(path, keys) {
if (!keys)
return path;
var groupsRegex = /\((?:\?<(.*?)>)?(?!\?)/g;
var index = 0;
var execResult = groupsRegex.exec(path.source);
while (execResult) {
keys.push({
// Use parenthesized substring match if available, index otherwise
name: execResult[1] || index++,
prefix: "",
suffix: "",
modifier: "",
pattern: "",
});
execResult = groupsRegex.exec(path.source);
}
return path;
}
/**
* Transform an array into a regexp.
*/
function arrayToRegexp(paths, keys, options) {
var parts = paths.map(function (path) { return pathToRegexp(path, keys, options).source; });
return new RegExp("(?:".concat(parts.join("|"), ")"), flags(options));
}
/**
* Create a path regexp from string input.
*/
function stringToRegexp(path, keys, options) {
return tokensToRegexp(parse(path, options), keys, options);
}
/**
* Expose a function for taking tokens and returning a RegExp.
*/
function tokensToRegexp(tokens, keys, options) {
if (options === undefined) { options = {}; }
var _a = options.strict, strict = _a === undefined ? false : _a, _b = options.start, start = _b === undefined ? true : _b, _c = options.end, end = _c === undefined ? true : _c, _d = options.encode, encode = _d === undefined ? function (x) { return x; } : _d, _e = options.delimiter, delimiter = _e === undefined ? "/#?" : _e, _f = options.endsWith, endsWith = _f === undefined ? "" : _f;
var endsWithRe = "[".concat(escapeString(endsWith), "]|$");
var delimiterRe = "[".concat(escapeString(delimiter), "]");
var route = start ? "^" : "";
// Iterate over the tokens and create our regexp string.
for (var _i = 0, tokens_1 = tokens; _i < tokens_1.length; _i++) {
var token = tokens_1[_i];
if (typeof token === "string") {
route += escapeString(encode(token));
}
else {
var prefix = escapeString(encode(token.prefix));
var suffix = escapeString(encode(token.suffix));
if (token.pattern) {
if (keys)
keys.push(token);
if (prefix || suffix) {
if (token.modifier === "+" || token.modifier === "*") {
var mod = token.modifier === "*" ? "?" : "";
route += "(?:".concat(prefix, "((?:").concat(token.pattern, ")(?:").concat(suffix).concat(prefix, "(?:").concat(token.pattern, "))*)").concat(suffix, ")").concat(mod);
}
else {
route += "(?:".concat(prefix, "(").concat(token.pattern, ")").concat(suffix, ")").concat(token.modifier);
}
}
else {
if (token.modifier === "+" || token.modifier === "*") {
throw new TypeError("Can not repeat \"".concat(token.name, "\" without a prefix and suffix"));
}
route += "(".concat(token.pattern, ")").concat(token.modifier);
}
}
else {
route += "(?:".concat(prefix).concat(suffix, ")").concat(token.modifier);
}
}
}
if (end) {
if (!strict)
route += "".concat(delimiterRe, "?");
route += !options.endsWith ? "$" : "(?=".concat(endsWithRe, ")");
}
else {
var endToken = tokens[tokens.length - 1];
var isEndDelimited = typeof endToken === "string"
? delimiterRe.indexOf(endToken[endToken.length - 1]) > -1
: endToken === undefined;
if (!strict) {
route += "(?:".concat(delimiterRe, "(?=").concat(endsWithRe, "))?");
}
if (!isEndDelimited) {
route += "(?=".concat(delimiterRe, "|").concat(endsWithRe, ")");
}
}
return new RegExp(route, flags(options));
}
/**
* Normalize the given path string, returning a regular expression.
*
* An empty array can be passed in for the keys, which will hold the
* placeholder key descriptions. For example, using `/user/:id`, `keys` will
* contain `[{ name: 'id', delimiter: '/', optional: false, repeat: false }]`.
*/
function pathToRegexp(path, keys, options) {
if (path instanceof RegExp)
return regexpToRegexp(path, keys);
if (Array.isArray(path))
return arrayToRegexp(path, keys, options);
return stringToRegexp(path, keys, options);
}
// imports here
// Misc here
const defaultNotFoundHandler$1 = async (_, res) => {
res.statusCode = 404;
res.write("API route not found");
res.end();
};
// Router class here
class Router {
_basePathDelimited;
_basePath;
_useNotFoundHandler;
_notFoundHandler;
_minCompressionSize;
_logger;
_methodListMap;
_defaultMiddlewareList;
constructor(basePath, config = {}) {
// Make sure to properly delimit the basePath
this._basePathDelimited = basePath.replace(/\/*$/, "/");
// Make sure to strip off the trailing slashes
this._basePath = basePath.replace(/\/*$/, "");
this._useNotFoundHandler = config.useNotFoundHandler ?? true;
this._notFoundHandler = config.notFoundHandler ?? defaultNotFoundHandler$1;
this._minCompressionSize = config.minCompressionSize ?? 1024;
this._logger = new Logger(`Router (${this._basePath})`);
// Initialise the method list manually
this._methodListMap = {
ALL: [],
GET: [],
DELETE: [],
PATCH: [],
POST: [],
PUT: [],
OPTIONS: [],
HEAD: [],
};
this._defaultMiddlewareList = [];
}
// Getter methods here
get basePath() {
return this._basePathDelimited;
}
// Private methods here
searchMethodElements(req, list) {
let matchedEl = null;
// Next see if we have a registered callback for the HTTP req path
for (let el of list) {
let routerMatch = el.match(req.urlObj);
// If result is false that means we found nothing
if (routerMatch === false) {
continue;
}
// If we are here we found the callback
matchedEl = el;
// Don't forget to set the matchedInfo and params properties
req.matchedInfo = routerMatch.matchedInfo;
req.params = routerMatch.params;
// Stop looking
break;
}
return matchedEl;
}
findEndpoint(req) {
let method = req.method;
// Check for a CORS Preflight request - yes there is middleware for this
// but this has to be checked here because we will not have a registered
// endpoint under OPTIONS
if (req.method === "OPTIONS" &&
req.headers["access-control-request-method"] !== undefined) {
// Get the method this preflight request is checking for and use that
// to see there is an endpoint registered for it
method = req.headers["access-control-request-method"];
}
// If the method is HEAD then check the GET method map
if (req.method === "HEAD") {
method = "GET";
}
// Make sure we don't have some odd method we never heard about
let list = this._methodListMap[method];
if (list === undefined) {
return null;
}
// First search for the routes in the req method list
let matchedEl = this.searchMethodElements(req, list);
if (matchedEl === null) {
// If we are here that means we did not find a callback for the req path
// and we should check for a fallback callback
matchedEl = this.searchMethodElements(req, this._methodListMap["ALL"]);
}
return matchedEl;
}
async callMiddleware(req, res, el, middlewareStack) {
// Check if there handlers still be be called on the stack
if (middlewareStack.length) {
// Call the top handler and pass the rest of the handlers after it
await middlewareStack[0](req, res, async () => {
await this.callMiddleware(req, res, el, middlewareStack.slice(1));
});
}
else {
// No more handlers but make sure is NOT an unhandled preflight check.
// If it is then we DO NOT want to call the endpoint handler
if (req.method !== "OPTIONS") {
await this.callEndpoint(req, res, el);
}
}
}
async callEndpoint(req, res, el) {
// Check if this should be a server sent event endpoint
if (el.sseServerOptions !== undefined) {
req.sseServer = new SseServer(req, res, el.sseServerOptions);
}
// The callback can be async or not so check for it
if (el.callback.constructor.name === "AsyncFunction") {
// This is async so use await
await el.callback(req, res);
}
else {
// This is a synchronous call
el.callback(req, res);
}
}
async addResponse(req, res, etag) {
let body = null;
// Check if a json or a body response has been passed back
if (res.json !== undefined) {
res.setHeader("Content-Type", "application/json; charset=utf-8");
body = JSON.stringify(res.json);
}
else if (res.body !== undefined) {
// Check if the content-type has not been set
if (!res.hasHeader("Content-Type")) {
// It hasn't so set it to the default type
res.setHeader("Content-Type", "text/plain; charset=utf-8");
}
body = res.body;
}
// Check if the user didnt pass any data to send back (body is null)
if (body === null) {
// This means there will be an empty body so check if the StatusCode has
// been change from the default 200 - if it has leave it alone beacuse
// the user must have set it
if (res.statusCode === 200) {
// Otherwise set the status code to indicate an empty body
res.statusCode = 204;
}
// Don't forget to set the server-timing header before we leave
res.setServerTimingHeader();
// Nothing else to do including calculating and etag so get out of here
return;
}
// We need to ensure body is a string or a Buffer or we will have problems
if (Buffer.isBuffer(body) === false && typeof body !== "string") {
this._logger.error("(%s) response body for (%s) is not of type string or Buffer", req.method, req.urlObj.pathname);
res.statusCode = 500;
res.end();
return;
}
// Check if the user wants an etag added to the response
if (etag) {
let etag = crypto.createHash("sha1").update(body).digest("hex");
// All headers need to be set, except content-length, for a 304
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Etag", etag);
// Check if any cache validators exist on the request
if (req.headers["if-none-match"] === etag) {
// Don't forget to set the server-timing header after we do everything else
res.setServerTimingHeader();
res.statusCode = 304;
res.end();
return;
}
}
// Check out if the req will accept a gzip res AND the body is large enough
// AND compression is not turned off for this request
let gzipIt = false;
// Check if the res was proxied. If it was then DO NOT set the
// transfer-encoding/content-encoding header nor the content-length.
// Assume that has already been done
if (res.proxied === false) {
if (req.headers["accept-encoding"]?.includes("gzip") === true &&
Buffer.byteLength(body) >= this._minCompressionSize &&
req.dontCompressResponse === false) {
// It does ...
gzipIt = true;
// Dont set the content-length. Use transfer-encoding instead
res.setHeader("Transfer-Encoding", "chunked");
res.setHeader("Content-Encoding", "gzip");
}
else {
// It does not ...
// Only set the length when we don't do a 304
res.setHeader("Content-Length", Buffer.byteLength(body));
}
}
// Don't forget to set the server-timing header after we do everything else
res.setServerTimingHeader();
// Check if this was a HEAD method - if so we don't want to write the body
if (req.method !== "HEAD") {
if (gzipIt) {
const passThrough = new PassThrough();
passThrough.end(body);
// NOTE1: pipeline will close the res when it is finished
await streams
.pipeline(passThrough, zlib.createGzip(), res)
.catch((e) => {
// We can't do anything else here because either:
// - the stream is closed which means we can't send back an error
// - we have an internal error, but we have already started streaming
// so we can't do anything
this._logger.error("addResponse had this error during streaming: (%s)", e);
});
}
else {
res.write(body);
}
}
res.end();
}
// Public methods here
inPath(pathname) {
// Make sure to use the delimited base path to ensure a correct match
return pathname.startsWith(this._basePathDelimited);
}
async handleReq(req, res) {
// See if this request matches a registered endpoint
let matchedEl = this.findEndpoint(req);
if (matchedEl === null) {
// Check if we should use the supplied Not Found handler or not
if (this._useNotFoundHandler) {
await this._notFoundHandler(req, res);
return true;
}
// Couldn't find a match so flag that the req has not been handled
return false;
}
await this.callMiddleware(req, res, matchedEl, matchedEl.middlewareList).catch((e) => {
let message;
// If a redirect call res.redirect() and get out of the error handler
if (e instanceof HttpRedirect) {
res.redirect(e.location, e.statusCode, e.message);
return;
}
// If it is a HttpError assume the error message has already been logged
if (e instanceof HttpError) {
res.statusCode = e.status;
message = e.message;
}
else {
// We don't know what this is so log it and make sure to return a 500
this._logger.error("Unknown error happened while handling URL (%s) - (%s)", req.urlObj.pathname, e);
res.statusCode = 500;
message = "Unknown error happened";
}
// Check if res.write() has NOT been called yet
if (!res.headersSent) {
res.setHeader("Content-Type", "text/plain; charset=utf-8");
res.setHeader("Content-Length", Buffer.byteLength(message));
res.write(message);
}
// Check if the res.end() has NOT been called yet
if (!res.writableEnded) {
// End the response now
res.end();
}
});
// If this is an SSE server dont call addResponse or res.end()
if (req.sseServer !== undefined) {
return true;
}
// Check if res.write() has NOT been called yet
if (!res.headersSent) {
// Check if the callback wants us to add the body, headers etc
await this.addResponse(req, res, matchedEl.etag);
}
// Check if the res.end() has NOT been called yet
if (!res.writableEnded) {
// End the response now
res.end();
}
// Flag this req has been handled
return true;
}
pathToRegexMatcher(path) {
// Create the matching function
let match$1 = match(path, {
decode: decodeURIComponent,
strict: true,
});
return (url) => {
let result = match$1(url.pathname);
if (result === false) {
return false;
}
return {
params: result.params,
matchedInfo: result,
};
};
}
matchAllMatcher(_) {
// This will match everything
return (url) => {
return {
matchedInfo: url.pathname,
params: {}, // We dont know that the params are so just ignore them
};
};
}
use(middleware) {
this._defaultMiddlewareList.push(middleware);
return this;
}
endpoint(method, path, callback, endpointOptions = {}) {
let options = {
useDefaultMiddlewares: true,
etag: false,
generateMatcher: this.pathToRegexMatcher,
...endpointOptions,
};
// Make sure we have the middlewares requested
let middlewareList = [];
// Check if the user wants the default middlewares
if (options.useDefaultMiddlewares) {
// ... stick the default middlewares in first
// NOTE: Any middleware added to the defaults after this endpoint is
// added will not be used by this endpoint
middlewareList = [...this._defaultMiddlewareList];
}
if (options.middlewareList !== undefined) {
middlewareList = [...middlewareList, ...options.middlewareList];
}
// GEt the full path - check if the path already includes the basePath
let fullPath = this.inPath(path) ? path : `${this._basePath}${path}`;
// Finally add it to the list of callbacks
this._methodListMap[method].push({
match: options.generateMatcher(fullPath),
callback,
middlewareList,
sseServerOptions: options.sseServerOptions,
etag: options.etag,
});
this._logger.startupMsg("Added %s endpoint for path (%s)", method.toUpperCase(), fullPath);
return this;
}
// endpoint helper methods here
del(path, callback, endpointOptions = {}) {
this.endpoint("DELETE", path, callback, endpointOptions);
return this;
}
get(path, callback, endpointOptions = {}) {
this.endpoint("GET", path, callback, endpointOptions);
return this;
}
patch(path, callback, endpointOptions = {}) {
this.endpoint("PATCH", path, callback, endpointOptions);
return this;
}
post(path, callback, endpointOptions = {}) {
this.endpoint("POST", path, callback, endpointOptions);
return this;
}
put(path, callback, endpointOptions = {}) {
this.endpoint("PUT", path, callback, endpointOptions);
return this;
}
all(path, callback, endpointOptions = {}) {
this.endpoint("ALL", path, callback, endpointOptions);
return this;
}
route(path) {
let server = this;
return {
get(callback, endpointOptions = {}) {
server.endpoint("GET", path, callback, endpointOptions);
return server.route(path);
},
patch(callback, endpointOptions = {}) {
server.endpoint("PATCH", path, callback, endpointOptions);
return server.route(path);
},
post(callback, endpointOptions = {}) {
server.endpoint("POST", path, callback, endpointOptions);
return server.route(path);
},
put(callback, endpointOptions = {}) {
server.endpoint("PUT", path, callback, endpointOptions);
return server.route(path);
},
del(callback, endpointOptions = {}) {
server.endpoint("DELETE", path, callback, endpointOptions);
return server.route(path);
},
all(callback, endpointOptions = {}) {
server.endpoint("ALL", path, callback, endpointOptions);
return server.route(path);
},
};
}
// Middleware methods here
static body(options = {}) {
// Rem we have to call bodyMiddleware since it returns the middleware
return bodyMiddleware(options);
}
static json() {
return jsonMiddleware();
}
static cors(options = {}) {
return corsMiddleware(options);
}
static csrf(options = {}) {
return csrfChecksMiddleware(options);
}
static getSecHeaders(options) {
return getSecurityHeaders(options);
}
static secHeaders(options) {
return securityHeadersMiddleware(options);
}
static expressWrapper(options) {
return expressWrapper(options);
}
static dontCompressResponse() {
return dontCompressResponse();
}
static setLatencyMetricName(name) {
return setLatencyMetricName(name);
}
}
// imports here
// Misc here
const defaultNotFoundHandler = async (_, res) => {
res.statusCode = 404;
res.write("File not found");
res.end();
};
// StaticFileServer class here
class StaticFileServer {
_logger;
_filePath;
_immutableRegExp;
_defaultDirFile;
_defaultCharSet;
_notFoundHandler;
_staticFileMap;
_contentTypes;
_securityHeaders;
constructor(config) {
// Make sure there is no trailing slash at the end of the path
this._logger = new Logger(config.loggerName);
this._logger.startupMsg("Creating static file server ...");
this._filePath = config.filePath.replace(/\/*$/, "");
// Initialise the immutable regexs array
this._immutableRegExp = [];
// Check if user has provided an immutable config value
if (config.immutableRegExp !== undefined) {
// Check if user has provided a regexp or string or array
if (config.immutableRegExp instanceof RegExp) {
this._immutableRegExp.push(config.immutableRegExp);
}
else if (typeof config.immutableRegExp === "string") {
this._immutableRegExp.push(new RegExp(config.immutableRegExp));
}
else if (Array.isArray(config.immutableRegExp)) {
for (const exp of config.immutableRegExp) {
// This is an array so iterate through each element and add it to the list
if (exp instanceof RegExp) {
this._immutableRegExp.push(exp);
}
else if (typeof exp === "string") {
this._immutableRegExp.push(new RegExp(exp));
}
}
}
}
this._defaultDirFile = config.defaultDirFile ?? "index.html";
this._defaultCharSet = config.defaultCharSet ?? "charset=utf-8";
this._notFoundHandler = config.notFoundHandler ?? defaultNotFoundHandler;
this._staticFileMap = new Map();
this._contentTypes = new Map();
// Get the standard sec headers and add the users specified headers as well
this._securityHeaders = Router.getSecHeaders({
headers: config.securityHeaders,
});
// Populate contentTypes using the predefined types
for (const type in contentTypes) {
this._contentTypes.set(type, contentTypes[type]);
}
// Then add any extra content types. NOTE: This allows you to overwrite
// the predefined types
if (config.extraContentTypes !== undefined) {
for (const type in config.extraContentTypes) {
this._contentTypes.set(type, config.extraContentTypes[type]);
}
}
// Get all of the files at start up - but a constructor cant be async so
// run getFilesRecursively() at the earliest possibile time
setImmediate(async () => {
await this.getFilesRecursively();
});
}
// Private methods here
async getFilesRecursively(urlPath = "/") {
// Note: urlPath should always start and end in "/"
const dir = `${this._filePath}${urlPath}`;
let dirFiles = [];
// Get a list of files in the dir and check for errors
try {
dirFiles = fs.readdirSync(dir);
}
catch (e) {
this._logger.warn("No permissions to read from dir (%s)", dir);
}
// Iterate through each file and check if it is a dir or not
for (const file of dirFiles) {
const fullPath = `${dir}${file}`;
const stats = fs.statSync(fullPath);
const url = `${urlPath}${file}`;
if (stats.isDirectory()) {
// Get the files in this dir
this.getFilesRecursively(`${url}/`);
}
else if (stats.isFile()) {
// Add the file to the list
await this.addFile(fullPath, `${url}`, stats);
}
}
}
lookupType(file) {
// Look up the file extension to get content type - drop the leading '.'
const ext = path.extname(file).slice(1);
const type = this._contentTypes.get(ext);
if (type !== undefined) {
return `${type}; ${this._defaultCharSet}`;
}
// This is the default content type
return `text/plain; ${this._defaultCharSet}`;
}
async calculateEtag(fileBuffer, fileName) {
// MD5 hash the file contents to calculate the etag
const contents = stream.Readable.from(fileBuffer);
const hash = crypto.createHash("sha1");
// Flag to check if we successfully pipe the file to the hash
let failed = false;
await streams.pipeline(contents, hash).catch((e) => {
this._logger.trace("Error attempting to create etag for file (%s) (%s): ", fileName, e);
failed = true;
});
if (failed) {
return null;
}
return hash.digest("hex");
}
async addFile(fullPath, urlPath, stats) {
// Use a flag to decide if we add the file to the file map or not
let addFile = true;
try {
// Test if we can read the file
fs.accessSync(fullPath, fs.constants.R_OK);
}
catch (e) {
// There was an error which means we cant read the file so DO NOT add it
addFile = false;
this._logger.warn("No permissions to read file : (%s)", fullPath);
}
if (addFile === false) {
// Can't add file so do nothing
return false;
}
// Add the file and it's details to the map
const modTimeMs = stats.mtime.getTime();
// Get rid of the ms from the time because we lose it when we convert to a
// UTC string which means we get a mismatch checking "If-Modified-Since"
const modTimeNoMs = Math.trunc(modTimeMs / 1000) * 1000;
const fileBuffer = fs.readFileSync(fullPath);
const eTag = await this.calculateEtag(fileBuffer, fullPath);
if (eTag === null) {
// Couldn't calculate the etag so do nothing
return false;
}
// Default immutable to false until we can prove it is
let immutable = false;
// Now check if the path matches one of the RegExps
for (const regexp of this._immutableRegExp) {
if (regexp.test(fullPath) === true) {
immutable = true;
break;
}
}
const fileDetails = {
contentType: this.lookupType(fullPath), // In case urlPath is a dir
size: stats.size,
lastModifiedNoMs: modTimeNoMs,
lastModifiedMs: modTimeMs,
lastModifiedUtcStr: new Date(modTimeNoMs).toUTCString(),
eTag,
fullPath,
immutable,
fileBuffer,
compressedBuffer: zlib.gzipSync(fileBuffer),
};
this._staticFileMap.set(urlPath, fileDetails);
this._logger.trace("Added (%s) to file map. Details: contentType (%s), size (%s), lastModifiedMs(%s), eTag (%s), fullPath (%s), immutable (%j)", urlPath, fileDetails.contentType, fileDetails.size, fileDetails.lastModifiedMs, fileDetails.eTag, fileDetails.fullPath, fileDetails.immutable);
return true;
}
async getFileDetails(file) {
// Check for the details first. If it exists we want to use the stored full
// path just in case file is s dir. It will save and extra stat!
let details = this._staticFileMap.get(file);
let fullPath = details?.fullPath ?? `${this._filePath}${file.replace(/\/*$/, "")}`;
// If we can't stat the file (doesn't exist) then stat will throw
let stats = await fsPromises.stat(fullPath).catch((e) => {
this._logger.trace("Received an error when trying to stat (%s): (%s)", fullPath, e);
});
if (stats === undefined) {
return undefined;
}
// Check if the file is a directory (should only happen the 1st time)
if (stats.isDirectory()) {
// This is a dir so set the file to be the default file for a dir
fullPath += `/${this._defaultDirFile}`;
// Get the stats again for the default file. If we can't stat the file
// (doesn't exist) then stat will throw
stats = await fsPromises.stat(fullPath).catch((e) => {
this._logger.trace("Received an error when trying to stat (%s): (%s)", fullPath, e);
});
if (stats === undefined) {
return undefined;
}
}
// Check if the file wasn't in the file map or it was modified
if (details === undefined ||
details.lastModifiedMs !== stats.mtime.getTime() ||
details.size !== stats.size) {
// Add the file to the file map and get the new details
await this.addFile(fullPath, file, stats);
details = this._staticFileMap.get(file);
}
return details;
}
// Public methods here
async handleReq(req, res) {
// We only handle GET and HEAD for static files. Return a not found
if (req.method !== "GET" && req.method !== "HEAD") {
this._notFoundHandler(req, res);
return;
}
// Get the file details and if it doesn't exist return a not found
const details = await this.getFileDetails(req.urlObj.pathname);
if (details === undefined) {
this._notFoundHandler(req, res);
return;
}
const cacheControl = details.immutable
? "max-age=31536000, immutable"
: "no-cache";
// All headers need to be set, except content-length, for a 304
res.setHeader("Cache-Control", cacheControl);
res.setHeader("Etag", details.eTag);
res.setHeader("Last-Modified", details.lastModifiedUtcStr);
res.setHeader("Date", new Date().toUTCString());
res.setHeader("Content-Type", details.contentType);
// Set all of the sec headers
for (const header of this._securityHeaders) {
res.setHeader(header.name, header.value);
}
// Don't forget to set the server-timing header
res.latencyMetricName = "sf-srv";
res.setServerTimingHeader();
// Check if any cache validators exist on the request - check etag first
if (req.headers["if-none-match"] === details.eTag) {
res.statusCode = 304;
res.end();
return;
}
if (req.headers["if-modified-since"] !== undefined) {
const modifiedDate = new Date(req.headers["if-modified-since"]).getTime();
// NOTE: Check the times are the same, if they are different, even if
// details.lastModifiedNoMs is LESS than modifiedDate, it will still
// because that implies there is a potential issue and it is best to
// be safe
if (modifiedDate === details.lastModifiedNoMs) {
res.statusCode = 304;
res.end();
return;
}
}
let fileRead;
// Check out if the req will accept a gzip res
if (req.headers["accept-encoding"]?.includes("gzip") === true) {
// It does ...
fileRead = stream.Readable.from(details.compressedBuffer);
// Dont set the content-length. Use transfer-encoding instead
res.setHeader("Transfer-Encoding", "chunked");
res.setHeader("Content-Encoding", "gzip");
}
else {
// It does not ...
fileRead = stream.Readable.from(details.fileBuffer);
// Only set the length when we don't do a 304
res.setHeader("Content-Length", details.size);
}
// If it's a HEAD then do not set the body
if (req.method === "HEAD") {
res.end();
return;
}
// NOTE: pipeline will close the res when it is finished
await streams.pipeline(fileRead, res).catch((e) => {
// We can't do anything else here because either:
// - the stream is closed which means we can't send back an error
// - we have an internal error, but we have already started streaming
// so we can't do anything
this._logger.trace("Error attempting to read (%s): (%s)", fileRead, e);
});
}
}
// imports here
class HttpConfigError {
message;
constructor(message) {
this.message = message;
}
}
// HttpServer class here
class HttpServer {
_logger;
_socketMap;
_socketId;
_networkInterface;
_networkPort;
_networkIp;
_baseUrl;
_name;
_healthcheckCallbacks;
_httpKeepAliveTimeout;
_httpHeaderTimeout;
_healthCheckPath;
_healthCheckGoodResCode;
_healthCheckBadResCode;
_enableHttps;
_keyFile;
_certFile;
_maintenanceModeOn;
_maintenanceRoute;
_apiRouterList;
_defaultApiRouter;
_ssrRouter;
_staticFileServer;
_server;
constructor(networkInterface, networkPort, config = {}) {
this._name = `${networkInterface}-${networkPort}`;
this._logger = new Logger(`HttpServer-${this._name}`);
this._httpKeepAliveTimeout = config.keepAliveTimeout ?? 65000;
this._httpHeaderTimeout = config.headerTimeout ?? 66000;
this._healthCheckPath = config.healthcheckPath ?? "/healthcheck";
this._healthCheckGoodResCode = config.healthcheckGoodRes ?? 200;
this._healthCheckBadResCode = config.healthcheckBadRes ?? 503;
this._enableHttps = config.enableHttps ?? false;
this._maintenanceRoute = config.maintenanceRoute;
this._maintenanceModeOn = config.startInMaintenanceMode ?? false;
this._logger.startupMsg("Maintenance mode is set to (%j)", this._maintenanceModeOn);
this._socketMap = new Map();
this._socketId = 0;
this._networkIp = "";
this._baseUrl = "";
this._networkInterface = networkInterface;
this._networkPort = networkPort;
this._healthcheckCallbacks = [];
this._apiRouterList = [];
// Create the default router AFTER you initialise the _routerList
this._defaultApiRouter = this.addRouter(config.defaultRouterBasePath ?? "/api");
if (this._enableHttps) {
this._keyFile = config.httpsKeyFile;
this._certFile = config.httpsCertFile;
}
// Make sure the SSR Router DOES NOT use the not found handler - we need it
// to pass control to the static file server and do not add it to the
// _apiRouterList since it doesnt have a fixed base path
this._ssrRouter = new Router("/", { useNotFoundHandler: false });
this._logger.startupMsg("SSR router created");
if (config.staticFileServer !== undefined) {
this._staticFileServer = new StaticFileServer({
loggerName: `HttpServer-${this._name}/StaticFile`,
filePath: config.staticFileServer.path,
extraContentTypes: config.staticFileServer.extraContentTypes,
immutableRegExp: config.staticFileServer.immutableRegExp,
securityHeaders: config.staticFileServer.securityHeaders,
});
}
}
// Getter methods here
get networkIp() {
return this._networkIp;
}
get networkPort() {
return this._networkPort;
}
get baseUrl() {
return this._baseUrl;
}
get httpsEnabled() {
return this._enableHttps;
}
get name() {
return this._name;
}
get ssrRouter() {
return this._ssrRouter;
}
// Setter methods here
set maintenanceModeOn(on) {
this._maintenanceModeOn = on;
this._logger.info("Maintenance mode set to (%j)", this._maintenanceModeOn);
}
// Private methods here
findInterfaceIp(networkInterface) {
const ipv4Regex = /^(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})(\.(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{1,2})){3}$/;
if (ipv4Regex.test(networkInterface)) {
this._logger.startupMsg(`Using provided IP (${networkInterface})`);
return networkInterface;
}
this._logger.startupMsg(`Finding IP for interface (${networkInterface})`);
let ifaces = os.networkInterfaces();
this._logger.startupMsg("Interfaces on host: %j", ifaces);
if (ifaces[networkInterface] === undefined) {
return null;
}
let ip = "";
// Search for the first I/F with a family of type IPv4
let found = ifaces[networkInterface]?.find((i) => i.family === "IPv4");
if (found !== undefined) {
ip = found.address;
this._logger.startupMsg(`Found IP (${ip}) for interface ${networkInterface}`);
}
if (ip.length === 0) {
return null;
}
return ip;
}
async startListening(server) {
// Start listening
server.listen(this._networkPort, this._networkIp);
// Since this is an async event we need to wait for the "listening" event
// to fire, so lets wrap this in a Promise and resolve the promise when
// it happens
return new Promise((resolve, _) => {
server.on("listening", () => {
this._logger.startupMsg(`Now listening on (${this._baseUrl}). HTTP manager started!`);
resolve();
});
// We also want to track all of the sockets that are opened
server.on("connection", (socket) => {
// We need a local copy of the socket ID for this closure to work
let socketId = this._socketId++;
this._socketMap.set(socketId, socket);
this._logger.trace("'connection' for socketId (%d) on remote connection (%s/%s)", socketId, socket.remoteAddress, socket.remotePort);
// Check when the socket closes
socket.on("close", () => {
// First check if the socket has not been closed during a stop()
if (this._socketMap.has(socketId)) {
this._socketMap.delete(socketId);
this._logger.trace("'close' for socketId (%d) on remote connection (%s/%s)", socketId, socket.remoteAddress, socket.remotePort);
}
});
});
});
}
async handleReq(req, res) {
// Check if we are in maintenance mode
if (this._maintenanceModeOn && this._maintenanceRoute !== undefined) {
this._logger.trace("Maintenance mode on. Redirecting (%s) to (%s)", req.url, this._maintenanceRoute);
// This isn't very sexy and seems a little heavy handed but works a treat
// we just point the req to the maintenance route and pray the user set
// it up!
req.method = "GET";
req.url = this._maintenanceRoute;
}
// We have to do this here because the url will not be set until
// after this object it created: See req-res.ts
let protocol = this._enableHttps ? "https" : "http";
req.urlObj = new URL(req.url, `${protocol}://${req.headers.host}`);
this._logger.trace("Received (%s) req for (%s)", req.method, req.urlObj.pathname);
// Look for a router with a basePath that matches the start of the req path
// NOTE: Make sure to delimit the pathname in case it is a match for
// the root of the basepath
let router = this._apiRouterList.find((el) => el.inPath(`${req.urlObj.pathname}/`));
// Try and handle the request (if router exists)
if ((await router?.handleReq(req, res)) === true) {
return;
}
// If we're here this wasn't an API req so check if it was SSR req
if (await this._ssrRouter.handleReq(req, res)) {
return;
}
// If we're here this wasn't a SSR req so check if we're serving
// static files
if (this._staticFileServer !== undefined) {
await this._staticFileServer.handleReq(req, res);
return;
}
// If we are here then we dont know this URL so return a 404
res.statusCode = 404;
res.write("Not found");
res.end();
}
async healthcheckCallback(_1, res) {
let healthy = true;
for (let cb of this._healthcheckCallbacks) {
healthy = await cb();
if (!healthy) {
break;
}
}
if (healthy) {
res.statusCode = this._healthCheckGoodResCode;
res.body = "Healthy";
}
else {
res.statusCode = this._healthCheckBadResCode;
res.body = "Not Healthy";
}
}
// Public methods here
async start() {
this._logger.startupMsg("Initialising HTTP manager ...");
let ip = this.findInterfaceIp(this._networkInterface);
if (ip === null) {
throw new Error(`${this._networkInterface} is not an interface on this server`);
}
this._networkIp = ip;
this._logger.startupMsg(`Will listen on interface ${this._networkInterface} (IP: ${this._networkIp})`);
// Create either a HTTP or HTTPS server
if (this._enableHttps) {
this._baseUrl = `https://${this._networkIp}:${this._networkPort}`;
if (this._keyFile === undefined) {
throw new HttpConfigError("HTTPS is enabled but no key file provided!");
}
if (this._certFile === undefined) {
throw new HttpConfigError("HTTPS is enabled but no cert file provided!");
}
this._logger.startupMsg(`Attempting to listen on (${this._baseUrl})`);
const options = {
IncomingMessage: ServerRequest,
ServerResponse: ServerResponse, // Something wrong with typedefs
key: fs.readFileSync(this._keyFile),
cert: fs.readFileSync(this._certFile),
};
this._server = https.createServer(options, (req, res) => {
this.handleReq(req, res);
});
}
else {
this._baseUrl = `http://${this._networkIp}:${this._networkPort}`;
this._logger.startupMsg(`Attempting to listen on (${this._baseUrl})`);
const options = {
IncomingMessage: ServerRequest,
ServerResponse: ServerResponse, // Something wrong with typedefs
};
this._server = http.createServer(options, (req, res) => {
this.handleReq(req, res);
});
}
this._server.keepAliveTimeout = this._httpKeepAliveTimeout;
this._server.headersTimeout = this._httpHeaderTimeout;
await this.startListening(this._server);
// Now we need to add the endpoint for healthchecks
this._defaultApiRouter.get(this._healthCheckPath, async (req, res) => this.healthcheckCallback(req, res), { useDefaultMiddlewares: false });
}
async stop() {
this._logger.shutdownMsg("Closing all connections now ...");
// Close all the remote connections
this._socketMap.forEach((socket, key) => {
socket.destroy();
this._logger.trace("Destroying socketId (%d) for remote connection (%s/%s)", key, socket.remoteAddress, socket.remotePort);
});
// Just in case someone calls stop() a 2nd time
this._socketMap.clear();
if (this._server !== undefined) {
this._logger.shutdownMsg("Closing HTTP manager port now ...");
this._server.close();
this._logger.shutdownMsg("Port closed");
// Just in case someone calls stop() a 2nd time
this._server = undefined;
}
return;
}
addHealthcheck(callback) {
this._healthcheckCallbacks.push(callback);
}
addRouter(basePath, routerConfig = {}) {
// Make sure the basePath is properly terminated
basePath = basePath.replace(/\/*$/, "/");
// Check to make sure this basePath does not overlap with another router's
// basePath
let found = this._apiRouterList.find((el) => {
return el.inPath(basePath) || el.basePath.startsWith(basePath);
});
// If there is an overlap with an existing router then "stop the press"!
if (found !== undefined) {
throw new Error(`${basePath} clashes with basePath of ${found.basePath}`);
}
// If we are here then all is good so create the new router
let router = new Router(basePath, routerConfig);
this._apiRouterList.push(router);
this._logger.startupMsg("(%s) router created", basePath.replace(/\/$/, ""));
return router;
}
router(basePath) {
if (basePath === undefined) {
return this._defaultApiRouter;
}
// Make sure to remove any trailing slashes and then delimit properly
let basePathSanitised = basePath.replace(/\/*$/, "/");
return this._apiRouterList.find((el) => el.basePath === basePathSanitised);
}
// Methods for the default router here
use(middleware) {
return this._defaultApiRouter.use(middleware);
}
del(path, callback, options = {}) {
return this._defaultApiRouter.del(path, callback, options);
}
get(path, callback, options = {}) {
return this._defaultApiRouter.get(path, callback, options);
}
patch(path, callback, options = {}) {
return this._defaultApiRouter.patch(path, callback, options);
}
post(path, callback, options = {}) {
return this._defaultApiRouter.post(path, callback, options);
}
put(path, callback, options = {}) {
return this._defaultApiRouter.put(path, callback, options);
}
endpoint(method, path, callback, options = {}) {
return this._defaultApiRouter.endpoint(method, path, callback, options);
}
route(path) {
return this._defaultApiRouter.route(path);
}
}
// imports here
// BSPlugin class here
class BSPlugin {
_name;
_version;
_logger;
// Constructor here
constructor(name, version) {
this._name = name;
this._version = version;
this._logger = new Logger(this._name);
this.startupMsg("Initialising ...");
}
// Protected methods (that can be overridden) here
async stop() {
// This is a default stop method. Override it if you need to clean up
this.shutdownMsg("Stopped!");
}
// Getters here
get name() {
return this._name;
}
get version() {
return this._version;
}
get stopHandler() {
return this.stop;
}
// Protected methods here
// Log convinence methods
fatal(...args) {
this._logger.fatal(...args);
}
error(...args) {
this._logger.error(...args);
}
warn(...args) {
this._logger.warn(...args);
}
info(...args) {
this._logger.info(...args);
}
startupMsg(...args) {
this._logger.startupMsg(...args);
}
shutdownMsg(...args) {
this._logger.shutdownMsg(...args);
}
debug(...args) {
this._logger.debug(...args);
}
trace(...args) {
this._logger.trace(...args);
}
force(...args) {
this._logger.force(...args);
}
}
// imports here
// Misc consts here
const LOGGER_APP_NAME = "App";
// NOTE: 1.20.4 is replaced with package.json#version by a
// rollup plugin at build time
const VERSION = "1.20.4";
// Module private variables here
let _logger;
let _httpServerList;
let _pluginMap;
let _sharedStore;
const _shutdownHandler = async () => {
await bs.exit(0);
};
const _exceptionHandler = async (e) => {
bs.error("Caught unhandled error - (%s)", e);
await bs.exit(1);
};
let _finallyHandler = async () => {
bs.shutdownMsg("Done!");
};
let _stopHandler = async () => {
bs.shutdownMsg("Stopped!");
};
let _restartHandler = async () => {
bs.shutdownMsg("Restarted!");
};
// The shell object here
const bs = Object.freeze({
// request wrapper
request: async (origin, path, reqOptions) => {
return request(origin, path, reqOptions);
},
// Config helper methods here
/**
* Gets a string config value.
*
* @param config - The config key to get.
* @param defaultVal - The default value if config not found.
* @param options - Options for getting the config.
* @returns The string config value.
*/
getConfigStr: (config, defaultVal, options) => {
let value = configMan.getStr(config, defaultVal, options);
logConfigManMsgs();
return value;
},
/**
* Gets a boolean config value.
*
* @param config - The config key to get.
* @param defaultVal - The default value if config not found.
* @param options - Options for getting the config.
* @returns The boolean config value.
*/
getConfigBool: (config, defaultVal, options) => {
let value = configMan.getBool(config, defaultVal, options);
logConfigManMsgs();
return value;
},
/**
* Gets a number config value.
*
* @param config - The config key to get.
* @param defaultVal - The default value if config not found.
* @param options - Options for getting the config.
* @returns The number config value.
*/
getConfigNum: (config, defaultVal, options) => {
let value = configMan.getNum(config, defaultVal, options);
logConfigManMsgs();
return value;
},
/**
* Gets an object config value.
*
* @param config - The config key to get.
* @param defaultVal - The default value if config not found.
* @param options - Options for getting the config.
* @returns The object config value.
*/
getConfigObj: (config, defaultVal, options) => {
let value = (configMan.getObject(config, defaultVal, options));
logConfigManMsgs();
return value;
},
/**
* Gets an array config value.
*
* @param config - The config key to get.
* @param defaultVal - The default value if config not found.
* @param options - Options for getting the config.
* @returns The object config value.
*/
getConfigArray: (config, defaultVal, options) => {
let value = configMan.getObject(config, defaultVal, options);
logConfigManMsgs();
return value;
},
// Log convience methods here
fatal: (...args) => {
_logger.fatal(...args);
},
error: (...args) => {
_logger.error(...args);
},
warn: (...args) => {
_logger.warn(...args);
},
info: (...args) => {
_logger.info(...args);
},
startupMsg: (...args) => {
_logger.startupMsg(...args);
},
shutdownMsg: (...args) => {
_logger.shutdownMsg(...args);
},
debug: (...args) => {
_logger.debug(...args);
},
trace: (...args) => {
_logger.trace(...args);
},
force: (...args) => {
_logger.force(...args);
},
setLogLevel: (level) => {
_logger.setLevel(level);
},
// General functions here
shellVersion: () => {
return VERSION;
},
setFinallyHandler: (handler) => {
_finallyHandler = handler;
},
setStopHandler: (handler) => {
_stopHandler = handler;
},
setRestartHandler: (handler) => {
_restartHandler = handler;
},
exit: async (code, hard = true) => {
bs.shutdownMsg("Exiting ...");
// Clear the global and const stores
_sharedStore.clear();
// Make sure we stop all of the HttpSevers - probably best to do it first
for (let httpServer of _httpServerList) {
await httpServer.stop();
}
// Clear the HttpServer list
_httpServerList = [];
// Stop the application second
bs.shutdownMsg("Attempting to stop the application ...");
await _stopHandler().catch((e) => {
bs.error(e);
});
// Stop the plugins in the reverse order you started them
for (let plugin of [..._pluginMap.values()].reverse()) {
bs.shutdownMsg(`Attempting to stop plugin ${plugin.name} ...`);
await plugin.stopHandler().catch((e) => {
bs.error(e);
});
}
// Clear the plugin list
_pluginMap.clear();
// If there was a finally handler provided then call it last
if (_finallyHandler !== undefined) {
bs.shutdownMsg("Calling the 'finally handler' ...");
await _finallyHandler().catch((e) => {
bs.error(e);
});
}
// Remove the event handlers for catching exit events
process.removeListener("SIGINT", _shutdownHandler);
process.removeListener("SIGTERM", _shutdownHandler);
process.removeListener("beforeExit", _shutdownHandler);
process.removeListener("uncaughtException", _exceptionHandler);
process.removeListener("SIGHUP", bs.restart);
bs.shutdownMsg("So long and thanks for all the fish!");
// Check if the exit should also exit the process (a hard stop)
if (hard) {
process.exit(code);
}
},
restart: async () => {
bs.info("Restarting now!");
// Re-init the logger in case config values have changed
_logger = new Logger(LOGGER_APP_NAME);
// Do a soft exit
await bs.exit(0, false);
// Then re-init this bad boy
init();
// Now call the users restart handler
await _restartHandler();
},
shutdownError: async (code = 1, testing = false) => {
bs.error("Heuston, we have a problem. Shutting down now ...");
if (testing) {
// Do a soft stop so we don't force any testing code to exit
await bs.exit(code, false);
return;
}
await bs.exit(code);
},
// Utility functions here
addHttpServer: async (networkInterface, networkPort, httpConfig = {}, startServer = true) => {
let server = new HttpServer(networkInterface, networkPort, httpConfig);
// Automatically start the server if requested
if (startServer) {
await server.start();
}
_httpServerList.push(server);
return server;
},
httpServer: (index = 0) => {
// Check if there are any http servers first
if (_httpServerList.length === 0) {
throw Error(`There are no http servers!!`);
}
// Check if the requested server DOES NOT exist
if (index >= _httpServerList.length) {
throw Error(`There is no http servers with the index ${index}`);
}
return _httpServerList[index];
},
addPlugin: (name, pluginClass, config = {}) => {
// Make sure we don't have a duplicate name
if (_pluginMap.has(name)) {
throw Error(`There is already a plugin with the name ${name}`);
}
// Create the plugin
let plugin = new pluginClass(name, config);
// And then cache the plugin
_pluginMap.set(name, plugin);
return plugin;
},
plugin: (name) => {
// Search for the plugin that has a matching name
let plugin = _pluginMap.get(name);
// Check if the plugin DOES NOT exist
if (plugin === undefined) {
throw Error(`There is no plugin with the name ${name}`);
}
return plugin;
},
save: (name, value) => {
if (_sharedStore.has(name)) {
throw Error(`There is already a value saved with the name ${name}`);
}
_sharedStore.set(name, value);
},
update: (name, value) => {
_sharedStore.set(name, value);
},
retrieve: (name) => {
return _sharedStore.get(name);
},
sleep: async (durationInSeconds) => {
// Convert duration to ms
let ms = Math.round(durationInSeconds * 1000);
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
},
question: async (ask, questionOptions) => {
let input = process.stdin;
let output = process.stdout;
let options = {
muteAnswer: false,
muteChar: "*",
...questionOptions,
};
return new Promise((resolve) => {
let rl = readline.createInterface({
input,
output,
});
if (options.muteAnswer) {
input.on("keypress", () => {
// get the number of characters entered so far:
var len = rl.line.length;
if (options.muteChar.length === 0) {
// move cursor back one since we will always be at the start
readline.moveCursor(output, -1, 0);
// clear everything to the right of the cursor
readline.clearLine(output, 1);
}
else {
// move cursor back to the beginning of the input
readline.moveCursor(output, -len, 0);
// clear everything to the right of the cursor
readline.clearLine(output, 1);
// If there is a muteChar then replace the original input with it
for (var i = 0; i < len; i++) {
// In case the user passes a string just use the 1st char
output.write(options.muteChar[0]);
}
}
});
}
// Insert a space after the question for convience
rl.question(`${ask} `, (answer) => {
resolve(answer);
rl.close();
});
});
},
});
// Private functions here
let logConfigManMsgs = () => {
let messages = configMan.getMessages();
for (let message of messages) {
_logger.startupMsg(message[0]);
}
configMan.clearMessages();
};
function init() {
// Initialise the private variables
_logger = new Logger(LOGGER_APP_NAME);
_httpServerList = [];
_pluginMap = new Map();
_sharedStore = new Map();
// Now spit out the versions
bs.startupMsg(`Bamboo Shell version (${VERSION})`);
bs.startupMsg(`NODE_ENV is (${process.env.NODE_ENV === undefined ? "development" : process.env.NODE_ENV})`);
// Now set up the event handler
bs.startupMsg("Setting up shutdown event handlers ...");
// Call exit() on a Ctrl-C
process.on("SIGINT", _shutdownHandler);
// Call exit() when the program is terminated
process.on("SIGTERM", _shutdownHandler);
// Call exit() during normal programming termination
process.on("beforeExit", _shutdownHandler);
// Catch and log any execptions and then call exit()
process.on("uncaughtException", _exceptionHandler);
// Call resatrt() on a HUP signal
process.on("SIGHUP", bs.restart);
// And it's party time!
bs.startupMsg("Ready to Rock and Roll baby!");
}
// OK - lets light this candle!
init();
export { BSPlugin, ConfigError, HttpConfigError, HttpError, HttpRedirect, HttpServer, LogLevel, Logger, ReqAborted, ReqError, Router, ServerRequest, ServerResponse, SseServer, bs };
//# sourceMappingURL=shell.mjs.map