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

cs__scoped_require 'contrast/utils/object_share'
cs__scoped_require 'contrast/utils/random_util'

# This class is used by the CSRF rule to inject the Contrast CSRF token into
# the Response's body, allowing for the detection of CSRF attacks.
class Contrast::Agent::Protect::Rule::Csrf::CsrfTokenInjector # rubocop:disable Style/ClassAndModuleChildren
  TKN_NAME = 'cs_csrf_tkn'
  def token_name
    TKN_NAME
  end

  SESSION_TOKEN_PREFIX = '__CONTRAST__'
  def token_key
    @_token_key ||= SESSION_TOKEN_PREFIX + token_name
  end

  def javascript
    @_javascript ||= build_javascript
  end

  SCRIPT_LOC = File.join('csrf', 'inject.js').cs__freeze
  NAME_MARKER = '!TOKEN_NAME!'
  VALUE_MARKER = '!TOKEN_VALUE!'
  def build_javascript
    script = Contrast::Utils::ResourceLoader.load(SCRIPT_LOC)
    script&.sub!(NAME_MARKER, TKN_NAME)
    script
  end

  CSRF_TOKEN_LENGTH = 8
  def get_expected_token context
    cookies = context.request.request_cookies
    token = cookies[token_key]
    return token if token
    return unless context.response

    build_token(context)
  end

  def build_token context
    token = Contrast::Utils::RandomUtil.secure_random_string(CSRF_TOKEN_LENGTH)
    context&.response&.set_header(token_key, token)
    token
  end

  CONTENT_TYPE = 'CONTENT-TYPE'

  def wedge_token? response
    return true unless response

    content_type = response.header(CONTENT_TYPE)
    return true unless content_type

    content_type = content_type.to_s
    !content_type.start_with?(*DATA_CONTENT_TYPES)
  end

  END_BODY_TAG = %r{</body>}i.cs__freeze
  def do_injection context
    return unless wedge_token?(context&.response)

    body_string = context&.response&.body
    return unless body_string

    index = body_string.index(END_BODY_TAG)
    return unless index

    injection = get_expected_token(context)
    return unless injection

    injection = javascript.sub(VALUE_MARKER, injection)
    body_string.insert(index, injection)
    context&.response&.update_body(body_string)
  end

  DATA_CONTENT_TYPES = [
    'image/', 'audio/', 'video/',
    'application/json', 'application/xml',
    'application/octet', 'application/force',
    'text/json', 'text/xml'
  ].cs__freeze
end