# Copyright (c) 2021 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/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::Logger::InstanceMethods
      include Contrast::Components::Scope::InstanceMethods

      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
      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
        return app.call(env) unless ::Contrast::AGENT.enabled?

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

      private

      def setup_agent
        ::Contrast::SETTINGS.reset_state

        inform_deprecations

        if ::Contrast::CONFIG.invalid?
          ::Contrast::AGENT.disable!
          logger.error('!!! CONFIG FILE IS INVALID - DISABLING CONTRAST AGENT !!!')
        elsif ::Contrast::AGENT.disabled?
          logger.warn('Contrast disabled by configuration. Continuing without instrumentation.')
        else
          ::Contrast::AGENT.enable!
        end
      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

        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]
      def pre_call_with_agent context, request_handler
        with_contrast_scope do
          context.service_extract_request
          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]
      def post_call_with_agent context, env, request_handler, response
        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
            request_handler.send_activity_messages
          end
        end
      rescue StandardError => e
        raise e if security_exception?(e)

        logger.error('Unable to execute agent post_call', 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 security_exception?(exception)
          exception_control = ::Contrast::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

      # Is the given exception one raised by our Protect code?
      #
      # @param exception [Exception]
      # @return [Boolean]
      def security_exception? exception
        exception.is_a?(Contrast::SecurityException) || exception.message&.include?(SECURITY_EXCEPTION_MARKER)
      end

      # As we deprecate support to prepare to remove dead code, we need to inform our users still relying on the now
      # deprecated and soon to be removed functionality. This method handles doing that by leveraging the standard
      # Kernel#warn approach
      def inform_deprecations
        # Ruby 2.5 is currently in security maintenance, meaning int is only receiving updates for security issues. It
        # will move to eol on 31 March 2021. As such, we can remove support for it in Q3. We'll begin the deprecation
        # warnings now so that customers have time to reach out if they'll be impacted.
        # TODO: RUBY-715 remove this part of the method, leaving it empty if there are no other deprecations, when we
        #   drop 2.5 support.
        return unless RUBY_VERSION < '2.6.0'

        Kernel.warn('[Contrast Security] [DEPRECATION] Support for Ruby 2.5 will be removed in April 2021. '\
                    'Please contact Customer Support prior if you require continued support.')
      end
    end
  end
end