/**
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 moduleHook = require('../hooks/require.js');
const patcher = require('../hooks/patcher.js');
const { PATCH_TYPES } = require('../constants');
const reply = require('./hapi-16-reply.js');
const { utils } = require('../core/hapi');
const { emitSessionEvent } = require('./frameworks/common');

const sessionChecks = {
  contrast_httponly(opt) {
    return !opt || opt.isHttpOnly !== undefined;
  },
  'contrast_secure-flag-missing'(opt) {
    return !opt || opt.isSecure !== undefined;
  }
};

class Hapi16SessionHandler {
  constructor(inAssessMode) {
    this.assessMode = inAssessMode;
  }

  /**
   * Hook all the different occurences of the "state" function in Hapi
   */
  hookState() {
    if (!this.assessMode) {
      return;
    }

    // We replace `state` without patcher, and so we're using self rather than an arrow
    // function to ensure we're not messing with the type of function (function --> arrow).
    // Could be overly paranoid.
    const self = this;

    reply.hook(function(reply) {
      const { state } = reply;
      reply.state = function(...args) {
        const [, , options] = args;
        const { request } = reply;
        const r = state.apply(reply, args);
        // NOTE: this function does not typically return anything, we just
        // capture and return this as proper monkey patching etiquette

        if (options) {
          self.captureSessionEvents(
            options,
            'reply.state',
            'P2',
            reply,
            Array.from(args),
            r,
            request
          );
        }

        return r;
      };
      return reply;
    });

    moduleHook.resolve(
      {
        name: 'hapi',
        file: 'lib/response.js'
      },
      this.hookResponse.bind(this)
    );
    moduleHook.resolve(
      {
        name: '@hapi/hapi',
        file: 'lib/response.js'
      },
      this.hookResponse.bind(this)
    );

    moduleHook.resolve(
      {
        name: 'hapi',
        file: 'lib/plugin.js'
      },
      this.hookPlugin.bind(this)
    );
    moduleHook.resolve(
      {
        name: '@hapi/hapi',
        file: 'lib/plugin.js'
      },
      this.hookPlugin.bind(this)
    );
  }

  /**
   * The module hook will not emit the event for a hapi load until after
   * all of hapi's modules have been loaded. Because we rely on hooking files loaded by hapi,
   * we need to separate out hooks that truly depend on the hapi module itself from hooks which do not.
   */
  hookConfigs(hapi) {
    if (!this.assessMode) {
      return;
    }

    patcher.patch(hapi, 'Server', {
      name: 'hapi.Server session',
      patchType: PATCH_TYPES.FRAMEWORK,
      alwaysRun: true,
      post: (data) => {
        const server = data.result;
        const options = data.args[0];

        const sourceType = 'P0';

        if (server) {
          Object.getOwnPropertyNames(sessionChecks).forEach(function(
            eventName
          ) {
            server.decorate('request', eventName, undefined);
          });
        }

        if (!options) {
          return;
        }

        const state = _.get(options, 'connections.state');
        if (state) {
          this.captureSessionEvents(
            state,
            'Server',
            sourceType,
            data.result,
            data.args,
            data.result,
            server._requestor._decorations
          );
        }
      }
    });

    patcher.patch(hapi.Server.prototype, 'connection', {
      name: 'hapi.Server.prototype.connection',
      patchType: PATCH_TYPES.FRAMEWORK,
      alwaysRun: true,
      post: (data) => {
        const server = data.obj;
        const options = data.args[0];
        if (options) {
          const { state } = options;
          if (state) {
            this.captureSessionEvents(
              state,
              'Server.prototype.connection',
              'P0',
              data.obj,
              data.args,
              data.result,
              server._requestor._decorations
            );
          }
        }
      }
    });
  }

  hookResponse(Response) {
    patcher.patch(Response.prototype, 'state', {
      name: 'hapi.Response.prototype.state',
      patchType: PATCH_TYPES.FRAMEWORK,
      alwaysRun: true,
      post: (data) => {
        const /*name = data.args[0],
					value = data.args[1],*/
          options = data.args[data.args.length - 1],
          response = data.obj,
          { request } = response;

        if (!options) {
          return;
        }

        this.captureSessionEvents(
          options,
          'response.state',
          'P2',
          data.obj,
          data.args,
          data.result,
          request
        );
      }
    });
  }

  hookPlugin(Plugin) {
    patcher.patch(Plugin.prototype, 'state', {
      name: 'hapi.Plugin.prototype.state',
      patchType: PATCH_TYPES.FRAMEWORK,
      alwaysRun: true,
      post: (data) => {
        const /*name = data.args[0],*/
          options = data.args[1],
          server = data.obj;

        const decorations = server._requestor && server._requestor._decorations;
        if (!decorations) {
          return;
        }
        this.captureSessionEvents(
          options,
          'response.Plugin.prototype.state',
          'P1',
          data.obj,
          data.args,
          data.result,
          decorations
        );
      }
    });
  }

  /**
   * Interrogates the request object for state and attempts to find whether
   * a cookie is being set in the response, and if so, if we have created
   * and event for setting it.
   *
   * Emits a session event.
   *
   * @param {hapi.Request} request
   */
  checkCookies(request) {
    const events = {};
    Object.getOwnPropertyNames(sessionChecks).forEach(function(eventName) {
      const event = request[eventName];
      if (!event) {
        return;
      }

      // data.events is a hash which maps a ruleId to the event for that ID.
      // if a single event maps to every rule, as in Express, the event will
      // be in events.any
      const ruleId = eventName.replace('contrast_', '');
      events[ruleId] = event;
    });

    emitSessionEvent(events);
  }

  /**
   * Inspect options and create an event if necessary, then
   * persist that event on the request decorator.
   *
   * @param {Object} options @see {@link https://hapijs.com/api#serverstatename-options|request extension points}
   * @param {string} method Method name of the method which triggered the event
   * @param {string} sourceType The source type of the event (P0, P1, etc). Where we got options from
   * @param {*} obj The 'this' for the event creation
   * @param {*[]} args The args for event creation
   * @param {*} result The result for event creation
   * @param {Object} decoratee The decorated request object to persist the event on
   */
  captureSessionEvents(
    options,
    method,
    sourceType,
    obj,
    args,
    result,
    decoratee
  ) {
    if (!options) {
      return;
    }

    let event, ctxt;
    Object.getOwnPropertyNames(sessionChecks).forEach(function(eventName) {
      const changed = sessionChecks[eventName];

      if (!changed(options)) {
        return;
      }

      ctxt = ctxt || utils.createSessionContext(obj, args, result, sourceType);
      event =
        event ||
        utils.createSessionEvent('hapi', method, ctxt, sourceType, options);

      // check if this is a proper decoration rather than a request object
      if (decoratee.raw && decoratee.raw.req && decoratee.raw.res) {
        // request object
        decoratee[eventName] = event;
      } else {
        decoratee[eventName].method = event;
      }
    });
  }
}

module.exports = Hapi16SessionHandler;
