UNPKG

routup

Version:

Routup is a minimalistic http based routing framework.

1,611 lines (1,541 loc) 74 kB
'use strict'; var smob = require('smob'); var buffer = require('buffer'); var uncrypto = require('uncrypto'); var proxyAddr = require('proxy-addr'); var mimeExplorer = require('mime-explorer'); var Negotiator = require('negotiator'); var readableStream = require('readable-stream'); var http = require('@ebec/http'); var pathToRegexp = require('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 uncrypto.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.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 = smob.merge(input, options); } return async (body, encoding, size)=>{ const buff = buffer.Buffer.isBuffer(body) ? body : buffer.Buffer.from(body, encoding); if (typeof options.threshold !== 'undefined') { size = size ?? buffer.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 proxyAddr.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 mimeExplorer.getType(type); } function getCharsetForMimeType(type) { if (/^text\/|^application\/(javascript|json)/.test(type)) { return 'utf-8'; } const meta = mimeExplorer.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, smob.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 (smob.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 (smob.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 = proxyAddr.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 (smob.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 = readableStream.Readable.fromWeb(context.body); } else { readable = readableStream.Readable.from(context.body); } } else { readable = new readableStream.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 http.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 readableStream.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.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.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.Buffer.byteLength(chunk, encoding); } else { // convert chunk to Buffer and calculate chunk = buffer.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.Buffer.from(chunk, chunkEncoding || encoding || 'utf8') : chunk; if (typeof output !== 'undefined') { output = buffer.Buffer.concat([ output, chunkEncoded ]); } else { output = chunkEncoded; } } encoding = chunkEncoding; if (callback) { callback(); } }; const writable = new readableStream.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 smob.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 { // ---------------------------------