/**
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 moduleHook = require('../../hooks/require');
const patcher = require('../../hooks/patcher');
const agentEmitter = require('../../agent-emitter');
const {
  prototype: { decorateRequest },
  setFrameworkRequest
} = require('../../hooks/frameworks/base');
const HapiRequest = require('../../reporter/models/frameworks/hapi-request');
const { get } = require('lodash');
const { PATCH_TYPES } = require('../../constants');
const logger = require('../logger')('hooks:hapi');
const utils = require('./utils');

const constants = {
  LIFECYLE_METHODS: [
    'onRequest',
    'onPreAuth',
    'onPostAuth',
    'onPreHandler',
    'onPostHandler',
    'onPreResponse'
  ],
  FINISHED: 'Hapi.requestFinished',
  ORIG_FUNC: Symbol('originalFunction'),
  EVENTS: {
    HAPI_EXPORT: 'hapi-export', // used to patch methods on the hapi framework
    HAPI_ROUTES: 'hapi-routes', // used to instrument registered routes
    HAPI_ADD_ROUTE: 'hapi-add-route', // used to instrument adding a route
    HAPI_PRE_AUTH: 'hapi-pre-auth', // used to implement hapi onPreAuth hooks
    HAPI_POST_AUTH: 'hapi-post-auth', // uses to implement hapi onPostAuth hooks
    HAPI_PRE_HANDLER: 'hapi-pre-handler', // used to implement hapi onPreHandler hooks
    HAPI_FINISHED: 'hapi-response-finished', // used to implement when hapi response is finished
    HAPI_PRE_RES: 'hapi-pre-response', // used to implement hapi onPreResponse hooks
    HAPI_MULTI_PART: 'hapi-multi-part' // used to add instrumentation for multipart form parsing
  }
};

class HapiCore {
  constructor() {
    this.version = '>=17';
    this.instrumentHapi();
  }

  /**
   * Sets up handlers when `hapi` and `hapi-multer` and `hapi-cookie` is required
   */
  instrumentHapi() {
    const { version } = this;
    moduleHook.resolve(
      { name: 'hapi', version },
      this.registerXportHandlers.bind(this)
    );
    moduleHook.resolve(
      { name: '@hapi/hapi', version },
      this.registerXportHandlers.bind(this)
    );
    moduleHook.resolve(
      { name: 'pez' },
      this.registerMultipartHandler.bind(this)
    );
    moduleHook.resolve(
      { name: '@hapi/pez' },
      this.registerMultipartHandler.bind(this)
    );
  }

  /**
   * Patches `Pez.Dispenser.prototype.emit` to emit when multipart events have been emmited
   * @param {Pez}
   */
  registerMultipartHandler(Pez) {
    patcher.patch(Pez.Dispenser.prototype, 'emit', {
      name: 'Pez.Dispenser.emit',
      patchType: PATCH_TYPES.FRAMEWORK,
      alwaysRun: true,
      pre: (wrapCtx) => {
        const { args } = wrapCtx;
        agentEmitter.emit(constants.EVENTS.HAPI_MULTI_PART, args);
      }
    });
  }

  /**
   * Sets up handlers when hapi is required
   *
   * @param {Hapi} hapi export prototype
   */
  registerXportHandlers(hapi) {
    agentEmitter.emit(constants.EVENTS.HAPI_EXPORT, hapi);
    this.registerServerHandler(hapi);
  }

  registerServerHandler(hapi) {
    const self = this;
    // These are the session hooks which depend on hapi itself being loaded;
    // because this only executes after all of hapi's sub-dependencies are required,
    // we cannot wait to hook those until we have access to the hapi module.
    patcher.patch(hapi, ['Server', 'server'], {
      name: 'hapi.server',
      patchType: PATCH_TYPES.FRAMEWORK,
      alwaysRun: true,
      post(data) {
        const server = data.result;
        if (server) {
          self.instrumentServer(server);
          self.hookRoute(server);
        }
      }
    });
  }

  hookRoute(server) {
    patcher.patch(server._core.router, 'add', {
      alwaysRun: true,
      name: 'hapi.server.route',
      patchType: PATCH_TYPES.FRAMEWORK,
      post(data) {
        const route = get(data, 'args[1]');
        agentEmitter.emit(constants.EVENTS.HAPI_ADD_ROUTE, route);
      }
    });
  }

  instrumentServer(server) {
    const self = this;
    // mark that this server instance has not been hooked yet.
    server.toolkitHooked = false;
    server.ext('onPreStart', function onPreStart(core) {
      logger.info('hapi version %s', core.version);
      self.instrumentHapiHooks(core);
    });

    server.ext('onPostStart', function onPostStart(core) {
      const routes = get(core, '_core.router.routes');
      agentEmitter.emit(constants.EVENTS.HAPI_ROUTES, routes);
    });
  }

  instrumentHapiHooks(server) {
    server.ext('onRequest', function onRequest(request, h) {
      // Set the framework-specific request model in our first hook point
      setFrameworkRequest(request, HapiRequest);
      return h.continue;
    });

    server.ext('onPreAuth', function onPreAuth(request, h) {
      agentEmitter.emit(constants.EVENTS.HAPI_PRE_AUTH, request, h);
      return h.continue;
    });

    server.ext('onPostAuth', function onPostAuth(request, h) {
      agentEmitter.emit(constants.EVENTS.HAPI_POST_AUTH, {
        request,
        h,
        server
      });

      // Update the domain's model of the request with body and params since it has been
      // fully parsed and validated
      decorateRequest({
        normalizedUri: get(request, 'route.path'),
        body: request.payload,
        parameters: request.params,
        query: request.query
      });

      return h.continue;
    });

    server.ext('onPreHandler', function onPreHandler(request, h) {
      agentEmitter.emit(constants.EVENTS.HAPI_PRE_HANDLER, {
        request,
        h,
        server
      });

      return h.continue;
    });

    server.ext('onPostHandler', function onPostHandler(request, h) {
      const { response } = request;
      // if the response was ended before we got here, we may not get a response that looks like
      // what we expect it to
      if (response && !response.isBoom) {
        response.events.on('finish', function() {
          agentEmitter.emit(constants.EVENTS.HAPI_FINISHED, request);
        });
      }
      return h.continue;
    });
    server.ext('onPreResponse', function onPreResponse(request, h) {
      agentEmitter.emit(constants.EVENTS.HAPI_PRE_RES, request);
      return h.continue;
    });
  }
}

module.exports = HapiCore;
module.exports.constants = constants;
module.exports.utils = utils;
