quiet an eslint warning
[squeep-api-dingus] / lib / dingus.js
index 84c1d14b6a38e4d9050e76eaf7095588f501196d..943aa2f91e7bfc141960b35182bbbc23ef816f16 100644 (file)
@@ -27,7 +27,7 @@ const defaultOptions = {
   strictAccept: true,
   selfBaseUrl: '',
   staticMetadata: true,
-  staticPath: undefined, // no reasonable default
+  staticPath: undefined, // No reasonable default
   trustProxy: true,
   querystring,
 };
@@ -226,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 = [];
@@ -240,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);
       };
     }
@@ -306,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);
@@ -337,13 +348,26 @@ class Dingus {
   /**
    * Return all body data from a request.
    * @param {http.ClientRequest} req
+   * @param {Number=} maximumBodySize
+   * @param {Boolean=} toString
    */
-  async bodyData(req) {
+  async bodyData(req, maximumBodySize, toString = true) {
     const _scope = _fileScope('bodyData');
     return new Promise((resolve, reject) => {
       const body = [];
-      req.on('data', (chunk) => body.push(chunk));
-      req.on('end', () => resolve(Buffer.concat(body).toString()));
+      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', () => {
+        const bodyBuffer = Buffer.concat(body);
+        resolve(toString ? bodyBuffer.toString() : bodyBuffer);
+      });
       req.on('error', (e) => {
         this.logger.error(_scope, 'failed', { error: e });
         reject(e);
@@ -357,11 +381,19 @@ class Dingus {
    * @param {http.ClientRequest} req
    * @param {http.ServerResponse} res
    * @param {object} ctx
-   */
-  async ingestBody(req, res, ctx) {
-    ctx.rawBody = await this.bodyData(req);
-    const contentType = Dingus.getRequestContentType(req);
-    this.parseBody(contentType, ctx);
+   * @param {object}
+   * @param {Boolean} .parseEmptyBody
+   * @param {Boolean} .persistRawBody
+   */
+  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);
+    }
   }
 
 
@@ -493,7 +525,7 @@ 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) {
@@ -533,18 +565,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);
@@ -621,11 +655,11 @@ 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);
   }