router_Request.js

/**
 *  lambda-lambda-lambda/router
 *  AWS Lambda@Edge serverless application router.
 *
 *  Copyright 2021-2023, Marc S. Brooks (https://mbrooks.info)
 *  Licensed under the MIT license:
 *  http://www.opensource.org/licenses/mit-license.php
 */

'use strict';

// Local modules.
const {RouterError} = require('./Error');

/**
 * Provides AWS CloudFront request instance and methods.
 *
 * @see https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-event-structure.html#example-origin-request
 */
class RouterRequest {

  /**
   * @param {CloudFrontRequest} cfReqObj
   *   CloudFront request object.
   */
  constructor(cfReqObj) {
    this.cfReqObj = cfReqObj;
    this.plugins  = {};

    Object.seal(this);
  }

  /**
   * Return CloudFront request object.
   *
   * @return {CloudFrontRequest}
   */
  data() {
    return this.cfReqObj;
  }

  /**
   * Return value for given HTTP header name.
   *
   * @param {String} name
   *   HTTP header name.
   *
   * @return {String|undefined}
   *
   * @example
   * const value = req.header('Content-Type');
   * // text/html
   */
  header(name) {
    const headers = this.getHeaders();

    let value;
    Object.keys(headers).forEach(function(key) {
      if (name && key === name.toLowerCase()) {
        value = headers[key][0].value;
      }
    });
    return value;
  }

  /**
   * Check Accept matches the given value.
   *
   * @param {String} mimeType
   *   MIME type value.
   *
   * @return {Boolean}
   *
   * @example
   * const value = req.is('text/html');
   * // true
   */
  is(mimeType) {
    return (this.header('Accept') === mimeType);
  }

  /**
   * Return the HTTP request parameters or name/value.
   *
   * @param {String} name
   *   Parameter name (optional).
   *
   * @return {String|Object|undefined}
   *
   * @example
   * const params = req.param();
   * // {name: 'value'}
   *
   * const value = req.param('name');
   * // value
   */
  param(name) {
    let obj = {};

    if (this.queryString()) {
      obj = RouterRequest.parseParams(this.queryString());
    } else if (this.body()) {
      obj = RouterRequest.parseBody(this.body());
    }

    if (Object.keys(obj).length > 0) {
      if (name) {
        return obj[name];
      }

      return obj;
    }
  }

  /**
   * Return the HTTP method of the request.
   *
   * @return {String}
   */
  method() {
    return this.data().method;
  }

  /**
   * Return the serialized query parameters.
   *
   * @return {String}
   */
  queryString() {
    return this.data().querystring;
  }

  /**
   * Return the relative path of the requested object.
   *
   * @return {String}
   */
  uri() {
    return this.data().uri;
  }

  /**
   * Return the HTTP client IP (remote address).
   *
   * @return {String}
   */
  clientIp() {
    return this.data().clientIp;
  }

  /**
   * Return the base64-encoded body data.
   *
   * @return {String}
   */
  body() {
    return this.data().body.data;
  }

  /**
   * Return the headers of the request.
   *
   * @return {CloudFrontFunctionsHeaders}
   */
  getHeaders() {
    return this.data().headers;
  }

  /**
   * Set/Get value passed down the application stack.
   *
   * @param {String} name
   *   Plugin name.
   *
   * @param {*} value
   *   Plugin value.
   *
   * @return {*|undefined}
   *
   * @example
   * req.plugin('name', 'value');
   *
   *   ..
   *
   * const value = req.plugin('name');
   * // value
   */
  plugin(name, value) {
    const plugin = this.plugins[name];

    if ((!plugin && name && value) || (plugin && value)) {
      return this.plugins[name] = value;
    } else if (plugin && !value) {
      return this.plugins[name];
    }

    throw new RouterError(`Plugin "${name}" doesn't exist`);
  }

  /**
   * Convert base64-encoded request body to object.
   *
   * @param {String} str
   *   base64 string.
   *
   * @return {Object|undefined}
   */
  static parseBody(str) {
    const body = Buffer.from(
      str, 'base64'
    ).toString();

    if (RouterRequest.isJson(body)) {
      return JSON.parse(body);
    }

    if (RouterRequest.isParams(body)) {
      return RouterRequest.parseParams(body);
    }
  }

  /**
   * Convert serialized query string to object.
   *
   * @param {String} str
   *   Parameter name/value pairs.
   *
   * @return {Object}
   */
  static parseParams(str) {
    return Object.fromEntries(new URLSearchParams(str));
  }

  /**
   * Check valid HTTP request parameters.
   *
   * @param {String} str
   *   Parameter name/value pairs.
   *
   * @return {Boolean}
   */
  static isParams(str) {
    const values = Array.from(new URLSearchParams(str));

    return (values.length > 0 && values[0][0] !== str);
  }

  /**
   * Check valid JSON string format.
   *
   * @param {String} str
   *   JSON string.
   *
   * @return {Boolean}
   */
  static isJson(str) {
    let data;

    try {
      data = JSON.parse(str);
    } catch (err) {
      return false;
    }

    return (Array.isArray(data) || typeof data === 'object');
  }
};

module.exports = RouterRequest;