/**
Copyright: 2024 Contrast Security, Inc
  Contact: support@contrastsecurity.com
  License: Commercial

  NOTICE: This Software and the patented inventions embodied within may only be
  used as part of Contrast Security’s commercial offerings. Even though it is
  made available through public repositories, use of this Software is subject to
  the applicable End User Licensing Agreement found at
  https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
  between Contrast Security and the End User. The Software may not be reverse
  engineered, modified, repackaged, sold, redistributed or otherwise used in a
  way not consistent with the End User License Agreement.
*/
'use strict';
const logger = require('../../core/logger')('contrast:hooks:koa:sources');
const agentEmitter = require('../../agent-emitter');
const {
  constants: { EVENTS }
} = require('../../core/koa');

class KoaAssessSources {
  constructor() {
    agentEmitter.on(
      EVENTS.REQ_START,
      this.registerRequestStartHandler.bind(this)
    );

    agentEmitter.on(EVENTS.CTX_PARAMS, this.registerParamsHandler.bind(this));

    agentEmitter.on(
      EVENTS.CTX_REQUEST_BODY,
      this.registerBodyHandler.bind(this)
    );

    agentEmitter.on(
      EVENTS.CTX_REQUEST_HEADER,
      this.registerHeaderHandler.bind(this)
    );

    agentEmitter.on(
      EVENTS.MULTI_PART,
      this.registerMultipartHandler.bind(this)
    );
    agentEmitter.on(EVENTS.COOKIE, this.registerCookieHandler.bind(this));
  }

  /**
   * Handles emitting source events `assess.query` and `assess.headers` that are available when a request is handled in Koa
   *
   * @param {Koa.Context} ctx koa request ctx
   */
  registerRequestStartHandler(ctx) {
    /**
     * In Koa, there's a very strange system for parsing and storing querystrings.
     * Strings are generally stored in a string format but are generally used as
     * an object. In doing this, Koa parses between the two formats a number of
     * times every single time ctx.query is read or written to. The code for this
     * is in koa/request.js:~172. One of the places were the object format used
     * by users is constant is in the ctx.request._querycache, where object
     * forms are cached after being stored. We wrap this querycache in a
     * membrane to track the data we cannot otherwise get at.
     * @link:  https://github.com/koajs/koa/blob/master/lib/request.js#L172
     * Passing a bogus 4th arg to the emit so that the getter fires first so _querycache is populated
     * and assess will properly wrap the _querycache object
     */
    agentEmitter.emit(
      'assess.query',
      ctx.request,
      '_querycache',
      ctx.request.query
    );

    // Some earlier versions of koa use __defineGetter__ only (delegates module) to create
    // the header and headers property on the request. The property does not have a setter.
    // This causes an exception when we try to wrap the property in a membrane proxy. We use
    // __lookupS/Getter__ because __defineGetter__ does not expose properties when using
    // getOwnPropertyDescriptor on instances or prototypes. For these koa versions, for now,
    // we won't track headers.
    if (
      ctx.request.__lookupGetter__('header') &&
      !ctx.request.__lookupSetter__('header')
    ) {
      logger.debug(
        'koa version does not allow setting of `request.header`. headers will not be tracked'
      );
      return;
    }

    agentEmitter.emit('assess.headers', ctx.request, 'header');
  }

  /**
   * Handles when ctx.params is parsed by emitting `assess.params`
   *
   * @param {Koa.Context} ctx koa request ctx
   */
  registerParamsHandler(ctx) {
    agentEmitter.emit('assess.params', ctx, 'params');
  }

  /**
   * Handles when ctx.request.body is set by emitting `assess.body`
   *
   * @param {Koa.Context.Request} ctx koa request ctx
   */
  registerBodyHandler({ request }) {
    agentEmitter.emit('assess.body', request, 'body');
  }

  /**
   * Handles when ctx.request.header is called by emitting `assess.headers`
   *
   * @param {Koa.Context.Request} ctx koa request ctx
   */
  registerHeaderHandler({ request }) {
    agentEmitter.emit('assess.headers', request, 'headers');
  }

  /**
   * Handles when `koa-multer` middleware is run and properly parses
   * a multipart form and puts the body on the `req`
   *
   * @param {Object} params
   * @param {IncomingMessage} params.req
   * @param {ServerResponse} param.res
   */
  registerMultipartHandler({ req }) {
    agentEmitter.emit('assess.body', req, 'body');
    agentEmitter.emit('assess.body', req, 'file');
    agentEmitter.emit('assess.body', req, 'files');
  }

  /**
   * Handles when `koa-cookie` middleware is run and properly parses
   * a cookies that are put on ctx
   *
   * @param {Koa.Context.Request} ctx koa request ctx
   */
  registerCookieHandler(ctx) {
    agentEmitter.emit('assess.cookies', ctx, 'cookie');
  }
}

module.exports = KoaAssessSources;
