1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144
|
# 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 = 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] = 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 encode_to_utf_8(value)
if value.encoding != Encoding::UTF_8 && value.respond_to?(:force_encoding)
value = value.dup.force_encoding(Encoding::UTF_8)
end
if !value.valid_encoding?
value = value.scrub
end
value
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
# 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
# NOTE: This will be removed in version 3.0+
def is_server_protocol?(key, value, protocol_version)
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
|