UNPKG

@bs-core/shell

Version:
1,282 lines (1,277 loc) 175 kB
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", //