X-Git-Url: http://git.squeep.com/?a=blobdiff_plain;f=lib%2Fdingus.js;h=4eded66a727e1d831e7249b462abb3d5da50fa17;hb=9ff4b5a259e3583d15aeca1dce4c9ddcbb752023;hp=0143585c1d83732a9e064d2f8687596af88f42be;hpb=ca35f167826c0732571da5f35e3c25881d138b79;p=squeep-api-dingus diff --git a/lib/dingus.js b/lib/dingus.js index 0143585..4eded66 100644 --- a/lib/dingus.js +++ b/lib/dingus.js @@ -27,6 +27,7 @@ const defaultOptions = { strictAccept: true, selfBaseUrl: '', staticMetadata: true, + staticPath: undefined, // no reasonable default trustProxy: true, querystring, }; @@ -105,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); } @@ -225,11 +226,13 @@ 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 */ - static setHeadHandler(req, res, ctx) { + static setHeadHandler(req, res, ctx, persistResponseBody = false) { if (req.method === 'HEAD') { const origEnd = res.end.bind(res); const chunks = []; @@ -239,8 +242,11 @@ class Dingus { }; res.end = function (data, encoding, ...rest) { Dingus.pushBufChunk(chunks, data, encoding); - ctx.responseBody = Buffer.concat(chunks); - res.setHeader(Enum.Header.ContentLength, Buffer.byteLength(ctx.responseBody)); + const responseBody = Buffer.concat(chunks); + res.setHeader(Enum.Header.ContentLength, Buffer.byteLength(responseBody)); + if (persistResponseBody) { + ctx.responseBody = responseBody; + } return origEnd(undefined, encoding, ...rest); }; } @@ -259,9 +265,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) { @@ -285,7 +291,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); @@ -305,21 +311,27 @@ class Dingus { /** - * Parse rawBody from ctx as contentType into parsedBody. - * @param {string} contentType - * @param {object} ctx - */ - parseBody(contentType, ctx) { + * Parse rawBody as contentType into ctx.parsedBody. + * @param {string} contentType + * @param {object} ctx + * @param {string|buffer} + */ + 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(ctx.rawBody); + ctx.parsedBody = this.querystring.parse(rawBody); break; case Enum.ContentType.ApplicationJson: try { - ctx.parsedBody = JSON.parse(ctx.rawBody); + ctx.parsedBody = JSON.parse(rawBody); } catch (e) { this.logger.debug(_scope, 'JSON parse failed', { requestId: ctx.requestId, error: e }); throw new ResponseError(Enum.ErrorResponse.BadRequest, e.message); @@ -336,12 +348,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 }); @@ -356,11 +377,19 @@ class Dingus { * @param {http.ClientRequest} req * @param {http.ServerResponse} res * @param {object} ctx + * @param {object} + * @param {Boolean} .parseEmptyBody + * @param {Boolean} .persistRawBody */ - async ingestBody(req, res, ctx) { - ctx.rawBody = await this.bodyData(req); - const contentType = Dingus.getRequestContentType(req); - this.parseBody(contentType, ctx); + async ingestBody(req, res, ctx, { parseEmptyBody = true, persistRawBody = false, maximumBodySize } = {}) { + const rawBody = await this.bodyData(req, maximumBodySize); + if (persistRawBody) { + ctx.rawBody = rawBody; + } + if (rawBody || parseEmptyBody) { + const contentType = Dingus.getRequestContentType(req); + this.parseBody(contentType, ctx, rawBody); + } } @@ -492,7 +521,13 @@ class Dingus { */ async serveFile(req, res, ctx, directory, fileName) { const _scope = _fileScope('serveFile'); - this.logger.debug(_scope, 'called', { req: common.requestLogData(req), ctx }); + this.logger.debug(_scope, 'called', { 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); @@ -614,16 +649,47 @@ class Dingus { if (err && err.statusCode) { res.statusCode = err.statusCode; body = this.renderError(res.getHeader(Enum.Header.ContentType), err); - this.logger.debug(_scope, 'handler error', { err, ...common.handlerLogData(req, res, ctx) }); + this.logger.debug(_scope, 'handler error', { err, req, res, ctx }); } else { res.statusCode = 500; body = this.renderError(res.getHeader(Enum.Header.ContentType), Enum.ErrorResponse.InternalServerError); - this.logger.error(_scope, 'handler exception', { err, ...common.handlerLogData(req, res, ctx) }); + this.logger.error(_scope, 'handler exception', { err, req, res, ctx }); } res.end(body); } + /** + * @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