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 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
|
# frozen_string_literal: true
require "net/http"
require "zlib"
module Sentry
class HTTPTransport < Transport
GZIP_ENCODING = "gzip"
GZIP_THRESHOLD = 1024 * 30
CONTENT_TYPE = 'application/x-sentry-envelope'
DEFAULT_DELAY = 60
RETRY_AFTER_HEADER = "retry-after"
RATE_LIMIT_HEADER = "x-sentry-rate-limits"
USER_AGENT = "sentry-ruby/#{Sentry::VERSION}"
# The list of errors ::Net::HTTP is known to raise
# See https://github.com/ruby/ruby/blob/b0c639f249165d759596f9579fa985cb30533de6/lib/bundler/fetcher.rb#L281-L286
HTTP_ERRORS = [
Timeout::Error, EOFError, SocketError, Errno::ENETDOWN, Errno::ENETUNREACH,
Errno::EINVAL, Errno::ECONNRESET, Errno::ETIMEDOUT, Errno::EAGAIN,
Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError,
Zlib::BufError, Errno::EHOSTUNREACH, Errno::ECONNREFUSED
].freeze
def initialize(*args)
super
log_debug("Sentry HTTP Transport will connect to #{@dsn.server}") if @dsn
end
def send_data(data)
encoding = ""
if should_compress?(data)
data = Zlib.gzip(data)
encoding = GZIP_ENCODING
end
headers = {
'Content-Type' => CONTENT_TYPE,
'Content-Encoding' => encoding,
'User-Agent' => USER_AGENT
}
auth_header = generate_auth_header
headers['X-Sentry-Auth'] = auth_header if auth_header
response = conn.start do |http|
request = ::Net::HTTP::Post.new(endpoint, headers)
request.body = data
http.request(request)
end
if response.code.match?(/\A2\d{2}/)
handle_rate_limited_response(response) if has_rate_limited_header?(response)
elsif response.code == "429"
log_debug("the server responded with status 429")
handle_rate_limited_response(response)
else
error_info = "the server responded with status #{response.code}"
error_info += "\nbody: #{response.body}"
error_info += " Error in headers is: #{response['x-sentry-error']}" if response['x-sentry-error']
raise Sentry::ExternalError, error_info
end
rescue SocketError, *HTTP_ERRORS => e
on_error if respond_to?(:on_error)
raise Sentry::ExternalError.new(e&.message)
end
def endpoint
@dsn.envelope_endpoint
end
def generate_auth_header
return nil unless @dsn
now = Sentry.utc_now.to_i
fields = {
'sentry_version' => PROTOCOL_VERSION,
'sentry_client' => USER_AGENT,
'sentry_timestamp' => now,
'sentry_key' => @dsn.public_key
}
fields['sentry_secret'] = @dsn.secret_key if @dsn.secret_key
'Sentry ' + fields.map { |key, value| "#{key}=#{value}" }.join(', ')
end
def conn
server = URI(@dsn.server)
# connection respects proxy setting from @transport_configuration, or environment variables (HTTP_PROXY, HTTPS_PROXY, NO_PROXY)
# Net::HTTP will automatically read the env vars.
# See https://ruby-doc.org/3.2.2/stdlibs/net/Net/HTTP.html#class-Net::HTTP-label-Proxies
connection =
if proxy = normalize_proxy(@transport_configuration.proxy)
::Net::HTTP.new(server.hostname, server.port, proxy[:uri].hostname, proxy[:uri].port, proxy[:user], proxy[:password])
else
::Net::HTTP.new(server.hostname, server.port)
end
connection.use_ssl = server.scheme == "https"
connection.read_timeout = @transport_configuration.timeout
connection.write_timeout = @transport_configuration.timeout if connection.respond_to?(:write_timeout)
connection.open_timeout = @transport_configuration.open_timeout
ssl_configuration.each do |key, value|
connection.send("#{key}=", value)
end
connection
end
private
def has_rate_limited_header?(headers)
headers[RETRY_AFTER_HEADER] || headers[RATE_LIMIT_HEADER]
end
def handle_rate_limited_response(headers)
rate_limits =
if rate_limits = headers[RATE_LIMIT_HEADER]
parse_rate_limit_header(rate_limits)
elsif retry_after = headers[RETRY_AFTER_HEADER]
# although Sentry doesn't send a date string back
# based on HTTP specification, this could be a date string (instead of an integer)
retry_after = retry_after.to_i
retry_after = DEFAULT_DELAY if retry_after == 0
{ nil => Time.now + retry_after }
else
{ nil => Time.now + DEFAULT_DELAY }
end
rate_limits.each do |category, limit|
if current_limit = @rate_limits[category]
if current_limit < limit
@rate_limits[category] = limit
end
else
@rate_limits[category] = limit
end
end
end
def parse_rate_limit_header(rate_limit_header)
time = Time.now
result = {}
limits = rate_limit_header.split(",")
limits.each do |limit|
next if limit.nil? || limit.empty?
begin
retry_after, categories = limit.strip.split(":").first(2)
retry_after = time + retry_after.to_i
categories = categories.split(";")
if categories.empty?
result[nil] = retry_after
else
categories.each do |category|
result[category] = retry_after
end
end
rescue StandardError
end
end
result
end
def should_compress?(data)
@transport_configuration.encoding == GZIP_ENCODING && data.bytesize >= GZIP_THRESHOLD
end
# @param proxy [String, URI, Hash] Proxy config value passed into `config.transport`.
# Accepts either a URI formatted string, URI, or a hash with the `uri`, `user`, and `password` keys.
# @return [Hash] Normalized proxy config that will be passed into `Net::HTTP`
def normalize_proxy(proxy)
return proxy unless proxy
case proxy
when String
uri = URI(proxy)
{ uri: uri, user: uri.user, password: uri.password }
when URI
{ uri: proxy, user: proxy.user, password: proxy.password }
when Hash
proxy
end
end
def ssl_configuration
configuration = {
verify: @transport_configuration.ssl_verification,
ca_file: @transport_configuration.ssl_ca_file
}.merge(@transport_configuration.ssl || {})
configuration[:verify_mode] = configuration.delete(:verify) ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE
configuration
end
end
end
|