X-Git-Url: https://git.squeep.com/?a=blobdiff_plain;f=lib%2Fdingus.js;h=0a5f9d8d50badb5dc0ceb3b4a3c4d876a87cc58e;hb=88c98d0981e4822d4f054ad822a6fa69614deecc;hp=4eded66a727e1d831e7249b462abb3d5da50fa17;hpb=9ff4b5a259e3583d15aeca1dce4c9ddcbb752023;p=squeep-api-dingus diff --git a/lib/dingus.js b/lib/dingus.js index 4eded66..0a5f9d8 100644 --- a/lib/dingus.js +++ b/lib/dingus.js @@ -13,22 +13,25 @@ const querystring = require('querystring'); const common = require('./common'); const ContentNegotiation = require('./content-negotiation'); const Enum = require('./enum'); -const { DingusError, ResponseError } = require('./errors'); +const { ResponseError, RouterNoPathError, RouterNoMethodError } = require('./errors'); const { extensionToMime } = require('./mime-helper'); const Router = require('./router'); const Template = require('./template'); // For logging. -const _fileScope = common.fileScope(__filename); +const { fileScope } = require('@squeep/log-helper'); +const _fileScope = fileScope(__filename); const defaultOptions = { - ignoreTrailingSlash: false, + ignoreTrailingSlash: true, proxyPrefix: '', strictAccept: true, selfBaseUrl: '', staticMetadata: true, - staticPath: undefined, // no reasonable default + staticPath: undefined, // No reasonable default trustProxy: true, + intrinsicHeadMethod: true, + intrinsicHeadPersistBody: false, querystring, }; @@ -36,15 +39,17 @@ class Dingus { /** * @param {Object} logger object which implements logging methods * @param {Object} options - * @param {Boolean} options.ignoreTrailingSlash + * @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 {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 */ - constructor(logger = common.nullLogger, options = {}) { + constructor(logger = console, options = {}) { common.setOptions(this, defaultOptions, options); this.router = new Router(options); @@ -60,7 +65,6 @@ class Dingus { ]; this.logger = logger; - common.ensureLoggerLevels(this.logger); } @@ -139,9 +143,9 @@ class Dingus { */ _getAddress(req) { // TODO: RFC7239 Forwarded support - const address = (this.trustProxy && req && req.getHeader(Enum.Header.XForwardedFor)) || - (this.trustProxy && req && req.getHeader(Enum.Header.XRealIP)) || - (req && req.connection && req.connection.remoteAddress) || + const address = (this.trustProxy && req?.getHeader(Enum.Header.XForwardedFor)) || + (this.trustProxy && req?.getHeader(Enum.Header.XRealIP)) || + (req?.connection?.remoteAddress) || ''; return address.split(/\s*,\s*/u)[0]; } @@ -153,8 +157,8 @@ class Dingus { */ _getProtocol(req) { // TODO: RFC7239 Forwarded support - const protocol = (this.trustProxy && req && req.getHeader(Enum.Header.XForwardedProto)) || - ((req && req.connection && req.connection.encrypted) ? 'https' : 'http'); + const protocol = (this.trustProxy && req?.getHeader(Enum.Header.XForwardedProto)) || + ((req?.connection?.encrypted) ? 'https' : 'http'); return protocol.split(/\s*,\s*/u)[0]; } @@ -254,13 +258,14 @@ class Dingus { /** - * Dispatch the handler for a request - * @param {http.ClientRequest} req - * @param {http.ServerResponse} res - * @param {object} ctx + * Resolve the handler to invoke for a request. + * @param {http.ClientRequest} req + * @param {http.ServerResponse} res + * @param {object} ctx + * @returns {object} */ - async dispatch(req, res, ctx = {}) { - const _scope = _fileScope('dispatch'); + _determineHandler(req, res, ctx) { + const _scope = _fileScope('_determineHandler'); const { pathPart, queryParams } = this._splitUrl(req.url); ctx.queryParams = queryParams; @@ -269,26 +274,59 @@ class Dingus { try { ({ handler, handlerArgs } = this.router.lookup(req.method, pathPart, ctx)); } catch (e) { - if (e instanceof DingusError) { - switch (e.message) { - case 'NoPath': - handler = this.handlerNotFound.bind(this); - break; - case 'NoMethod': - handler = this.handlerMethodNotAllowed.bind(this); - break; - default: - this.logger.error(_scope, 'unknown dingus error', { error: e }); - handler = this.handlerInternalServerError.bind(this); - } - } else if (e instanceof URIError) { + if (e instanceof URIError) { handler = this.handlerBadRequest.bind(this); + } else if (e instanceof RouterNoPathError) { + handler = this.handlerNotFound.bind(this); + } else if (e instanceof RouterNoMethodError) { + if (this.intrinsicHeadMethod && req.method === 'HEAD') { + ({ handler, handlerArgs } = this._determineHeadHandler(req, res, ctx, pathPart)); + } else { + handler = this.handlerMethodNotAllowed.bind(this); + } } else { - this.logger.error(_scope, 'lookup failure', { error: e }); + this.logger.error(_scope, 'unexpected error', { error: e }); handler = this.handlerInternalServerError.bind(this); } } + return { handler, handlerArgs }; + } + + /** + * 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} + */ + _determineHeadHandler(req, res, ctx, pathPart) { + const _scope = _fileScope('_determineHeadHandler'); + let handler, handlerArgs = []; + try { + ({ handler, handlerArgs } = this.router.lookup('GET', pathPart, ctx)); + Dingus.setHeadHandler(req, res, ctx, this.intrinsicHeadPersistBody); + } catch (e) { + if (e instanceof RouterNoMethodError) { + handler = this.handlerMethodNotAllowed.bind(this); + } else { + this.logger.error(_scope, 'unexpected error', { error: e }); + handler = this.handlerInternalServerError.bind(this); + } + } + return { handler, handlerArgs }; + } + + + /** + * Dispatch the handler for a request + * @param {http.ClientRequest} req + * @param {http.ServerResponse} res + * @param {object} ctx + */ + async dispatch(req, res, ctx = {}) { + const { handler, handlerArgs } = this._determineHandler(req, res, ctx); try { await this.preHandler(req, res, ctx); return await handler(req, res, ctx, ...handlerArgs); @@ -314,16 +352,11 @@ class Dingus { * Parse rawBody as contentType into ctx.parsedBody. * @param {string} contentType * @param {object} ctx - * @param {string|buffer} + * @param {string|buffer} rawBody */ parseBody(contentType, ctx, rawBody) { const _scope = _fileScope('parseBody'); - if (!rawBody) { - // 1.2.4 and earlier expected rawBody on context - rawBody = ctx.rawBody; - } - switch (contentType) { case Enum.ContentType.ApplicationForm: ctx.parsedBody = this.querystring.parse(rawBody); @@ -349,8 +382,9 @@ class Dingus { * Return all body data from a request. * @param {http.ClientRequest} req * @param {Number=} maximumBodySize + * @param {Boolean=} toString */ - async bodyData(req, maximumBodySize) { + async bodyData(req, maximumBodySize, toString = true) { const _scope = _fileScope('bodyData'); return new Promise((resolve, reject) => { const body = []; @@ -363,7 +397,10 @@ class Dingus { reject(new ResponseError(Enum.ErrorResponse.RequestEntityTooLarge)); } }); - req.on('end', () => resolve(Buffer.concat(body).toString())); + req.on('end', () => { + const bodyBuffer = Buffer.concat(body); + resolve(toString ? bodyBuffer.toString() : bodyBuffer); + }); req.on('error', (e) => { this.logger.error(_scope, 'failed', { error: e }); reject(e); @@ -483,8 +520,8 @@ class Dingus { */ async _serveFileMetaHeaders(res, directory, fileName) { const _scope = _fileScope('_serveFileMetaHeaders'); - this.logger.debug(_scope, 'called', { directory, fileName }); + let added = false; const metaPrefix = '.'; const metaSuffix = '.meta'; const metaFileName = `${metaPrefix}${fileName}${metaSuffix}`; @@ -492,7 +529,7 @@ class Dingus { const [stat, data] = await this._readFileInfo(metaFilePath); if (!stat) { - return; + return added; } const lineBreakRE = /\r\n|\n|\r/; @@ -505,8 +542,10 @@ class Dingus { const result = headerParseRE.exec(line); const { groups: header } = result; res.setHeader(header.name, header.value); + added = true; } }); + return added; } @@ -535,7 +574,7 @@ class Dingus { // We will not deal with any subdirs, nor any dot-files. // (Note that we could not deal with subdirs even if we wanted, due to simple router matching scheme.) if (fileName.indexOf(path.sep) >= 0 - || fileName.charAt(0) === '.') { + || fileName.startsWith('.')) { this.logger.debug(_scope, 'rejected filename', { fileName }); return this.handlerNotFound(req, res, ctx); } @@ -561,18 +600,20 @@ class Dingus { break; } const suffix = Enum.EncodingTypeSuffix[encoding]; - if (suffix) { - const encodedFilePath = `${filePath}${suffix}`; - const [ encodedStat, encodedData ] = await this._readFileInfo(encodedFilePath); - if (encodedStat) { - ([ stat, data ] = [ encodedStat, encodedData ]); - ctx.selectedEncoding = encoding; - Dingus.addEncodingHeader(res, encoding); - res.setHeader(Enum.Header.Vary, Enum.Header.AcceptEncoding); - this.logger.debug(_scope, 'serving encoded version', { ctx, encodedFilePath }); - } - break; + if (!suffix) { + this.logger.error(_scope, 'supported encoding missing mapped suffix', { ctx, encoding }); + continue; + } + const encodedFilePath = `${filePath}${suffix}`; + const [ encodedStat, encodedData ] = await this._readFileInfo(encodedFilePath); + if (encodedStat) { + ([ stat, data ] = [ encodedStat, encodedData ]); + ctx.selectedEncoding = encoding; + Dingus.addEncodingHeader(res, encoding); + res.setHeader(Enum.Header.Vary, Enum.Header.AcceptEncoding); + this.logger.debug(_scope, 'serving encoded version', { ctx, encodedFilePath }); } + break; } const lastModifiedDate = new Date(stat.mtimeMs); @@ -597,7 +638,7 @@ class Dingus { res.setHeader(Enum.Header.CacheControl, 'public'); if (this.staticMetadata) { - await this._serveFileMetaHeaders(res, directory, fileName); + ctx.metaHeaders = await this._serveFileMetaHeaders(res, directory, fileName); } this.logger.debug(_scope, 'serving file', { filePath, contentType }); @@ -646,7 +687,7 @@ class Dingus { res.setHeader(Enum.Header.ContentType, Enum.ContentType.TextPlain); } - if (err && err.statusCode) { + if (err?.statusCode) { res.statusCode = err.statusCode; body = this.renderError(res.getHeader(Enum.Header.ContentType), err); this.logger.debug(_scope, 'handler error', { err, req, res, ctx });