File: request.rb

package info (click to toggle)
ruby-sentry-ruby-core 5.3.0-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, forky, sid, trixie
  • size: 412 kB
  • sloc: ruby: 3,030; makefile: 8; sh: 4
file content (144 lines) | stat: -rw-r--r-- 4,522 bytes parent folder | download
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