/**
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 _ = require('lodash');
const agentEmitter = require('../../agent-emitter');
const {
  AsyncStorage,
  KEYS: { RES, REQUEST }
} = require('../../core/async-storage');
const { INPUT_TYPES } = require('../../constants');
const SinkEvent = require('../../protect/models/sink-event');
const SourceEvent = require('../../protect/models/source-event');
const logger = require('../../core/logger')('contrast:hooks:frameworks:common');

const getRawResponse = (response) => _.get(response, 'raw.res');
const getRawRequest = (request) => _.get(request, 'raw.req');

const createSinkEvent = (data, type, id) => new SinkEvent({ data, id, type });
const createSourceEvent = (frameworkRequest, req, res, dataPath, type, id) =>
  new SourceEvent({
    _incomingMessage: req,
    _serverResponse: res,
    dataPath,
    dataOwner: frameworkRequest,
    id,
    request: frameworkRequest,
    type
  });

module.exports.getRawRequest = getRawRequest;
module.exports.getRawResponse = getRawResponse;
module.exports.createSinkEvent = createSinkEvent;
module.exports.createSourceEvent = createSourceEvent;

/**
 * Alerts listeners that there are watchable properties on the request
 * but emitting the request and the property name.
 *
 * @param {hapi.Request} request The request object we're emitting
 */
module.exports.emitWatchableObjects = function emitWatchableObjects(request) {
  agentEmitter.emit('assess.body', request, 'payload');
  agentEmitter.emit('assess.cookies', request, 'state');
  agentEmitter.emit('assess.headers', request, 'headers');
  agentEmitter.emit('assess.query', request, 'query');
  agentEmitter.emit('assess.params', request, 'params');
  agentEmitter.emit('assess.body', request, 'payload.data.file');
};

/**
 * emit a protect lifecycle source event
 * @param {Object}  frameworkRequest
 * @param {IncomingMessage} req http request object
 * @param {ServerResponse} res http response object
 * @param {string} dataPath object property accessor for `request'
 * @param {string} type from constants.js/INPUT_TYPES
 * @param {string} id framework id
 */
module.exports.emitSourceEvent = function emitSourceEvent(
  frameworkRequest,
  req,
  res,
  dataPath,
  type,
  id
) {
  const reqPath = _.get(frameworkRequest, dataPath);
  if (_.isEmpty(reqPath)) {
    return;
  }

  const sourceEvent = createSourceEvent(
    frameworkRequest,
    req,
    res,
    dataPath,
    type,
    id
  );
  agentEmitter.emit('protect.source', sourceEvent);
};

module.exports.emitSinkEvent = function emitSinkEvent(
  data,
  type,
  id,
  checkObj = true
) {
  if (checkObj && _.isEmpty(data)) {
    return;
  }

  agentEmitter.emit('protect.sink', createSinkEvent(data, type, id));
};

/**
 * emits a protect source event of type MULTIPART_NAME
 *
 * @param {Object} params
 * @param {String} params.id id of agentEmitter
 * @param {String} params.name name of file
 */
module.exports.emitMultipartNameSource = function emitMultipartNameSource({
  id,
  name
}) {
  const type = INPUT_TYPES.MULTIPART_NAME;
  // This will result in a UserInput whose name matches its value.
  const data = { [name]: name };
  const event = new SourceEvent({ id, type, data });
  agentEmitter.emit('protect.source', event);
};

/**
 * emits a protect source event of type MULTIPART_VALUE
 *
 * @param {Object} params
 * @param {String} params.id id of agentEmitter
 * @param {Object} params.fields parsed multipart form data fields
 */
module.exports.emitMultipartValueSource = function emitMultipartValueSource({
  id,
  data
}) {
  const type = INPUT_TYPES.MULTIPART_VALUE;
  const event = new SourceEvent({ id, type, data });
  agentEmitter.emit('protect.source', event);
};

/**
 * Gets framework request model and response headers from AsyncStorage
 * If headers are passed in, it will use those as responseHeaders
 * Note: headers are only passed in for Fastify as it obfuscates the headers
 * until it calls res.writeHead/res.end
 *
 * @param {Object} headers response headers from Fastify request
 * @return { Object} request,responseHeaders of a request
 */
const getHttpData = (headers) => {
  const ctx = AsyncStorage.getContext();
  if (!ctx) {
    logger.warn(
      'request storage context not accessible: %s',
      new Error().stack
    );
    return null;
  }
  const { [REQUEST]: request, [RES]: res } = ctx;
  return { request, responseHeaders: headers || res.getHeaders() };
};

/**
 * Gets the request and response headers from async storage and emits a send event
 * used for a lot of the response scanning http rules
 *
 * @param {Object} body passed to response
 * @param {Object} headers response headers
 */
module.exports.emitSendEvent = function emitSendEvent(body, headers) {
  const httpData = getHttpData(headers);
  if (httpData) {
    const { request, responseHeaders } = httpData;
    agentEmitter.emit('send', { request, responseHeaders, body });
  }
};

/**
 * Gets the framework request and res from AsyncStorage and properly emits the session event used for
 *
 * @param {Object} events SessionEvent objects
 */
module.exports.emitSessionEvent = function emitSessionEvent(events) {
  const httpData = getHttpData();
  if (httpData) {
    const { request, responseHeaders } = httpData;
    agentEmitter.emit('session', { request, responseHeaders, events });
  }
};
