# frozen_string_literal: true module Sentry class RequestInterface < Interface REQUEST_ID_HEADERS = %w[action_dispatch.request_id HTTP_X_REQUEST_ID].freeze CONTENT_HEADERS = %w[CONTENT_TYPE CONTENT_LENGTH].freeze IP_HEADERS = [ "REMOTE_ADDR", "HTTP_CLIENT_IP", "HTTP_X_REAL_IP", "HTTP_X_FORWARDED_FOR" ].freeze # See Sentry server default limits at # https://github.com/getsentry/sentry/blob/master/src/sentry/conf/server.py MAX_BODY_LIMIT = 4096 * 4 # @return [String] attr_accessor :url # @return [String] attr_accessor :method # @return [Hash] attr_accessor :data # @return [String] attr_accessor :query_string # @return [String] attr_accessor :cookies # @return [Hash] attr_accessor :headers # @return [Hash] attr_accessor :env # @param env [Hash] # @param send_default_pii [Boolean] # @param rack_env_whitelist [Array] # @see Configuration#send_default_pii # @see Configuration#rack_env_whitelist def initialize(env:, send_default_pii:, rack_env_whitelist:) env = env.dup unless send_default_pii # need to completely wipe out ip addresses RequestInterface::IP_HEADERS.each do |header| env.delete(header) end end request = ::Rack::Request.new(env) if send_default_pii self.data = read_data_from(request) self.cookies = request.cookies self.query_string = request.query_string end self.url = request.scheme && request.url.split("?").first self.method = request.request_method self.headers = filter_and_format_headers(env, send_default_pii) self.env = filter_and_format_env(env, rack_env_whitelist) end private def read_data_from(request) if request.form_data? request.POST elsif request.body # JSON requests, etc data = request.body.read(MAX_BODY_LIMIT) data = Utils::EncodingHelper.encode_to_utf_8(data.to_s) request.body.rewind data end rescue IOError => e e.message end def filter_and_format_headers(env, send_default_pii) env.each_with_object({}) do |(key, value), memo| begin key = key.to_s # rack env can contain symbols next memo["X-Request-Id"] ||= Utils::RequestId.read_from(env) if Utils::RequestId::REQUEST_ID_HEADERS.include?(key) next if is_server_protocol?(key, value, env["SERVER_PROTOCOL"]) next if is_skippable_header?(key) next if key == "HTTP_AUTHORIZATION" && !send_default_pii # Rack stores headers as HTTP_WHAT_EVER, we need What-Ever key = key.sub(/^HTTP_/, "") key = key.split("_").map(&:capitalize).join("-") memo[key] = Utils::EncodingHelper.encode_to_utf_8(value.to_s) rescue StandardError => e # Rails adds objects to the Rack env that can sometimes raise exceptions # when `to_s` is called. # See: https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/remote_ip.rb#L134 Sentry.logger.warn(LOGGER_PROGNAME) { "Error raised while formatting headers: #{e.message}" } next end end end def is_skippable_header?(key) key.upcase != key || # lower-case envs aren't real http headers key == "HTTP_COOKIE" || # Cookies don't go here, they go somewhere else !(key.start_with?("HTTP_") || CONTENT_HEADERS.include?(key)) end # In versions < 3, Rack adds in an incorrect HTTP_VERSION key, which causes downstream # to think this is a Version header. Instead, this is mapped to # env['SERVER_PROTOCOL']. But we don't want to ignore a valid header # if the request has legitimately sent a Version header themselves. # See: https://github.com/rack/rack/blob/028438f/lib/rack/handler/cgi.rb#L29 def is_server_protocol?(key, value, protocol_version) rack_version = Gem::Version.new(::Rack.release) return false if rack_version >= Gem::Version.new("3.0") key == "HTTP_VERSION" && value == protocol_version end def filter_and_format_env(env, rack_env_whitelist) return env if rack_env_whitelist.empty? env.select do |k, _v| rack_env_whitelist.include? k.to_s end end end end