# Copyright (c) 2020 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/interface'
require 'contrast/utils/heap_dump_util'
require 'contrast/agent/request_handler'
require 'contrast/agent/static_analysis'

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
      include Contrast::Components::Interface
      access_component :agent, :config, :logging, :scope, :settings

      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 AGENT.enabled?
          logger.error(
              'The Agent was unable to initialize before the application middleware was initialized. Disabling permanently.')
          AGENT.disable! # ensure the agent is disabled (probably redundant)
          return
        end
        agent_startup_routine
      end

      # Startup the Agent as part of the initialization process:
      # - start the service sending thread, responsible for sending and
      #   processing messages
      # - start the heartbeat thread, which triggers service startup
      # - 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 service') do
          Contrast::Agent.thread_watcher.ensure_running?
        end

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

        AGENT.enable_tracepoint
        Contrast::Agent::AtExitHook.exit_hook
      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.
      #
      # @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
        Contrast::Utils::HeapDumpUtil.run

        if AGENT.enabled?
          Contrast::Agent::StaticAnalysis.catchup
          call_with_agent(env)
        else
          app.call(env)
        end
      end

      private

      def setup_agent
        SETTINGS.reset_state

        if CONFIG.invalid?
          AGENT.disable!
          logger.error('!!! CONFIG FILE IS INVALID - DISABLING CONTRAST AGENT !!!')
        elsif AGENT.disabled?
          logger.warn('Contrast disabled by configuration. Continuing without instrumentation.')
        else
          AGENT.enable!
        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.
      def call_with_agent env
        Contrast::Agent.thread_watcher.ensure_running?
        return unless AGENT.enabled?

        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)

          # prefilter sequence
          with_contrast_scope do
            context.service_extract_request
            request_handler.ruleset.prefilter
          end

          response = application_code(env) # pass request down the Rack chain with original env

          # postfilter sequence
          with_contrast_scope do
            context.extract_after(response) # update context with final response information

            if Contrast::Agent.framework_manager.streaming?(env)
              context.reset_activity
              request_handler.stream_safe_postfilter
            else
              request_handler.ruleset.postfilter
              # return response stored in the context in case any postfilter rules updated the response data
              response = context.response&.rack_response || response
              request_handler.send_activity_messages
            end
          end
        ensure
          logger.request_end
        end

        response
      rescue StandardError => e
        handle_exception(e)
      end

      def application_code env
        logger.trace_with_time('application') do
          app.call(env)
        end
      rescue Contrast::SecurityException => e
        logger.trace('Security Exception raised during application lifecycle to prevent an attack', e)
        raise e
      end

      SECURITY_EXCEPTION_MARKER = 'Contrast::SecurityException'
      # We're only going to suppress SecurityExceptions indicating a blocked attack.
      # And, only if the config.agent.ruby.exceptions.capture? is set
      def handle_exception exception
        if exception.is_a?(Contrast::SecurityException) ||
              exception.message&.include?(SECURITY_EXCEPTION_MARKER)

          exception_control = AGENT.exception_control
          raise exception unless exception_control[:enable]

          [exception_control[:status], {}, [exception_control[:message]]]
        else
          logger.debug('Re-throwing original error', exception)
          raise exception
        end
      end
    end
  end
end