@bs-core/shell
Version:
The Bamboo Shell
1,282 lines (1,277 loc) • 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",
//