# Copyright (c) 2023 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details.
# frozen_string_literal: true

require 'ipaddr'
require 'json'
require 'rack'

require 'contrast/security_exception'
require 'contrast/utils/object_share'
require 'contrast/components/logger'
require 'contrast/components/scope'
require 'contrast/utils/heap_dump_util'
require 'contrast/agent/telemetry/telemetry'
require 'contrast/agent/request/request_handler'
require 'contrast/agent/middleware/static_analysis'
require 'contrast/agent/telemetry/startup_metrics_event'
require 'contrast/agent/protect/input_analyzer/input_analyzer'
require 'contrast/utils/middleware_utils'
require 'contrast/utils/reporting/application_activity_batch_utils'
require 'contrast/utils/timer'

module Contrast
  module Agent
    # This class allows the Agent to plug into the Rack middleware stack. When the application is first started, we
    # initialize ourselves as a rack middleware inside of #initialize. Afterwards, we process each http request and
    # response as it goes through the middleware stack inside of #call.
    class Middleware # rubocop:disable Metrics/ClassLength
      include Contrast::Components::Logger::InstanceMethods
      include Contrast::Components::Scope::InstanceMethods
      include Contrast::Utils::MiddlewareUtils
      include Contrast::Utils::Reporting::ApplicationActivityBatchUtils

      attr_reader :app

      # Allows the Agent to function as a middleware. We perform all our one-time whole-app routines in here since
      # we're only going to be initialized a single time. Our initialization order is:
      # - capture the application
      # - setup the Agent
      # - startup the Agent
      #
      # @param app [Rack::Application] the application to be instrumented
      # @param _legacy_param [nil] was a flag we no longer need, but Sinatra may call it
      def initialize app, _legacy_param = nil
        @app = app  # THIS MUST BE FIRST AND ALWAYS SET!
        setup_agent # THIS MUST BE SECOND AND ALWAYS CALLED!
        unless ::Contrast::AGENT.enabled?
          logger.error('The Agent was unable to initialize before the application middleware was initialized. ' \
                       'Disabling permanently.')
          ::Contrast::AGENT.disable! # ensure the agent is disabled (probably redundant)
          return
        end
        agent_startup_routine
      rescue StandardError => e
        logger.error('Unable to initialize the agent. Disabling permanently.', e)
        ::Contrast::AGENT.disable! # ensure the agent is disabled (probably redundant)
      end

      # This is where we're hooked into the middleware stack. If the agent is enabled, we're ready to do some
      # processing on a per request basis. If not, we just pass the request along to the next middleware in the stack.
      # If Application Scope feature is enabled we execute the env call with that scope, enabling only analysis for
      # the current application, and nothing outside that.
      #
      # @param env [Hash] the various variables stored by this and other Middlewares to know the state and values of
      #   this Request
      # @return [Array,Rack::Response] the Response of this and subsequent Middlewares to be passed back to the user up
      #   the Rack framework.
      def call env
        return with_app_scope { call_routine(env) } if Contrast::RUBY_INTERFACE.start_with_application_scope?

        call_routine(env)
      end

      private

      # This is the call routine we do when we are hooked to the middleware stack.
      #
      # @param env [Hash] the various variables stored by this and other Middlewares to know the state and values of
      #   this Request
      # @return [Array,Rack::Response] the Response of this and subsequent Middlewares to be passed back to the user up
      #   the Rack framework.
      def call_routine env
        logger.trace_with_time('Elapsed time for Contrast::Agent::Middleware#call') do
          return app.call(env) unless ::Contrast::AGENT.enabled?

          Contrast::Agent.heapdump_util.start_thread!
          handle_first_request
          call_with_agent(env)
        end
      end

      # Startup the Agent as part of the initialization process:
      # - start the TeamServer sending thread, responsible for sending and processing messages
      # - start the heartbeat thread, which handles periodic messages to TeamServer
      # - start instrumenting libraries and do a 'catchup' patch for everything we didn't see get loaded
      # - enable TracePoint, which handles all class loads and required instrumentation going forward
      def agent_startup_routine
        logger.debug_with_time('middleware: starting reporting threads') do
          Contrast::Agent.thread_watcher.ensure_running?
        end

        if Contrast::Agent::Telemetry::Base.enabled?
          logger.debug_with_time('middleware: sending startup metrics telemetry event') do
            event = Contrast::Agent::Telemetry::StartupMetricsEvent.new
            Contrast::Agent.thread_watcher.telemetry_queue.send_event(event)
          end
        end

        logger.debug_with_time('middleware: instrument shared libraries and patch') do
          Contrast::Agent::Patching::Policy::Patcher.patch
        end

        logger.debug_with_time('middleware: enabling tracepoint') do
          ::Contrast::AGENT.enable_tracepoint
        end
        Contrast::Agent::AtExitHook.exit_hook
      end

      # Some things have to wait until first request to happen, either because resolution is not complete or because
      # the framework will preload classes, which confuses some of our instrumentation.
      def handle_first_request
        @_handle_first_request ||= begin
          Contrast::Agent::StaticAnalysis.catchup
          true
        end
      end

      # This is where we process each request we intercept as a middleware. We make the request context available
      # globally so that it can be accessed from anywhere. A RequestHandler object is made for each request, which
      # handles prefilter and postfilter operations.
      # @param env [Hash] the various variables stored by this and other Middlewares to know the state and values of
      #   this Request
      # @return [Array,Rack::Response] the Response of this and subsequent Middlewares to be passed back to the user
      #   up the Rack framework.
      def call_with_agent env
        Contrast::Agent.thread_watcher.ensure_running?
        framework_request = Contrast::Agent.framework_manager.retrieve_request(env)
        context = Contrast::Agent::RequestContext.new(framework_request)
        response = nil

        # Make the context available for the lifecycle of this request.
        Contrast::Agent::REQUEST_TRACKER.lifespan(context) do
          logger.request_start
          request_handler = Contrast::Agent::RequestHandler.new(context)

          pre_call_with_agent(context, request_handler)
          response = application_code(env)
          post_call_with_agent(context, env, request_handler, response)
        ensure
          logger.request_end
        end

        response
      rescue StandardError => e
        handle_exception(e)
      end

      # Handle the operations the Agent needs to accomplish prior to the Application code executing during this
      # request.
      #
      # @param context [Contrast::Agent::RequestContext]
      # @param request_handler [Contrast::Agent::RequestHandler]
      # @raise [StandardError] raises an error if the exception is security concern
      #    which is being triggered when there is a failure within the pre-call with the agent
      def pre_call_with_agent context, request_handler
        with_contrast_scope do
          context.protect_input_analysis
          request_handler.ruleset.prefilter
        end
      rescue StandardError => e
        raise(e) if security_exception?(e)

        logger.error('Unable to execute agent pre_call', e)
      end

      # Handle the operations the Agent needs to accomplish after the Application code executes during this request.
      #
      # @param context [Contrast::Agent::RequestContext]
      # @param env [Hash] the various variables stored by this and other Middlewares to know the state and values of
      #   this Request
      # @param request_handler [Contrast::Agent::RequestHandler]
      # @param response [Array,Rack::Response]
      # @raise [StandardError] raises an error if the exception is security concern
      #    which is being triggered when there is a failure within the post-call with the agent
      def post_call_with_agent context, env, request_handler, response
        with_contrast_scope do
          context.extract_after(response) # update context with final response information

          Contrast::Agent::FINDINGS.report_collected_findings unless Contrast::Agent::FINDINGS.collection.empty?
          # All protect rules, which are trigger but require response to be reported
          Contrast::Agent::EXPLOITS.report_recorded_exploits(context) unless Contrast::Agent::EXPLOITS.collection.empty?
          # Now we can build the ia_results only for postfilter rules.
          context.protect_postfilter_ia
          # Process Worth Watching Inputs for v2 rules
          Contrast::Agent.worth_watching_analyzer&.add_to_queue(context.agent_input_analysis)

          if Contrast::Agent.framework_manager.streaming?(env)
            context.reset_activity
            request_handler.stream_safe_postfilter
          else
            request_handler.ruleset.postfilter
            request_handler.report_observed_route
            add_activity_to_batch(context.activity)
            report_batch
          end
        end
      # unsuccessful attack
      rescue StandardError => e
        raise(e) if security_exception?(e)

        logger.error('Unable to execute agent post_call', e)
      end
    end
  end
end