routup
Version:
Routup is a minimalistic http based routing framework.
1,614 lines (1,545 loc) • 71.7 kB
JavaScript
import { merge, hasOwnProperty, distinctArray } from 'smob';
import { Buffer } from 'buffer';
import { subtle } from 'uncrypto';
import { compile, all } from 'proxy-addr';
import { getType, get } from 'mime-explorer';
import Negotiator from 'negotiator';
import { Readable, PassThrough, Writable } from 'readable-stream';
import { HTTPError } from '@ebec/http';
import { pathToRegexp } from 'path-to-regexp';
var MethodName = /*#__PURE__*/ function(MethodName) {
MethodName["GET"] = "GET";
MethodName["POST"] = "POST";
MethodName["PUT"] = "PUT";
MethodName["PATCH"] = "PATCH";
MethodName["DELETE"] = "DELETE";
MethodName["OPTIONS"] = "OPTIONS";
MethodName["HEAD"] = "HEAD";
return MethodName;
}({});
var HeaderName = /*#__PURE__*/ function(HeaderName) {
HeaderName["ACCEPT"] = "accept";
HeaderName["ACCEPT_CHARSET"] = "accept-charset";
HeaderName["ACCEPT_ENCODING"] = "accept-encoding";
HeaderName["ACCEPT_LANGUAGE"] = "accept-language";
HeaderName["ACCEPT_RANGES"] = "accept-ranges";
HeaderName["ALLOW"] = "allow";
HeaderName["CACHE_CONTROL"] = "cache-control";
HeaderName["CONTENT_DISPOSITION"] = "content-disposition";
HeaderName["CONTENT_ENCODING"] = "content-encoding";
HeaderName["CONTENT_LENGTH"] = "content-length";
HeaderName["CONTENT_RANGE"] = "content-range";
HeaderName["CONTENT_TYPE"] = "content-type";
HeaderName["CONNECTION"] = "connection";
HeaderName["COOKIE"] = "cookie";
HeaderName["ETag"] = "etag";
HeaderName["HOST"] = "host";
HeaderName["IF_MODIFIED_SINCE"] = "if-modified-since";
HeaderName["IF_NONE_MATCH"] = "if-none-match";
HeaderName["LAST_MODIFIED"] = "last-modified";
HeaderName["LOCATION"] = "location";
HeaderName["RANGE"] = "range";
HeaderName["RATE_LIMIT_LIMIT"] = "ratelimit-limit";
HeaderName["RATE_LIMIT_REMAINING"] = "ratelimit-remaining";
HeaderName["RATE_LIMIT_RESET"] = "ratelimit-reset";
HeaderName["RETRY_AFTER"] = "retry-after";
HeaderName["SET_COOKIE"] = "set-cookie";
HeaderName["TRANSFER_ENCODING"] = "transfer-encoding";
HeaderName["X_ACCEL_BUFFERING"] = "x-accel-buffering";
HeaderName["X_FORWARDED_HOST"] = "x-forwarded-host";
HeaderName["X_FORWARDED_FOR"] = "x-forwarded-for";
HeaderName["X_FORWARDED_PROTO"] = "x-forwarded-proto";
return HeaderName;
}({});
function isRequestCacheable(req, modifiedTime) {
const modifiedSince = req.headers[HeaderName.IF_MODIFIED_SINCE];
if (!modifiedSince) {
return false;
}
modifiedTime = typeof modifiedTime === 'string' ? new Date(modifiedTime) : modifiedTime;
return new Date(modifiedSince) >= modifiedTime;
}
/*
Set-Cookie header field-values are sometimes comma joined in one string. This splits them without choking on commas
that are within a single set-cookie field-value, such as in the Expires portion.
This is uncommon, but explicitly allowed - see https://tools.ietf.org/html/rfc2616#section-4.2
Node.js does this for every header *except* set-cookie - see https://github.com/nodejs/node/blob/d5e363b77ebaf1caf67cd7528224b651c86815c1/lib/_http_incoming.js#L128
React Native's fetch does this for *every* header, including set-cookie.
Based on: https://github.com/google/j2objc/commit/16820fdbc8f76ca0c33472810ce0cb03d20efe25
Credits to: https://github.com/tomball for original and https://github.com/chrusart for JavaScript implementation
*/ function splitCookiesString(input) {
if (Array.isArray(input)) {
return input.flatMap((el)=>splitCookiesString(el));
}
if (typeof input !== 'string') {
return [];
}
const cookiesStrings = [];
let pos = 0;
let start;
let ch;
let lastComma;
let nextStart;
let cookiesSeparatorFound;
const skipWhitespace = ()=>{
while(pos < input.length && /\s/.test(input.charAt(pos))){
pos += 1;
}
return pos < input.length;
};
const notSpecialChar = ()=>{
ch = input.charAt(pos);
return ch !== '=' && ch !== ';' && ch !== ',';
};
while(pos < input.length){
start = pos;
cookiesSeparatorFound = false;
while(skipWhitespace()){
ch = input.charAt(pos);
if (ch === ',') {
// ',' is a cookie separator if we have later first '=', not ';' or ','
lastComma = pos;
pos += 1;
skipWhitespace();
nextStart = pos;
while(pos < input.length && notSpecialChar()){
pos += 1;
}
// currently special character
if (pos < input.length && input.charAt(pos) === '=') {
// we found cookies separator
cookiesSeparatorFound = true;
// pos is inside the next cookie, so back up and return it.
pos = nextStart;
cookiesStrings.push(input.substring(start, lastComma));
start = pos;
} else {
// in param ',' or param separator ';',
// we continue from that comma
pos = lastComma + 1;
}
} else {
pos += 1;
}
}
if (!cookiesSeparatorFound || pos >= input.length) {
cookiesStrings.push(input.substring(start, input.length));
}
}
return cookiesStrings;
}
function isObject(item) {
return !!item && typeof item === 'object' && !Array.isArray(item);
}
function setProperty(record, property, value) {
record[property] = value;
}
function getProperty(req, property) {
return req[property];
}
/**
* Determine if object is a Stats object.
*
* @param {object} obj
* @return {boolean}
* @api private
*/ function isStatsObject(obj) {
// quack quack
return isObject(obj) && 'ctime' in obj && Object.prototype.toString.call(obj.ctime) === '[object Date]' && 'mtime' in obj && Object.prototype.toString.call(obj.mtime) === '[object Date]' && 'ino' in obj && typeof obj.ino === 'number' && 'size' in obj && typeof obj.size === 'number';
}
async function sha1(str) {
const enc = new TextEncoder();
const hash = await subtle.digest('SHA-1', enc.encode(str));
return btoa(String.fromCharCode(...new Uint8Array(hash)));
}
/**
* Generate an ETag.
*/ async function generateETag(input) {
if (isStatsObject(input)) {
const mtime = input.mtime.getTime().toString(16);
const size = input.size.toString(16);
return `"${size}-${mtime}"`;
}
if (input.length === 0) {
// fast-path empty
return '"0-2jmj7l5rSw0yVb/vlWAYkK/YBwk"';
}
const entity = Buffer.isBuffer(input) ? input.toString('utf-8') : input;
// compute hash of entity
const hash = await sha1(entity);
return `"${entity.length.toString(16)}-${hash.substring(0, 27)}"`;
}
/**
* Create a simple ETag.
*/ async function createEtag(input, options) {
options = options || {};
const weak = typeof options.weak === 'boolean' ? options.weak : isStatsObject(input);
// generate entity tag
const tag = await generateETag(input);
return weak ? `W/${tag}` : tag;
}
function buildEtagFn(input) {
if (typeof input === 'function') {
return input;
}
input = input ?? true;
if (input === false) {
return ()=>Promise.resolve(undefined);
}
let options = {
weak: true
};
if (isObject(input)) {
options = merge(input, options);
}
return async (body, encoding, size)=>{
const buff = Buffer.isBuffer(body) ? body : Buffer.from(body, encoding);
if (typeof options.threshold !== 'undefined') {
size = size ?? Buffer.byteLength(buff);
if (size <= options.threshold) {
return undefined;
}
}
return createEtag(buff, options);
};
}
function buildTrustProxyFn(input) {
if (typeof input === 'function') {
return input;
}
if (input === true) {
return ()=>true;
}
if (typeof input === 'number') {
return (_address, hop)=>hop < input;
}
if (typeof input === 'string') {
input = input.split(',').map((value)=>value.trim());
}
return compile(input || []);
}
function isInstance(input, sym) {
if (!isObject(input)) {
return false;
}
return input['@instanceof'] === sym;
}
function getMimeType(type) {
if (type.indexOf('/') !== -1) {
return type;
}
return getType(type);
}
function getCharsetForMimeType(type) {
if (/^text\/|^application\/(javascript|json)/.test(type)) {
return 'utf-8';
}
const meta = get(type);
if (meta && meta.charset) {
return meta.charset.toLowerCase();
}
return undefined;
}
function toMethodName(input, alt) {
if (input) {
return input.toUpperCase();
}
return alt;
}
const nextPlaceholder = (_err)=>{};
/**
* Based on https://github.com/unjs/pathe v1.1.1 (055f50a6f1131f4e5c56cf259dd8816168fba329)
*/ function normalizeWindowsPath(input = '') {
if (!input || !input.includes('\\')) {
return input;
}
return input.replace(/\\/g, '/');
}
const EXTNAME_RE = /.(\.[^./]+)$/;
function extname(input) {
const match = EXTNAME_RE.exec(normalizeWindowsPath(input));
return match && match[1] || '';
}
function basename(input, extension) {
const lastSegment = normalizeWindowsPath(input).split('/').pop();
if (!lastSegment) {
return input;
}
return lastSegment;
}
function isPromise(p) {
return isObject(p) && (p instanceof Promise || // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
typeof p.then === 'function');
}
function isNodeStream(input) {
return isObject(input) && typeof input.pipe === 'function' && typeof input.read === 'function';
}
function isWebStream(input) {
return isObject(input) && typeof input.pipeTo === 'function';
}
function isStream(data) {
return isNodeStream(data) || isWebStream(data);
}
const TRAILING_SLASH_RE = /\/$|\/\?/;
function hasTrailingSlash(input = '', queryParams = false) {
if (!queryParams) {
return input.endsWith('/');
}
return TRAILING_SLASH_RE.test(input);
}
function withoutTrailingSlash(input = '', queryParams = false) {
if (!queryParams) {
return (hasTrailingSlash(input) ? input.slice(0, -1) : input) || '/';
}
if (!hasTrailingSlash(input, true)) {
return input || '/';
}
const [s0, ...s] = input.split('?');
return (s0.slice(0, -1) || '/') + (s.length ? `?${s.join('?')}` : '');
}
function hasLeadingSlash(input = '') {
return input.startsWith('/');
}
function withLeadingSlash(input = '') {
return hasLeadingSlash(input) ? input : `/${input}`;
}
function cleanDoubleSlashes(input = '') {
if (input.indexOf('://') !== -1) {
return input.split('://').map((str)=>cleanDoubleSlashes(str)).join('://');
}
return input.replace(/\/+/g, '/');
}
function isWebBlob(input) {
return typeof Blob !== 'undefined' && input instanceof Blob;
}
function isWebResponse(input) {
return typeof Response !== 'undefined' && input instanceof Response;
}
const symbol$4 = Symbol.for('ReqEnv');
function setRequestEnv(req, key, value) {
const propertyValue = getProperty(req, symbol$4);
if (propertyValue) {
if (typeof key === 'object') {
if (value) {
setProperty(req, symbol$4, merge(propertyValue, key));
} else {
setProperty(req, symbol$4, key);
}
} else {
propertyValue[key] = value;
setProperty(req, symbol$4, propertyValue);
}
return;
}
if (typeof key === 'object') {
setProperty(req, symbol$4, key);
return;
}
setProperty(req, symbol$4, {
[key]: value
});
}
function useRequestEnv(req, key) {
const propertyValue = getProperty(req, symbol$4);
if (propertyValue) {
if (typeof key !== 'undefined') {
return propertyValue[key];
}
return propertyValue;
}
if (typeof key !== 'undefined') {
return undefined;
}
return {};
}
function unsetRequestEnv(req, key) {
const propertyValue = getProperty(req, symbol$4);
if (hasOwnProperty(propertyValue, key)) {
delete propertyValue[key];
}
}
function getRequestHeader(req, name) {
return req.headers[name];
}
function setRequestHeader(req, name, value) {
req.headers[name] = value;
}
const symbol$3 = Symbol.for('ReqNegotiator');
function useRequestNegotiator(req) {
let value = getProperty(req, symbol$3);
if (value) {
return value;
}
value = new Negotiator(req);
setProperty(req, symbol$3, value);
return value;
}
function getRequestAcceptableContentTypes(req) {
const negotiator = useRequestNegotiator(req);
return negotiator.mediaTypes();
}
function getRequestAcceptableContentType(req, input) {
input = input || [];
const items = Array.isArray(input) ? input : [
input
];
if (items.length === 0) {
return getRequestAcceptableContentTypes(req).shift();
}
const header = getRequestHeader(req, HeaderName.ACCEPT);
if (!header) {
return items[0];
}
let polluted = false;
const mimeTypes = [];
for(let i = 0; i < items.length; i++){
const mimeType = getMimeType(items[i]);
if (mimeType) {
mimeTypes.push(mimeType);
} else {
polluted = true;
}
}
const negotiator = useRequestNegotiator(req);
const matches = negotiator.mediaTypes(mimeTypes);
if (matches.length > 0) {
if (polluted) {
return items[0];
}
return items[mimeTypes.indexOf(matches[0])];
}
return undefined;
}
function getRequestAcceptableCharsets(req) {
const negotiator = useRequestNegotiator(req);
return negotiator.charsets();
}
function getRequestAcceptableCharset(req, input) {
input = input || [];
const items = Array.isArray(input) ? input : [
input
];
if (items.length === 0) {
return getRequestAcceptableCharsets(req).shift();
}
const negotiator = useRequestNegotiator(req);
return negotiator.charsets(items).shift() || undefined;
}
function getRequestAcceptableEncodings(req) {
const negotiator = useRequestNegotiator(req);
return negotiator.encodings();
}
function getRequestAcceptableEncoding(req, input) {
input = input || [];
const items = Array.isArray(input) ? input : [
input
];
if (items.length === 0) {
return getRequestAcceptableEncodings(req).shift();
}
const negotiator = useRequestNegotiator(req);
return negotiator.encodings(items).shift() || undefined;
}
function getRequestAcceptableLanguages(req) {
const negotiator = useRequestNegotiator(req);
return negotiator.languages();
}
function getRequestAcceptableLanguage(req, input) {
input = input || [];
const items = Array.isArray(input) ? input : [
input
];
if (items.length === 0) {
return getRequestAcceptableLanguages(req).shift();
}
const negotiator = useRequestNegotiator(req);
return negotiator.languages(items).shift() || undefined;
}
function matchRequestContentType(req, contentType) {
const header = getRequestHeader(req, HeaderName.CONTENT_TYPE);
if (!header) {
return true;
}
/* istanbul ignore next */ if (Array.isArray(header)) {
if (header.length === 0) {
return true;
}
return header[0] === getMimeType(contentType);
}
return header.split('; ').shift() === getMimeType(contentType);
}
const defaults = {
trustProxy: ()=>false,
subdomainOffset: 2,
etag: buildEtagFn(),
proxyIpMax: 0
};
const instances = {};
function setRouterOptions(id, input) {
instances[id] = input;
}
function findRouterOption(key, path) {
if (!path || path.length === 0) {
return defaults[key];
}
if (path.length > 0) {
for(let i = path.length; i >= 0; i--){
if (hasOwnProperty(instances, path[i]) && typeof instances[path[i]][key] !== 'undefined') {
return instances[path[i]][key];
}
}
}
return defaults[key];
}
const routerSymbol = Symbol.for('ReqRouterID');
function setRequestRouterPath(req, path) {
setProperty(req, routerSymbol, path);
}
function useRequestRouterPath(req) {
return getProperty(req, routerSymbol);
}
function getRequestHostName(req, options) {
options = options || {};
let trustProxy;
if (typeof options.trustProxy !== 'undefined') {
trustProxy = buildTrustProxyFn(options.trustProxy);
} else {
trustProxy = findRouterOption('trustProxy', useRequestRouterPath(req));
}
let hostname = req.headers[HeaderName.X_FORWARDED_HOST];
if (!hostname || !req.socket.remoteAddress || !trustProxy(req.socket.remoteAddress, 0)) {
hostname = req.headers[HeaderName.HOST];
} else {
hostname = Array.isArray(hostname) ? hostname.pop() : hostname;
if (hostname && hostname.indexOf(',') !== -1) {
hostname = hostname.substring(0, hostname.indexOf(',')).trimEnd();
}
}
if (!hostname) {
return undefined;
}
// IPv6 literal support
const offset = hostname[0] === '[' ? hostname.indexOf(']') + 1 : 0;
const index = hostname.indexOf(':', offset);
return index !== -1 ? hostname.substring(0, index) : hostname;
}
function isRequestHTTP2(req) {
return typeof getRequestHeader(req, ':path') !== 'undefined' && typeof getRequestHeader(req, ':method') !== 'undefined';
}
function getRequestIP(req, options) {
options = options || {};
let trustProxy;
if (typeof options.trustProxy !== 'undefined') {
trustProxy = buildTrustProxyFn(options.trustProxy);
} else {
trustProxy = findRouterOption('trustProxy', useRequestRouterPath(req));
}
const addrs = all(req, trustProxy);
return addrs[addrs.length - 1];
}
const symbol$2 = Symbol.for('ReqMountPath');
function useRequestMountPath(req) {
return getProperty(req, symbol$2) || '/';
}
function setRequestMountPath(req, basePath) {
setProperty(req, symbol$2, basePath);
}
const symbol$1 = Symbol.for('ReqParams');
function useRequestParams(req) {
return getProperty(req, symbol$1) || getProperty(req, 'params') || {};
}
function useRequestParam(req, key) {
return useRequestParams(req)[key];
}
function setRequestParams(req, data) {
setProperty(req, symbol$1, data);
}
function setRequestParam(req, key, value) {
const params = useRequestParams(req);
params[key] = value;
setRequestParams(req, params);
}
const PathSymbol = Symbol.for('ReqPath');
function useRequestPath(req) {
const path = getProperty(req, 'path') || getProperty(req, PathSymbol);
if (path) {
return path;
}
if (typeof req.url === 'undefined') {
return '/';
}
const parsed = new URL(req.url, 'http://localhost/');
setProperty(req, PathSymbol, parsed.pathname);
return parsed.pathname;
}
function getRequestProtocol(req, options) {
options = options || {};
let trustProxy;
if (typeof options.trustProxy !== 'undefined') {
trustProxy = buildTrustProxyFn(options.trustProxy);
} else {
trustProxy = findRouterOption('trustProxy', useRequestRouterPath(req));
}
let protocol = options.default;
/* istanbul ignore next */ if (hasOwnProperty(req.socket, 'encrypted') && !!req.socket.encrypted) {
protocol = 'https';
} else if (!protocol) {
protocol = 'http';
}
if (!req.socket.remoteAddress || !trustProxy(req.socket.remoteAddress, 0)) {
return protocol;
}
let header = req.headers[HeaderName.X_FORWARDED_PROTO];
/* istanbul ignore next */ if (Array.isArray(header)) {
header = header.pop();
}
if (!header) {
return protocol;
}
const index = header.indexOf(',');
return index !== -1 ? header.substring(0, index).trim() : header.trim();
}
function createRequest(context) {
let readable;
if (context.body) {
if (isWebStream(context.body)) {
readable = Readable.fromWeb(context.body);
} else {
readable = Readable.from(context.body);
}
} else {
readable = new Readable();
}
const headers = context.headers || {};
const rawHeaders = [];
let keys = Object.keys(headers);
for(let i = 0; i < keys.length; i++){
const header = headers[keys[i]];
if (Array.isArray(header)) {
for(let j = 0; j < header.length; j++){
rawHeaders.push(keys[i], header[j]);
}
} else if (typeof header === 'string') {
rawHeaders.push(keys[i], header);
}
}
const headersDistinct = {};
keys = Object.keys(headers);
for(let i = 0; i < keys.length; i++){
const header = headers[keys[i]];
if (Array.isArray(header)) {
headersDistinct[keys[i]] = header;
}
if (typeof header === 'string') {
headersDistinct[keys[i]] = [
header
];
}
}
Object.defineProperty(readable, 'connection', {
get () {
return {
remoteAddress: '127.0.0.1'
};
}
});
Object.defineProperty(readable, 'socket', {
get () {
return {
remoteAddress: '127.0.0.1'
};
}
});
Object.assign(readable, {
aborted: false,
complete: true,
headers,
headersDistinct,
httpVersion: '1.1',
httpVersionMajor: 1,
httpVersionMinor: 1,
method: context.method || 'GET',
rawHeaders,
rawTrailers: [],
trailers: {},
trailersDistinct: {},
url: context.url || '/',
setTimeout (_msecs, _callback) {
return this;
}
});
return readable;
}
class RoutupError extends HTTPError {
}
function isError(input) {
return input instanceof RoutupError;
}
/**
* Create an internal error object by
* - an existing error (accessible via cause property)
* - options
* - message
*
* @param input
*/ function createError(input) {
if (isError(input)) {
return input;
}
if (typeof input === 'string') {
return new RoutupError(input);
}
if (!isObject(input)) {
return new RoutupError();
}
return new RoutupError({
cause: input
}, input);
}
function setResponseCacheHeaders(res, options) {
options = options || {};
const cacheControls = [
'public'
].concat(options.cacheControls || []);
if (options.maxAge !== undefined) {
cacheControls.push(`max-age=${+options.maxAge}`, `s-maxage=${+options.maxAge}`);
}
if (options.modifiedTime) {
const modifiedTime = typeof options.modifiedTime === 'string' ? new Date(options.modifiedTime) : options.modifiedTime;
res.setHeader('last-modified', modifiedTime.toUTCString());
}
res.setHeader('cache-control', cacheControls.join(', '));
}
const symbol = Symbol.for('ResGone');
function isResponseGone(res) {
if (res.headersSent || res.writableEnded) {
return true;
}
return getProperty(res, symbol) ?? false;
}
function setResponseGone(res, value) {
setProperty(res, symbol, value);
}
function serializeEventStreamMessage(message) {
let result = '';
if (message.id) {
result += `id: ${message.id}\n`;
}
if (message.event) {
result += `event: ${message.event}\n`;
}
if (typeof message.retry === 'number' && Number.isInteger(message.retry)) {
result += `retry: ${message.retry}\n`;
}
result += `data: ${message.data}\n\n`;
return result;
}
class EventStream {
open() {
this.response.req.on('close', ()=>this.end());
this.response.req.on('error', (err)=>{
this.emit('error', err);
this.end();
});
this.passThrough.on('data', (chunk)=>this.response.write(chunk));
this.passThrough.on('error', (err)=>{
this.emit('error', err);
this.end();
});
this.passThrough.on('close', ()=>this.end());
this.response.setHeader(HeaderName.CONTENT_TYPE, 'text/event-stream');
this.response.setHeader(HeaderName.CACHE_CONTROL, 'private, no-cache, no-store, no-transform, must-revalidate, max-age=0');
this.response.setHeader(HeaderName.X_ACCEL_BUFFERING, 'no');
if (!isRequestHTTP2(this.response.req)) {
this.response.setHeader(HeaderName.CONNECTION, 'keep-alive');
}
this.response.statusCode = 200;
}
write(message) {
if (typeof message === 'string') {
this.write({
data: message
});
return;
}
if (!this.passThrough.closed && this.passThrough.writable) {
this.passThrough.write(serializeEventStreamMessage(message));
}
}
end() {
if (this.flushed) return;
this.flushed = true;
if (!this.passThrough.closed) {
this.passThrough.end();
}
this.emit('close');
setResponseGone(this.response, true);
this.response.end();
}
on(event, listener) {
if (typeof this.eventHandlers[event] === 'undefined') {
this.eventHandlers[event] = [];
}
this.eventHandlers[event].push(listener);
}
emit(event, ...args) {
if (typeof this.eventHandlers[event] === 'undefined') {
return;
}
const listeners = this.eventHandlers[event].slice();
for(let i = 0; i < listeners.length; i++){
listeners[i].apply(this, args);
}
}
constructor(response){
this.response = response;
this.passThrough = new PassThrough({
encoding: 'utf-8'
});
this.flushed = false;
this.eventHandlers = {};
this.open();
}
}
function createEventStream(response) {
return new EventStream(response);
}
function appendResponseHeader(res, name, value) {
let header = res.getHeader(name);
if (!header) {
res.setHeader(name, value);
return;
}
if (!Array.isArray(header)) {
header = [
header.toString()
];
}
res.setHeader(name, [
...header,
value
]);
}
function appendResponseHeaderDirective(res, name, value) {
let header = res.getHeader(name);
if (!header) {
if (Array.isArray(value)) {
res.setHeader(name, value.join('; '));
return;
}
res.setHeader(name, value);
return;
}
if (!Array.isArray(header)) {
if (typeof header === 'string') {
// split header by directive(s)
header = header.split('; ');
}
if (typeof header === 'number') {
header = [
header.toString()
];
}
}
if (Array.isArray(value)) {
header.push(...value);
} else {
header.push(`${value}`);
}
header = [
...new Set(header)
];
res.setHeader(name, header.join('; '));
}
function setResponseContentTypeByFileName(res, fileName) {
const ext = extname(fileName);
if (ext) {
let type = getMimeType(ext.substring(1));
if (type) {
const charset = getCharsetForMimeType(type);
if (charset) {
type += `; charset=${charset}`;
}
res.setHeader(HeaderName.CONTENT_TYPE, type);
}
}
}
function setResponseHeaderAttachment(res, filename) {
if (typeof filename === 'string') {
setResponseContentTypeByFileName(res, filename);
}
res.setHeader(HeaderName.CONTENT_DISPOSITION, `attachment${filename ? `; filename="${filename}"` : ''}`);
}
function setResponseHeaderContentType(res, input, ifNotExists) {
if (ifNotExists) {
const header = res.getHeader(HeaderName.CONTENT_TYPE);
if (header) {
return;
}
}
const contentType = getMimeType(input);
if (contentType) {
res.setHeader(HeaderName.CONTENT_TYPE, contentType);
}
}
async function sendStream(res, stream, next) {
if (isWebStream(stream)) {
return stream.pipeTo(new WritableStream({
write (chunk) {
res.write(chunk);
}
})).then(()=>{
if (next) {
return next();
}
res.end();
return Promise.resolve();
}).catch((err)=>{
if (next) {
return next(err);
}
return Promise.reject(err);
});
}
return new Promise((resolve, reject)=>{
stream.on('open', ()=>{
stream.pipe(res);
});
/* istanbul ignore next */ stream.on('error', (err)=>{
if (next) {
Promise.resolve().then(()=>next(err)).then(()=>resolve()).catch((e)=>reject(e));
return;
}
res.end();
reject(err);
});
stream.on('close', ()=>{
if (next) {
Promise.resolve().then(()=>next()).then(()=>resolve()).catch((e)=>reject(e));
return;
}
res.end();
resolve();
});
});
}
async function sendWebBlob(res, blob) {
setResponseHeaderContentType(res, blob.type);
await sendStream(res, blob.stream());
}
async function sendWebResponse(res, webResponse) {
if (webResponse.redirected) {
res.setHeader(HeaderName.LOCATION, webResponse.url);
}
if (webResponse.status) {
res.statusCode = webResponse.status;
}
if (webResponse.statusText) {
res.statusMessage = webResponse.statusText;
}
webResponse.headers.forEach((value, key)=>{
if (key === HeaderName.SET_COOKIE) {
res.appendHeader(key, splitCookiesString(value));
} else {
res.setHeader(key, value);
}
});
if (webResponse.body) {
await sendStream(res, webResponse.body);
return Promise.resolve();
}
res.end();
return Promise.resolve();
}
async function send(res, chunk) {
switch(typeof chunk){
case 'string':
{
setResponseHeaderContentType(res, 'html', true);
break;
}
case 'boolean':
case 'number':
case 'object':
{
if (chunk !== null) {
if (chunk instanceof Error) {
throw chunk;
}
if (isStream(chunk)) {
await sendStream(res, chunk);
return;
}
if (isWebBlob(chunk)) {
await sendWebBlob(res, chunk);
return;
}
if (isWebResponse(chunk)) {
await sendWebResponse(res, chunk);
return;
}
if (Buffer.isBuffer(chunk)) {
setResponseHeaderContentType(res, 'bin', true);
} else {
chunk = JSON.stringify(chunk);
setResponseHeaderContentType(res, 'application/json', true);
}
}
break;
}
}
let encoding;
if (typeof chunk === 'string') {
res.setHeader(HeaderName.CONTENT_ENCODING, 'utf-8');
appendResponseHeaderDirective(res, HeaderName.CONTENT_TYPE, 'charset=utf-8');
encoding = 'utf-8';
}
// populate Content-Length
let len;
if (chunk !== undefined && chunk !== null) {
if (Buffer.isBuffer(chunk)) {
// get length of Buffer
len = chunk.length;
} else if (chunk.length < 1000) {
// just calculate length when no ETag + small chunk
len = Buffer.byteLength(chunk, encoding);
} else {
// convert chunk to Buffer and calculate
chunk = Buffer.from(chunk, encoding);
encoding = undefined;
len = chunk.length;
}
res.setHeader(HeaderName.CONTENT_LENGTH, `${len}`);
}
if (typeof len !== 'undefined') {
const etagFn = findRouterOption('etag', useRequestRouterPath(res.req));
const chunkHash = await etagFn(chunk, encoding, len);
if (isResponseGone(res)) {
return;
}
if (typeof chunkHash === 'string') {
res.setHeader(HeaderName.ETag, chunkHash);
if (res.req.headers[HeaderName.IF_NONE_MATCH] === chunkHash) {
res.statusCode = 304;
}
}
}
// strip irrelevant headers
if (res.statusCode === 204 || res.statusCode === 304) {
res.removeHeader(HeaderName.CONTENT_TYPE);
res.removeHeader(HeaderName.CONTENT_LENGTH);
res.removeHeader(HeaderName.TRANSFER_ENCODING);
}
// alter headers for 205
if (res.statusCode === 205) {
res.setHeader(HeaderName.CONTENT_LENGTH, 0);
res.removeHeader(HeaderName.TRANSFER_ENCODING);
}
if (isResponseGone(res)) {
return;
}
if (res.req.method === 'HEAD' || res.req.method === 'head') {
// skip body for HEAD
res.end();
return;
}
if (typeof chunk === 'undefined' || chunk === null) {
res.end();
return;
}
if (typeof encoding !== 'undefined') {
res.end(chunk, encoding);
return;
}
res.end(chunk);
}
function sendAccepted(res, chunk) {
res.statusCode = 202;
res.statusMessage = 'Accepted';
return send(res, chunk);
}
function sendCreated(res, chunk) {
res.statusCode = 201;
res.statusMessage = 'Created';
return send(res, chunk);
}
async function sendFile(res, options, next) {
let stats;
try {
stats = await options.stats();
} catch (e) {
if (next) {
return next(e);
}
if (isResponseGone(res)) {
return Promise.resolve();
}
return Promise.reject(e);
}
const name = options.name || stats.name;
if (name) {
const fileName = basename(name);
if (options.attachment) {
const dispositionHeader = res.getHeader(HeaderName.CONTENT_DISPOSITION);
if (!dispositionHeader) {
setResponseHeaderAttachment(res, fileName);
}
} else {
setResponseContentTypeByFileName(res, fileName);
}
}
const contentOptions = {};
if (stats.size) {
const rangeHeader = res.req.headers[HeaderName.RANGE];
if (rangeHeader) {
const [x, y] = rangeHeader.replace('bytes=', '').split('-');
contentOptions.end = Math.min(parseInt(y, 10) || stats.size - 1, stats.size - 1);
contentOptions.start = parseInt(x, 10) || 0;
if (contentOptions.end >= stats.size) {
contentOptions.end = stats.size - 1;
}
if (contentOptions.start >= stats.size) {
res.setHeader(HeaderName.CONTENT_RANGE, `bytes */${stats.size}`);
res.statusCode = 416;
res.end();
return Promise.resolve();
}
res.setHeader(HeaderName.CONTENT_RANGE, `bytes ${contentOptions.start}-${contentOptions.end}/${stats.size}`);
res.setHeader(HeaderName.CONTENT_LENGTH, contentOptions.end - contentOptions.start + 1);
} else {
res.setHeader(HeaderName.CONTENT_LENGTH, stats.size);
}
res.setHeader(HeaderName.ACCEPT_RANGES, 'bytes');
if (stats.mtime) {
const mtime = new Date(stats.mtime);
res.setHeader(HeaderName.LAST_MODIFIED, mtime.toUTCString());
res.setHeader(HeaderName.ETag, `W/"${stats.size}-${mtime.getTime()}"`);
}
}
try {
const content = await options.content(contentOptions);
if (isStream(content)) {
return await sendStream(res, content, next);
}
return await send(res, content);
} catch (e) {
if (next) {
return next(e);
}
if (isResponseGone(res)) {
return Promise.resolve();
}
return Promise.reject(e);
}
}
function sendFormat(res, input) {
const { default: formatDefault, ...formats } = input;
const contentTypes = Object.keys(formats);
const contentType = getRequestAcceptableContentType(res.req, contentTypes);
if (contentType) {
formats[contentType]();
return;
}
formatDefault();
}
function sendRedirect(res, location, statusCode = 302) {
res.statusCode = statusCode;
res.setHeader('location', location);
const encodedLoc = location.replace(/"/g, '%22');
const html = `<!DOCTYPE html><html><head><meta http-equiv="refresh" content="0; url=${encodedLoc}"></head></html>`;
return send(res, html);
}
function createResponse(request) {
let output;
let encoding;
const write = (chunk, chunkEncoding, callback)=>{
if (typeof chunk !== 'undefined') {
const chunkEncoded = typeof chunk === 'string' ? Buffer.from(chunk, chunkEncoding || encoding || 'utf8') : chunk;
if (typeof output !== 'undefined') {
output = Buffer.concat([
output,
chunkEncoded
]);
} else {
output = chunkEncoded;
}
}
encoding = chunkEncoding;
if (callback) {
callback();
}
};
const writable = new Writable({
decodeStrings: false,
write (chunk, arg2, arg3) {
const chunkEncoding = typeof arg2 === 'string' ? encoding : 'utf-8';
let cb;
if (typeof arg2 === 'function') {
cb = arg2;
} else if (typeof arg3 === 'function') {
cb = arg3;
}
write(chunk, chunkEncoding, cb);
return true;
}
});
Object.defineProperty(writable, 'body', {
get () {
if (output) {
const arrayBuffer = new ArrayBuffer(output.length);
const view = new Uint8Array(arrayBuffer);
for(let i = 0; i < output.length; ++i){
view[i] = output[i];
}
return arrayBuffer;
}
return new ArrayBuffer(0);
}
});
const headers = {};
Object.assign(writable, {
req: request,
chunkedEncoding: false,
connection: null,
headersSent: false,
sendDate: false,
shouldKeepAlive: false,
socket: null,
statusCode: 200,
statusMessage: '',
strictContentLength: false,
useChunkedEncodingByDefault: false,
finished: false,
addTrailers (_headers) {},
appendHeader (name, value) {
if (name === HeaderName.SET_COOKIE) {
value = splitCookiesString(value);
}
name = name.toLowerCase();
const current = headers[name];
const all = [
...Array.isArray(current) ? current : [
current
],
...Array.isArray(value) ? value : [
value
]
].filter(Boolean);
headers[name] = all.length > 1 ? all : all[0];
return this;
},
assignSocket (_socket) {},
detachSocket (_socket) {},
flushHeaders () {},
getHeader (name) {
return headers[name.toLowerCase()];
},
getHeaderNames () {
return Object.keys(headers);
},
getHeaders () {
return headers;
},
hasHeader (name) {
return hasOwnProperty(headers, name.toLowerCase());
},
removeHeader (name) {
delete headers[name.toLowerCase()];
},
setHeader (name, value) {
if (name === HeaderName.SET_COOKIE && typeof value !== 'number') {
value = splitCookiesString(value);
}
headers[name.toLowerCase()] = value;
return this;
},
setHeaders (headers) {
if (headers instanceof Map) {
headers.entries().forEach(([key, value])=>{
this.setHeader(key, value);
});
return this;
}
headers.forEach((value, key)=>{
this.setHeader(key, value);
});
return this;
},
setTimeout (_msecs, _callback) {
return this;
},
writeContinue (_callback) {},
writeEarlyHints (_hints, callback) {
if (typeof callback !== 'undefined') {
callback();
}
},
writeProcessing () {},
writeHead (statusCode, arg1, arg2) {
this.statusCode = statusCode;
if (typeof arg1 === 'string') {
this.statusMessage = arg1;
arg1 = undefined;
}
const headers = arg2 || arg1;
if (headers) {
if (Array.isArray(headers)) {
for(let i = 0; i < headers.length; i++){
const keys = Object.keys(headers[i]);
for(let j = 0; j < keys.length; j++){
this.setHeader(keys[i], headers[i][keys[j]]);
}
}
} else {
const keys = Object.keys(headers);
for(let i = 0; i < keys.length; i++){
this.setHeader(keys[i], headers[keys[i]]);
}
}
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.headersSent = true;
return this;
}
});
return writable;
}
function dispatch(event, target) {
setRequestParams(event.request, event.params);
setRequestMountPath(event.request, event.mountPath);
setRequestRouterPath(event.request, event.routerPath);
return new Promise((resolve, reject)=>{
let handled = false;
const unsubscribe = ()=>{
event.response.off('close', done);
event.response.off('error', done);
};
const shutdown = (dispatched, err)=>{
if (handled) {
return;
}
handled = true;
unsubscribe();
if (err) {
reject(createError(err));
} else {
resolve(dispatched);
}
};
const done = (err)=>shutdown(true, err);
const next = (err)=>shutdown(false, err);
event.response.once('close', done);
event.response.once('error', done);
const handle = async (data)=>{
if (typeof data === 'undefined' || handled) {
return false;
}
handled = true;
unsubscribe();
if (!event.dispatched) {
await send(event.response, data);
}
return true;
};
try {
const output = target(next);
if (isPromise(output)) {
output.then((r)=>handle(r)).then((resolved)=>{
if (resolved) {
resolve(true);
}
}).catch((e)=>reject(createError(e)));
return;
}
Promise.resolve().then(()=>handle(output)).then((resolved)=>{
if (resolved) {
resolve(true);
}
}).catch((e)=>reject(createError(e)));
} catch (error) {
next(error);
}
});
}
function isDispatcherErrorEvent(event) {
return typeof event.error !== 'undefined';
}
class DispatchEvent {
get dispatched() {
return this._dispatched || this.response.writableEnded || this.response.headersSent;
}
set dispatched(value) {
this._dispatched = value;
}
constructor(context){
this.request = context.request;
this.response = context.response;
this.method = context.method || MethodName.GET;
this.methodsAllowed = [];
this.mountPath = '/';
this.params = {};
this.path = context.path || '/';
this.routerPath = [];
this.next = nextPlaceholder;
}
}
class DispatchErrorEvent extends DispatchEvent {
}
async function dispatchNodeRequest(router, request, response) {
const event = new DispatchEvent({
request,
response,
path: useRequestPath(request),
method: toMethodName(request.method, MethodName.GET)
});
await router.dispatch(event);
if (event.dispatched) {
return;
}
if (event.error) {
event.response.statusCode = event.error.statusCode;
if (event.error.statusMessage) {
event.response.statusMessage = event.error.statusMessage;
}
event.response.end();
return;
}
event.response.statusCode = 404;
event.response.end();
}
function createNodeDispatcher(router) {
return (req, res)=>{
// eslint-disable-next-line no-void
void dispatchNodeRequest(router, req, res);
};
}
function transformHeaderToTuples(key, value) {
const output = [];
if (Array.isArray(value)) {
for(let j = 0; j < value.length; j++){
output.push([
key,
value[j]
]);
}
} else if (value !== undefined) {
output.push([
key,
String(value)
]);
}
return output;
}
function transformHeadersToTuples(input) {
const output = [];
const keys = Object.keys(input);
for(let i = 0; i < keys.length; i++){
const key = keys[i].toLowerCase();
output.push(...transformHeaderToTuples(key, input[key]));
}
return output;
}
async function dispatchRawRequest(router, request) {
const method = toMethodName(request.method, MethodName.GET);
const req = createRequest({
url: request.path,
method,
body: request.body,
headers: request.headers
});
const res = createResponse(req);
const getHeaders = ()=>{
const output = {};
const headers = res.getHeaders();
const keys = Object.keys(headers);
for(let i = 0; i < keys.length; i++){
const header = headers[keys[i]];
if (typeof header === 'number') {
output[keys[i]] = `${header}`;
} else if (header) {
output[keys[i]] = header;
}
}
return output;
};
const createRawResponse = (input = {})=>({
status: input.status || res.statusCode,
statusMessage: input.statusMessage || res.statusMessage,
headers: getHeaders(),
body: res.body
});
const event = new DispatchEvent({
request: req,
response: res,
path: request.path,
method
});
await router.dispatch(event);
if (event.dispatched) {
return createRawResponse();
}
if (event.error) {
return createRawResponse({
status: event.error.statusCode,
statusMessage: event.error.statusMessage
});
}
return createRawResponse({
status: 404
});
}
function createRawDispatcher(router) {
return async (request)=>dispatchRawRequest(router, request);
}
async function dispatchWebRequest(router, request) {
const url = new URL(request.url);
const headers = {};
request.headers.forEach((value, key)=>{
headers[key] = value;
});
const method = toMethodName(request.method, MethodName.GET);
const res = await dispatchRawRequest(router, {
method,
path: url.pathname + url.search,
headers,
body: request.body
});
let body;
if (method === MethodName.HEAD || res.status === 304 || res.status === 101 || res.status === 204 || res.status === 205) {
body = null;
} else {
body = res.body;
}
return new Response(body, {
headers: transformHeadersToTuples(res.headers),
status: res.status,
statusText: res.statusMessage
});
}
function createWebDispatcher(router) {
return async (request)=>dispatchWebRequest(router, request);
}
var HandlerType = /*#__PURE__*/ function(HandlerType) {
HandlerType["CORE"] = "core";
HandlerType["ERROR"] = "error";
return HandlerType;
}({});
const HandlerSymbol = Symbol.for('Handler');
var HookName = /*#__PURE__*/ function(HookName) {
HookName["ERROR"] = "error";
HookName["DISPATCH_START"] = "dispatchStart";
HookName["DISPATCH_END"] = "dispatchEnd";
HookName["CHILD_MATCH"] = "childMatch";
HookName["CHILD_DISPATCH_BEFORE"] = "childDispatchBefore";
HookName["CHILD_DISPATCH_AFTER"] = "childDispatchAfter";
return HookName;
}({});
class HookManager {
// --------------------------------------------------
addListener(name, fn) {
this.items[name] = this.items[name] || [];
this.items[name].push(fn);
return ()=>{
this.removeListener(name,