X-Git-Url: https://git.squeep.com/?a=blobdiff_plain;f=lib%2Fdingus.js;h=dfddaff577312857bdc695820ed768ab680d11bd;hb=281f49e3960d1562b86c56ebb75f340798623c62;hp=f060c18f00d251f610a97929fdce663c9f01de89;hpb=8d101705d3394039ddf2d2f3339a5e6ada911579;p=squeep-api-dingus diff --git a/lib/dingus.js b/lib/dingus.js index f060c18..dfddaff 100644 --- a/lib/dingus.js +++ b/lib/dingus.js @@ -6,10 +6,14 @@ * Just a self-contained router and some request glue. */ +/** + * @typedef {import('node:http')} http + */ + require('./patches'); -const { promises: fsPromises } = require('fs'); -const path = require('path'); -const querystring = require('querystring'); +const { promises: fsPromises } = require('node:fs'); +const path = require('node:path'); +const querystring = require('node:querystring'); const common = require('./common'); const ContentNegotiation = require('./content-negotiation'); const Enum = require('./enum'); @@ -39,17 +43,17 @@ const cookieSplitRE = /; */; class Dingus { /** - * @param {Object} logger object which implements logging methods - * @param {Object} options - * @param {Boolean} options.ignoreTrailingSlash requests for '/foo/' will match a '/foo' route + * @param {object} logger console-like object which implements logging methods + * @param {object} options options + * @param {boolean} options.ignoreTrailingSlash requests for '/foo/' will match a '/foo' route * @param {string} options.proxyPrefix leading part of url path to strip - * @param {Boolean} options.strictAccept whether to error on unsupported Accept type + * @param {boolean} options.strictAccept whether to error on unsupported Accept type * @param {string} options.selfBaseUrl for constructing links - * @param {Boolean} options.staticMetadata serve static headers with static files - * @param {Boolean} options.trustProxy trust some header data to be provided by proxy - * @param {Boolean} options.intrinsicHeadMethod handle HEAD requests automatically if not specified as a route method - * @param {Boolean} options.intrinsicHeadPersistBody include un-sent body on ctx for automatic HEAD requests - * @param {Object} options.querystring alternate qs parser to use + * @param {boolean} options.staticMetadata serve static headers with static files + * @param {boolean} options.trustProxy trust some header data to be provided by proxy + * @param {boolean} options.intrinsicHeadMethod handle HEAD requests automatically if not specified as a route method + * @param {boolean} options.intrinsicHeadPersistBody include un-sent body on ctx for automatic HEAD requests + * @param {object} options.querystring alternate qs parser to use */ constructor(logger = console, options = {}) { common.setOptions(this, defaultOptions, options); @@ -73,6 +77,7 @@ class Dingus { /** * Resolve relative and empty paths in url * @param {string} p path + * @returns {string} normalized path */ _normalizePath(p) { const pathNorm = path.normalize(p); // This isn't perfectly correct, but it's easy... @@ -84,6 +89,7 @@ class Dingus { * Remove a leading portion of url path * N.B. This method gets obliterated if there is no prefix defined at construction * @param {string} p path + * @returns {string} de-prefixed path */ _stripPrefix(p) { if (p.startsWith(this.proxyPrefix)) { @@ -95,7 +101,8 @@ class Dingus { /** * Returns the path part, and querystring object, from a request url. - * @param {string} url + * @param {string} url url + * @returns {object} path string and query params */ _splitUrl(url) { const [ p, qs ] = common.splitFirst(url, '?'); @@ -108,12 +115,13 @@ class Dingus { /** * Insert a new path handler - * @param {string} method - * @param {string} urlPath - * @param {fn} handler + * @param {string|string[]} methods methods to match for this path, '*' allowed + * @param {string} urlPath request path to match + * @param {Function} handler handler to invoke on match + * @param {any[]} handlerArgs additional arguments for handler */ - on(method, urlPath, handler, ...handlerArgs) { - this.router.on(method, urlPath, handler, handlerArgs); + on(methods, urlPath, handler, ...handlerArgs) { + this.router.on(methods, urlPath, handler, handlerArgs); } @@ -123,9 +131,10 @@ class Dingus { * Sets requestId on ctx to a new uuid. * If X-Request-Id or X-Correlation-Id exist on incoming headers, sets them * on outgoing headers and sets on ctx. - * @param {http.ClientRequest} req - * @param {http.ServerResponse} res - * @param {object} ctx + * @param {http.ClientRequest} req request + * @param {http.ServerResponse} res response + * @param {object} ctx context + * @returns {string} id added request */ static tagContext(req, res, ctx) { const requestId = common.requestId(); @@ -144,7 +153,8 @@ class Dingus { /** * - * @param {http.ClientRequest} req + * @param {http.ClientRequest} req request + * @returns {string} first forwarded header ip */ _getAddress(req) { // TODO: RFC7239 Forwarded support @@ -158,7 +168,8 @@ class Dingus { /** * - * @param {http.ClientRequest} req + * @param {http.ClientRequest} req request + * @returns {string} first forwarded header protocol */ _getProtocol(req) { // TODO: RFC7239 Forwarded support @@ -170,9 +181,9 @@ class Dingus { /** * Sets ctx.clientAddress and ctx.clientProtocol. - * @param {http.ClientRequest} req - * @param {http.ServerResponse} res - * @param {object} ctx + * @param {http.ClientRequest} req request + * @param {http.ServerResponse} res response + * @param {object} ctx context */ clientAddressContext(req, res, ctx) { ctx.clientAddress = this._getAddress(req); @@ -182,9 +193,9 @@ class Dingus { /** * Sets ctx.cookie from Cookie header. - * @param {http.ClientRequest} req - * @param {http.ServerResponse} res - * @param {object} ctx + * @param {http.ClientRequest} req request + * @param {http.ServerResponse} res response + * @param {object} ctx context */ static ingestCookie(req, res, ctx) { ctx.cookie = {}; @@ -207,9 +218,9 @@ class Dingus { /** * Called before every request handler. * Sets tracking identifiers and client information on ctx. - * @param {http.ClientRequest} req - * @param {http.ServerResponse} res - * @param {object} ctx + * @param {http.ClientRequest} req request + * @param {http.ServerResponse} res response + * @param {object} ctx context */ async preHandler(req, res, ctx) { this.constructor.tagContext(req, res, ctx); @@ -220,9 +231,9 @@ class Dingus { /** * Helper for collecting chunks as array of buffers. - * @param {Buffer[]} chunks - * @param {string|Buffer} chunk - * @param {string} encoding + * @param {Buffer[]} chunks array to collect chunks in + * @param {string|Buffer} chunk chunk to collect + * @param {string} encoding chunk encoding */ static pushBufChunk(chunks, chunk, encoding = 'utf8') { if (chunk) { @@ -236,9 +247,9 @@ class Dingus { /** * Sets ctx.responseBody and calls handler upon res.end(). - * @param {http.ClientRequest} req - * @param {http.ServerResponse} res - * @param {object} ctx + * @param {http.ClientRequest} req request + * @param {http.ServerResponse} res response + * @param {object} ctx context * @param {*} handler fn(req, res, ctx) */ static setEndBodyHandler(req, res, ctx, handler) { @@ -262,10 +273,10 @@ class Dingus { * Intercept writes for head requests, do not send to client, * but send length, and make body available in context. * N.B. If persisted, ctx.responseBody will be a raw buffer, be aware when logging. - * @param {http.ClientRequest} req - * @param {http.ServerResponse} res - * @param {object} ctx - * @param {Boolean} persistResponseBody + * @param {http.ClientRequest} req request + * @param {http.ServerResponse} res response + * @param {object} ctx context + * @param {boolean} persistResponseBody store body buffer on context */ static setHeadHandler(req, res, ctx, persistResponseBody = false) { if (req.method === 'HEAD') { @@ -290,10 +301,10 @@ class Dingus { /** * Resolve the handler to invoke for a request. - * @param {http.ClientRequest} req - * @param {http.ServerResponse} res - * @param {object} ctx - * @returns {object} + * @param {http.ClientRequest} req request + * @param {http.ServerResponse} res response + * @param {object} ctx context + * @returns {object} handler and args */ _determineHandler(req, res, ctx) { const _scope = _fileScope('_determineHandler'); @@ -326,11 +337,11 @@ class Dingus { /** * For intrinsic HEAD requests, resolve the handler to invoke. - * @param {http.ClientRequest} req - * @param {http.ServerResponse} res - * @param {object} ctx - * @param {string} pathPart - * @returns {object} + * @param {http.ClientRequest} req request + * @param {http.ServerResponse} res response + * @param {object} ctx context + * @param {string} pathPart router path + * @returns {object} hander and handler args */ _determineHeadHandler(req, res, ctx, pathPart) { const _scope = _fileScope('_determineHeadHandler'); @@ -352,9 +363,10 @@ class Dingus { /** * Dispatch the handler for a request - * @param {http.ClientRequest} req - * @param {http.ServerResponse} res - * @param {object} ctx + * @param {http.ClientRequest} req request + * @param {http.ServerResponse} res response + * @param {object} ctx context + * @returns {Promise} handler promise */ async dispatch(req, res, ctx = {}) { const { handler, handlerArgs } = this._determineHandler(req, res, ctx); @@ -370,8 +382,8 @@ class Dingus { /** * Return normalized type, without any parameters. - * @param {http.ClientRequest} req - * @returns {string} + * @param {http.ClientRequest} req request + * @returns {string} bare content type */ static getRequestContentType(req) { const contentType = req.getHeader(Enum.Header.ContentType); @@ -381,10 +393,10 @@ class Dingus { /** * Parse rawBody as contentType into ctx.parsedBody. - * @param {string} contentType - * @param {object} ctx - * @param {string|buffer} rawBody - */ + * @param {string} contentType type + * @param {object} ctx context + * @param {string|Buffer} rawBody body data + */ parseBody(contentType, ctx, rawBody) { const _scope = _fileScope('parseBody'); @@ -411,9 +423,10 @@ class Dingus { /** * Return all body data from a request. - * @param {http.ClientRequest} req - * @param {Number=} maximumBodySize - * @param {Boolean=} toString + * @param {http.ClientRequest} req request + * @param {number=} maximumBodySize bytes + * @param {boolean=} toString whether to convert body data buffer to string + * @returns {Promise} body data */ async bodyData(req, maximumBodySize, toString = true) { const _scope = _fileScope('bodyData'); @@ -443,12 +456,13 @@ class Dingus { /** * Read and parse request body data. * Sets ctx.parsedBody, and optionally ctx.rawBody. - * @param {http.ClientRequest} req - * @param {http.ServerResponse} res - * @param {object} ctx - * @param {object} - * @param {Boolean} .parseEmptyBody - * @param {Boolean} .persistRawBody + * @param {http.ClientRequest} req request + * @param {http.ServerResponse} res response + * @param {object} ctx context + * @param {object} options options + * @param {boolean} options.parseEmptyBody attempt to parse an empty body, results may vary by encoding + * @param {boolean} options.persistRawBody store raw body buffer on context + * @param {number=} options.maximumBodySize bytes */ async ingestBody(req, res, ctx, { parseEmptyBody = true, persistRawBody = false, maximumBodySize } = {}) { const rawBody = await this.bodyData(req, maximumBodySize); @@ -464,8 +478,9 @@ class Dingus { /** * Return the best matching response type. - * @param {string[]} responseTypes - * @param {http.ClientRequest} req + * @param {string[]} responseTypes available content types + * @param {http.ClientRequest} req request + * @returns {string} preferred content type */ static getResponseContentType(responseTypes, req) { const acceptHeader = req.getHeader(Enum.Header.Accept); @@ -475,8 +490,9 @@ class Dingus { /** * Returns a list of the most-preferred content encodings for the response. - * @param {string[]} responseEncodings - * @param {http.ClientRequest} req + * @param {string[]} responseEncodings available encodings + * @param {http.ClientRequest} req request + * @returns {string} preferred encoding */ static getResponseEncoding(responseEncodings, req) { const acceptEncodingHeader = req.getHeader(Enum.Header.AcceptEncoding); @@ -488,9 +504,9 @@ class Dingus { * Set the best content type for the response. * Sets ctx.responseType, and Content-Type header. * @param {string[]} responseTypes default first - * @param {http.ClientRequest} req - * @param {http.ServerResponse} res - * @param {object} ctx + * @param {http.ClientRequest} req request + * @param {http.ServerResponse} res response + * @param {object} ctx context */ setResponseType(responseTypes, req, res, ctx) { const _scope = _fileScope('setResponseType'); @@ -509,8 +525,8 @@ class Dingus { /** * Inserts an encoding into Content-Encoding header. - * @param {http.ServerResponse} res - * @param {string} encoding + * @param {http.ServerResponse} res response + * @param {string} encoding encoding */ static addEncodingHeader(res, encoding) { const existingEncodings = res.getHeader(Enum.Header.ContentEncoding); @@ -523,7 +539,8 @@ class Dingus { /** * Attempt to fetch both data and metadata for a file. - * @param {string} filePath + * @param {string} filePath path to file + * @returns {Promise<[object=, object=]>} array with file contents and stats */ async _readFileInfo(filePath) { const _scope = _fileScope('_readFileInfo'); @@ -547,9 +564,10 @@ class Dingus { /** * Potentially add additional headers from static file meta-file. - * @param {http.ServerResponse} res - * @param {string} directory + * @param {http.ServerResponse} res response + * @param {string} directory path to file * @param {string} fileName - already normalized and filtered + * @returns {boolean} if meta headers were added */ async _serveFileMetaHeaders(res, directory, fileName) { const _scope = _fileScope('_serveFileMetaHeaders'); @@ -585,11 +603,12 @@ class Dingus { /** * Serve a file from a directory, with rudimentary cache awareness. * This will also serve pre-encoded variations if available and requested. - * @param {http.ClientRequest} req - * @param {http.ServerResponse} res - * @param {object} ctx - * @param {string} directory - * @param {string} fileName + * @param {http.ClientRequest} req request + * @param {http.ServerResponse} res response + * @param {object} ctx context + * @param {string} directory path to file + * @param {string} fileName file name + * @returns {Promise=} serves file or renders status code */ async serveFile(req, res, ctx, directory, fileName) { const _scope = _fileScope('serveFile'); @@ -681,11 +700,12 @@ class Dingus { /** * Return a content-type appropriate rendering of an errorResponse object. - * @param {string} type content-type of response + * @param {string} contentType content-type of response * @param {object} err either an Error object, or an error response - * @param {number} err.statusCode - * @param {string} err.errorMessage - * @param {string|string[]} err.details + * @param {number} err.statusCode htttp status code + * @param {string} err.errorMessage http status message + * @param {string|string[]} err.details additional details to include + * @returns {string} rendered err */ // eslint-disable-next-line class-methods-use-this renderError(contentType, err) { @@ -707,9 +727,9 @@ class Dingus { * Send an error response. Terminal. * Logs any non-error-response errors as such. * @param {object} err either an Error object, or an error response - * @param {http.ClientRequest} req - * @param {http.ServerResponse} res - * @param {object} ctx + * @param {http.ClientRequest} req request + * @param {http.ServerResponse} res response + * @param {object} ctx context */ sendErrorResponse(err, req, res, ctx) { const _scope = _fileScope('sendErrorResponse'); @@ -734,10 +754,10 @@ class Dingus { /** - * @param {http.ClientRequest} req - * @param {http.ServerResponse} res - * @param {object} ctx - * @param {String} file - override ctx.params.file + * @param {http.ClientRequest} req request + * @param {http.ServerResponse} res response + * @param {object} ctx context + * @param {string} file - override ctx.params.file */ async handlerGetStaticFile(req, res, ctx, file) { Dingus.setHeadHandler(req, res, ctx); @@ -750,12 +770,12 @@ class Dingus { /** - * @param {http.ClientRequest} req - * @param {http.ServerResponse} res - * @param {Object} ctx - * @param {String} newPath - * @param {Number} statusCode - */ + * @param {http.ClientRequest} req request + * @param {http.ServerResponse} res response + * @param {object} ctx context + * @param {string} newPath url to redirect to + * @param {number=} statusCode status code to use for redirect, default 307 + */ async handlerRedirect(req, res, ctx, newPath, statusCode = 307) { this.setResponseType(this.responseTypes, req, res, ctx); res.setHeader(Enum.Header.Location, newPath); @@ -765,9 +785,9 @@ class Dingus { /** - * @param {http.ClientRequest} req - * @param {http.ServerResponse} res - * @param {object} ctx + * @param {http.ClientRequest} req request + * @param {http.ServerResponse} res response + * @param {object} ctx context */ async handlerMethodNotAllowed(req, res, ctx) { this.setResponseType(this.responseTypes, req, res, ctx); @@ -776,9 +796,9 @@ class Dingus { /** - * @param {http.ClientRequest} req - * @param {http.ServerResponse} res - * @param {object} ctx + * @param {http.ClientRequest} req request + * @param {http.ServerResponse} res response + * @param {object} ctx context */ async handlerNotFound(req, res, ctx) { this.setResponseType(this.responseTypes, req, res, ctx); @@ -787,9 +807,9 @@ class Dingus { /** - * @param {http.ClientRequest} req - * @param {http.ServerResponse} res - * @param {object} ctx + * @param {http.ClientRequest} req request + * @param {http.ServerResponse} res response + * @param {object} ctx context */ async handlerBadRequest(req, res, ctx) { this.setResponseType(this.responseTypes, req, res, ctx); @@ -798,9 +818,9 @@ class Dingus { /** - * @param {http.ClientRequest} req - * @param {http.ServerResponse} res - * @param {object} ctx + * @param {http.ClientRequest} req request + * @param {http.ServerResponse} res response + * @param {object} ctx context */ async handlerInternalServerError(req, res, ctx) { this.setResponseType(this.responseTypes, req, res, ctx);