/**
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 agentEmitter = require('../../agent-emitter');
const { isString } = require('../../util/is-string');
const moduleHook = require('../../hooks/require');
const patcher = require('../../hooks/patcher');
const Scopes = require('../async-storage/scopes');
const { PATCH_TYPES } = require('../../constants');

// https://github.com/strongloop/loopback-next/blob/master/packages/openapi-v3/src/decorators/request-body.decorator.ts#L14
const REQUEST_BODY_INDEX = 'x-parameter-index';

const constants = {
  EVENTS: {
    SEND: {
      POST: 'loopback4 response send post'
    },
    SOURCES: {
      BODY: 'loopback4 body source',
      PARAMS: 'loopback4 param source'
    }
  }
};

class Loopback4Core {
  constructor() {
    this.instrumentLoopback4();
  }

  instrumentLoopback4() {
    moduleHook.resolve(
      { name: '@loopback/rest', file: 'dist/rest.server.js' },
      this.patchRestServer.bind(this)
    );
    moduleHook.resolve(
      { name: '@loopback/rest', file: 'dist/parser.js' },
      this.patchRestParser.bind(this)
    );
    moduleHook.resolve(
      { name: '@loopback/metadata', file: 'dist/decorator-factory.js' },
      this.patchDecoratorFactory.bind(this)
    );
    moduleHook.resolve({ name: 'http' }, this.patchServerResponse.bind(this));
  }

  patchRestServer(xport) {
    // the updateSpecFromRequest function mixes-in some data from the request
    // (namely the hostname) in to the in-memory openapi spec document
    // this then gets stringified and JSON.parse'ed again.  The resulting
    // JSON.parse causes the object to get wrapped in a deserialization membrane
    // b/c it contains tracked data.  This causes *every* property access
    // to generate a CallContext etc, which causes the loading of the explorer
    // page with our test app to take > 30 seconds.
    //
    // Since this is just a JSON file getting returned it can't cause an XSS
    // anyways, so just run this method in no-instrumentation scope so the
    // updated spec object does not become tainted.
    patcher.patch(xport.RestServer.prototype, 'updateSpecFromRequest', {
      scope: Scopes.SCOPE_NAMES.NO_PROPAGATION,
      patchType: PATCH_TYPES.FRAMEWORK,
      name: '@loopback/rest/rest.server.updateSpecFromRequest'
    });
  }

  patchDecoratorFactory(xport) {
    // loopback uses the following decorator pattern for declaring
    // route handlers: @operation(method, '/safe', spec)
    //
    // where spec is an object that contains data about how to handle
    // the request.  The implementation of this decorator does a
    // deep clone of the object by default, however the check
    // for whether the object can be cloned breaks with our Object
    // injection.  See NODE-1211 for more details.  Not performing
    // this deep-copy causes data to bleed between routes, causing
    // strange and difficult to debug behavior.
    //
    // This adds the *original* Object to the array of deep-cloneable
    // types (in this context we have the original Object b/c we
    // don't rewrite our own agent code).
    xport.DecoratorFactory._cloneableTypes.push(Object);
  }

  patchServerResponse(xport) {
    patcher.patch(xport.ServerResponse.prototype, 'end', {
      scope: Scopes.SCOPE_NAMES.ALWAYS_RUN,
      patchType: PATCH_TYPES.FRAMEWORK,
      name: 'http.ServerResponse.end - loopback',
      post(data) {
        if (data.args.length < 1) {
          return;
        }
        const body = data.args[0];
        if (isString(body)) {
          agentEmitter.emit(constants.EVENTS.SEND.POST, body);
        }
      }
    });
  }

  patchRestParser(xport) {
    patcher.patch(xport, 'parseOperationArgs', {
      patchType: PATCH_TYPES.ASSESS_SOURCE,
      name: 'loopback4 parseOperationArgs',
      post(data) {
        // wrap input arguments in an object to
        // simulate them being keys on a request object.
        // accessing inputs as properties on a request are fundamental
        // to how both assess and protect work - so this just fakes it.
        const fakeReq = {};
        fakeReq.params = {};
        data.result.then((result) => {
          const route = data.args[1];
          const { spec } = route;
          if (!spec || !spec.parameters) {
            return;
          }
          let requestBodyIndex = 0;
          if (spec.requestBody) {
            if (spec.requestBody[REQUEST_BODY_INDEX]) {
              requestBodyIndex = Math.min(
                spec.requestBody[REQUEST_BODY_INDEX],
                result.length - 1
              );
            }
            fakeReq.body = result[requestBodyIndex];
            agentEmitter.emit(constants.EVENTS.SOURCES.BODY, fakeReq);
            result[requestBodyIndex] = Loopback4Core.getTracked(
              fakeReq,
              'body'
            );
          }
          spec.parameters.forEach((paramSpec, index) => {
            switch (paramSpec.in) {
              case 'path': {
                if (spec.requestBody && index >= requestBodyIndex) {
                  index += 1;
                }
                fakeReq.params[paramSpec.name] = result[index];
                agentEmitter.emit(constants.EVENTS.SOURCES.PARAMS, fakeReq);
                result[index] = Loopback4Core.getTracked(
                  fakeReq.params,
                  paramSpec.name
                );
                break;
              }
            }
          });
        });
      }
    });
  }

  static getTracked(obj, prop) {
    return Scopes.runInAllowAllScope(() => obj[prop]);
  }
}

module.exports = Loopback4Core;
module.exports.constants = constants;
