add option to persist response body in context for HEAD requests
[squeep-api-dingus] / lib / dingus.js
index 343283974d7c723a6cbb8eb85fc3e3b942230fff..1098ea7dc54041f57a6987bfad6b9560a149ae94 100644 (file)
@@ -27,6 +27,7 @@ const defaultOptions = {
   strictAccept: true,
   selfBaseUrl: '',
   staticMetadata: true,
+  staticPath: undefined, // no reasonable default
   trustProxy: true,
   querystring,
 };
@@ -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);
       };
     }
@@ -336,12 +342,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 });
@@ -494,6 +509,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);