# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'resolv' require 'timeout' require 'contrast/utils/object_share' require 'contrast/utils/string_utils' require 'contrast/utils/hash_digest' require 'contrast/components/logger' require 'contrast/components/scope' require 'contrast/utils/request_utils' module Contrast module Agent # This class is the Contrast representation of the Rack::Request object. It # provides access to the original Rack::Request object as well as extracts # data in a format that the Agent expects, caching those transformations in # order to avoid repeatedly creating Strings & thrashing GC. # # @attr_reader rack_request [Rack::Request] The passed to the Agent RackRequest to be wrapped. # @attr_accessor route [Contrast::Api::Dtm::RouteCoverage] the route, used for findings, of this request # @attr_accessor observed_route [Contrast::Api::Dtm::ObservedRoute] the route, used for coverage of this request # @attr_accessor new_observed_route [Contrast::Agent::Reporting::ObservedRoute] the route, used for coverage, of # this request class Request include Contrast::Utils::RequestUtils include Contrast::Components::Logger::InstanceMethods include Contrast::Components::Scope::InstanceMethods extend Forwardable INNER_REST_TOKEN = %r{/\d+/}.cs__freeze LAST_REST_TOKEN = %r{/\d+$}.cs__freeze INNER_NUMBER_MARKER = '/{n}/' LAST_NUMBER_MARKER = '/{n}' attr_reader :rack_request attr_accessor :route, :observed_route, :new_observed_route # Delegate calls to the following methods to the attribute @rack_request def_delegators :@rack_request, :base_url, :cookies, :env, :ip, :media_type, :path, :port, :query_string, :request_method, :scheme, :url, :user_agent # Initialize new Contrast Request # # @param rack_request [Rack::Request] The passed to the Agent RackRequest to be wrapped. def initialize rack_request @rack_request = rack_request end # The HTTP version of this request # @return [String] def version env = rack_request.env return '1.1' unless env version = env['HTTP_VERSION'] return '1.1' unless version version.split('/')[-1] end # Returns a normalized form of the URI. In "normal" URIs # this will return an unchanged String, but in REST-y # URIs this will normalize the digit path tokens, e.g.: # # /accounts/5/view # ...becomes: # /accounts/{n}/view # # Should also handle the ;jsessionid. # # @return uri [String] def normalized_uri @_normalized_uri ||= begin path = rack_request.path_info uri = path.split(Contrast::Utils::ObjectShare::SEMICOLON)[0] # remove ;jsessionid uri = uri.split(Contrast::Utils::ObjectShare::QUESTION_MARK)[0] # remove ?query_string= uri.gsub(INNER_REST_TOKEN, INNER_NUMBER_MARKER) # replace interior tokens uri.gsub(LAST_REST_TOKEN, LAST_NUMBER_MARKER) # replace last token end end # Returns the request file type # # @return type [Symbol<:XML, :JSON, :NORMAL>] def document_type @_document_type ||= if /xml/i.match?(media_type) || body&.start_with?(' _e logger.warn('Unable to parse multipart request!') {} end end # returns or fenerates the hash checksum for the request # # @return @_hash_id [String] Contrast::Utils::HashDigest generated string checksum def hash_id @_hash_id ||= Contrast::Utils::HashDigest.generate_request_hash(self) end STATIC_SUFFIXES = /\.(?:js|css|jpeg|jpg|gif|png|ico|woff|svg|pdf|eot|ttf|jar)$/i.cs__freeze MEDIA_TYPE_MARKERS = %w[image/ text/css text/javascript].cs__freeze # Utility method for checking if a request is for a static resource. # @return [Boolean] true, if the request is for a well-known static # type as determined by the request suffix or the accept header. def static? return true if normalized_uri&.match?(STATIC_SUFFIXES) accepts = Array(headers['ACCEPT'])&.first&.to_s return false unless accepts return false if accepts.start_with?('*/*') accepts.start_with?(*MEDIA_TYPE_MARKERS) end end end end