X-Git-Url: http://git.squeep.com/?a=blobdiff_plain;f=lib%2Fdingus.js;h=11bdf998373789b9a9a1584ffb71f5b30661be76;hb=21c492b52444c5c95db2913d7429e281384a469f;hp=c8e4909bf1edca17202e1ca26a3310b9d748ffb3;hpb=29837f0eeb9fcb4c53426e5bd89e9bdf7e9d961b;p=squeep-api-dingus diff --git a/lib/dingus.js b/lib/dingus.js index c8e4909..11bdf99 100644 --- a/lib/dingus.js +++ b/lib/dingus.js @@ -26,6 +26,8 @@ const defaultOptions = { proxyPrefix: '', strictAccept: true, selfBaseUrl: '', + staticMetadata: true, + staticPath: undefined, // no reasonable default trustProxy: true, querystring, }; @@ -38,6 +40,7 @@ class Dingus { * @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 {Object} options.querystring alternate qs parser to use */ @@ -103,8 +106,8 @@ class Dingus { * @param {string} urlPath * @param {fn} handler */ - on(method, urlPath, handler) { - this.router.on(method, urlPath, handler); + on(method, urlPath, handler, ...handlerArgs) { + this.router.on(method, urlPath, handler, handlerArgs); } @@ -257,9 +260,9 @@ class Dingus { const { pathPart, queryParams } = this._splitUrl(req.url); ctx.queryParams = queryParams; - let handler; + let handler, handlerArgs = []; try { - handler = this.router.lookup(req.method, pathPart, ctx); + ({ handler, handlerArgs } = this.router.lookup(req.method, pathPart, ctx)); } catch (e) { if (e instanceof DingusError) { switch (e.message) { @@ -272,7 +275,7 @@ class Dingus { default: this.logger.error(_scope, 'unknown dingus error', { error: e }); handler = this.handlerInternalServerError.bind(this); - } + } } else if (e instanceof URIError) { handler = this.handlerBadRequest.bind(this); } else { @@ -283,7 +286,7 @@ class Dingus { try { await this.preHandler(req, res, ctx); - return await handler(req, res, ctx); + return await handler(req, res, ctx, ...handlerArgs); } catch (e) { ctx.error = e; this.sendErrorResponse(e, req, res, ctx); @@ -334,12 +337,21 @@ class Dingus { /** * Return all body data from a request. * @param {http.ClientRequest} req + * @param {Number=} maximumBodySize */ - async bodyData(req) { + async bodyData(req, maximumBodySize) { const _scope = _fileScope('bodyData'); return new Promise((resolve, reject) => { const body = []; - req.on('data', (chunk) => body.push(chunk)); + let length = 0; + req.on('data', (chunk) => { + body.push(chunk); + length += Buffer.byteLength(chunk); + if (maximumBodySize && length > maximumBodySize) { + this.logger.debug(_scope, 'body data exceeded limit', { length, maximumBodySize }); + reject(new ResponseError(Enum.ErrorResponse.RequestEntityTooLarge)); + } + }); req.on('end', () => resolve(Buffer.concat(body).toString())); req.on('error', (e) => { this.logger.error(_scope, 'failed', { error: e }); @@ -444,6 +456,41 @@ class Dingus { } + /** + * Potentially add additional headers from static file meta-file. + * @param {http.ServerResponse} res + * @param {string} directory + * @param {string} fileName - already normalized and filtered + */ + async _serveFileMetaHeaders(res, directory, fileName) { + const _scope = _fileScope('_serveFileMetaHeaders'); + this.logger.debug(_scope, 'called', { directory, fileName }); + + const metaPrefix = '.'; + const metaSuffix = '.meta'; + const metaFileName = `${metaPrefix}${fileName}${metaSuffix}`; + const metaFilePath = path.join(directory, metaFileName); + + const [stat, data] = await this._readFileInfo(metaFilePath); + if (!stat) { + return; + } + + const lineBreakRE = /\r\n|\n|\r/; + const lines = data.toString().split(lineBreakRE); + common.unfoldHeaderLines(lines); + + const headerParseRE = /^(?[^:]+): +(?.*)$/; + lines.forEach((line) => { + if (line) { + const result = headerParseRE.exec(line); + const { groups: header } = result; + res.setHeader(header.name, header.value); + } + }); + } + + /** * Serve a file from a directory, with rudimentary cache awareness. * This will also serve pre-encoded variations if available and requested. @@ -457,6 +504,12 @@ class Dingus { const _scope = _fileScope('serveFile'); this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx }); + // Require a directory field. + if (!directory) { + this.logger.debug(_scope, 'rejected unset directory', { fileName }); + return this.handlerNotFound(req, res, ctx); + } + // Normalize the supplied path, as encoded path-navigation may have been (maliciously) present. fileName = path.normalize(fileName); @@ -524,6 +577,10 @@ class Dingus { // We presume static files are relatively cacheable. res.setHeader(Enum.Header.CacheControl, 'public'); + if (this.staticMetadata) { + await this._serveFileMetaHeaders(res, directory, fileName); + } + this.logger.debug(_scope, 'serving file', { filePath, contentType }); res.end(data); } @@ -583,6 +640,37 @@ class Dingus { } + /** + * @param {http.ClientRequest} req + * @param {http.ServerResponse} res + * @param {object} ctx + * @param {String} file - override ctx.params.file + */ + async handlerGetStaticFile(req, res, ctx, file) { + Dingus.setHeadHandler(req, res, ctx); + + // Set a default response type to handle any errors; will be re-set to serve actual static content type. + this.setResponseType(this.responseTypes, req, res, ctx); + + await this.serveFile(req, res, ctx, this.staticPath, file || ctx.params.file); + } + + + /** + * @param {http.ClientRequest} req + * @param {http.ServerResponse} res + * @param {Object} ctx + * @param {String} newPath + * @param {Number} statusCode + */ + async handlerRedirect(req, res, ctx, newPath, statusCode = 307) { + this.setResponseType(this.responseTypes, req, res, ctx); + res.setHeader(Enum.Header.Location, newPath); + res.statusCode = statusCode; + res.end(); + } + + /** * @param {http.ClientRequest} req * @param {http.ServerResponse} res @@ -610,17 +698,18 @@ class Dingus { * @param {http.ServerResponse} res * @param {object} ctx */ - async handlerBadRequest(req, res, ctx) { + async handlerBadRequest(req, res, ctx) { this.setResponseType(this.responseTypes, req, res, ctx); throw new ResponseError(Enum.ErrorResponse.BadRequest); } + /** * @param {http.ClientRequest} req * @param {http.ServerResponse} res * @param {object} ctx */ - async handlerInternalServerError(req, res, ctx) { + async handlerInternalServerError(req, res, ctx) { this.setResponseType(this.responseTypes, req, res, ctx); throw new ResponseError(Enum.ErrorResponse.InternalServerError); }