You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

297 lines
8.2 KiB

Suppress backtrace when delivering toots (#12798) This is to suppress irrelevant backtrace from errors raised when delivering toots to remote servers. The errors are usually out of control by the local server and backtraces don't provide much information. This is similar to https://github.com/tootsuite/mastodon/pull/5174 and shortens backtraces like below: ``` WARN: Mastodon::UnexpectedResponseError: https://example.com/inbox returned code 523 WARN: app/workers/activitypub/delivery_worker.rb:48:in `block (3 levels) in perform_request' app/lib/request.rb:75:in `perform' app/workers/activitypub/delivery_worker.rb:47:in `block (2 levels) in perform_request' app/lib/request_pool.rb:53:in `use' app/lib/request_pool.rb:108:in `block (2 levels) in with' vendor/bundle/ruby/2.7.0/gems/activesupport-5.2.4.1/lib/active_support/notifications.rb:170:in `instrument' app/lib/request_pool.rb:107:in `block in with' app/lib/connection_pool/shared_connection_pool.rb:21:in `block (2 levels) in with' app/lib/connection_pool/shared_connection_pool.rb:20:in `handle_interrupt' app/lib/connection_pool/shared_connection_pool.rb:20:in `block in with' app/lib/connection_pool/shared_connection_pool.rb:16:in `handle_interrupt' app/lib/connection_pool/shared_connection_pool.rb:16:in `with' app/lib/request_pool.rb:106:in `with' app/workers/activitypub/delivery_worker.rb:46:in `block in perform_request' vendor/bundle/ruby/2.7.0/gems/stoplight-2.2.0/lib/stoplight/light/runnable.rb:51:in `run_code' vendor/bundle/ruby/2.7.0/gems/stoplight-2.2.0/lib/stoplight/light/runnable.rb:42:in `run_yellow' vendor/bundle/ruby/2.7.0/gems/stoplight-2.2.0/lib/stoplight/light/runnable.rb:24:in `run' app/workers/activitypub/delivery_worker.rb:57:in `perform_request' app/workers/activitypub/delivery_worker.rb:25:in `perform' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/processor.rb:192:in `execute_job' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/processor.rb:165:in `block (2 levels) in process' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/middleware/chain.rb:128:in `block in invoke' vendor/bundle/ruby/2.7.0/gems/nsa-0.2.7/lib/nsa/collectors/sidekiq.rb:31:in `block in call' vendor/bundle/ruby/2.7.0/gems/nsa-0.2.7/lib/nsa/statsd/publisher.rb:27:in `statsd_time' vendor/bundle/ruby/2.7.0/gems/nsa-0.2.7/lib/nsa/collectors/sidekiq.rb:30:in `call' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/middleware/chain.rb:130:in `block in invoke' app/lib/sidekiq_error_handler.rb:5:in `call' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/middleware/chain.rb:130:in `block in invoke' vendor/bundle/ruby/2.7.0/gems/scout_apm-2.3.0.pre3/lib/scout_apm/background_job_integrations/sidekiq.rb:69:in `call' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/middleware/chain.rb:130:in `block in invoke' vendor/bundle/ruby/2.7.0/gems/sidekiq-unique-jobs-6.0.18/lib/sidekiq_unique_jobs/server/middleware.rb:29:in `call' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/middleware/chain.rb:130:in `block in invoke' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/middleware/chain.rb:133:in `invoke' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/processor.rb:164:in `block in process' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/processor.rb:137:in `block (6 levels) in dispatch' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/job_retry.rb:109:in `local' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/processor.rb:136:in `block (5 levels) in dispatch' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/rails.rb:43:in `block in call' vendor/bundle/ruby/2.7.0/gems/activesupport-5.2.4.1/lib/active_support/execution_wrapper.rb:87:in `wrap' vendor/bundle/ruby/2.7.0/gems/activesupport-5.2.4.1/lib/active_support/reloader.rb:73:in `block in wrap' vendor/bundle/ruby/2.7.0/gems/activesupport-5.2.4.1/lib/active_support/execution_wrapper.rb:87:in `wrap' vendor/bundle/ruby/2.7.0/gems/activesupport-5.2.4.1/lib/active_support/reloader.rb:72:in `wrap' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/rails.rb:42:in `call' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/processor.rb:132:in `block (4 levels) in dispatch' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/processor.rb:250:in `stats' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/processor.rb:127:in `block (3 levels) in dispatch' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/job_logger.rb:8:in `call' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/processor.rb:126:in `block (2 levels) in dispatch' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/job_retry.rb:74:in `global' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/processor.rb:125:in `block in dispatch' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/logging.rb:48:in `with_context' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/logging.rb:42:in `with_job_hash_context' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/processor.rb:124:in `dispatch' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/processor.rb:163:in `process' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/processor.rb:83:in `process_one' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/processor.rb:71:in `run' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/util.rb:16:in `watchdog' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/util.rb:25:in `block in safe_thread' ``` ``` WARN: Stoplight::Error::RedLight: https://example.com/inbox WARN: vendor/bundle/ruby/2.7.0/gems/stoplight-2.2.0/lib/stoplight/light/runnable.rb:46:in `run_red' vendor/bundle/ruby/2.7.0/gems/stoplight-2.2.0/lib/stoplight/light/runnable.rb:25:in `run' app/workers/activitypub/delivery_worker.rb:57:in `perform_request' app/workers/activitypub/delivery_worker.rb:25:in `perform' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/processor.rb:192:in `execute_job' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/processor.rb:165:in `block (2 levels) in process' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/middleware/chain.rb:128:in `block in invoke' vendor/bundle/ruby/2.7.0/gems/nsa-0.2.7/lib/nsa/collectors/sidekiq.rb:31:in `block in call' vendor/bundle/ruby/2.7.0/gems/nsa-0.2.7/lib/nsa/statsd/publisher.rb:27:in `statsd_time' vendor/bundle/ruby/2.7.0/gems/nsa-0.2.7/lib/nsa/collectors/sidekiq.rb:30:in `call' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/middleware/chain.rb:130:in `block in invoke' app/lib/sidekiq_error_handler.rb:5:in `call' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/middleware/chain.rb:130:in `block in invoke' vendor/bundle/ruby/2.7.0/gems/scout_apm-2.3.0.pre3/lib/scout_apm/background_job_integrations/sidekiq.rb:69:in `call' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/middleware/chain.rb:130:in `block in invoke' vendor/bundle/ruby/2.7.0/gems/sidekiq-unique-jobs-6.0.18/lib/sidekiq_unique_jobs/server/middleware.rb:29:in `call' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/middleware/chain.rb:130:in `block in invoke' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/middleware/chain.rb:133:in `invoke' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/processor.rb:164:in `block in process' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/processor.rb:137:in `block (6 levels) in dispatch' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/job_retry.rb:109:in `local' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/processor.rb:136:in `block (5 levels) in dispatch' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/rails.rb:43:in `block in call' vendor/bundle/ruby/2.7.0/gems/activesupport-5.2.4.1/lib/active_support/execution_wrapper.rb:87:in `wrap' vendor/bundle/ruby/2.7.0/gems/activesupport-5.2.4.1/lib/active_support/reloader.rb:73:in `block in wrap' vendor/bundle/ruby/2.7.0/gems/activesupport-5.2.4.1/lib/active_support/execution_wrapper.rb:87:in `wrap' vendor/bundle/ruby/2.7.0/gems/activesupport-5.2.4.1/lib/active_support/reloader.rb:72:in `wrap' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/rails.rb:42:in `call' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/processor.rb:132:in `block (4 levels) in dispatch' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/processor.rb:250:in `stats' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/processor.rb:127:in `block (3 levels) in dispatch' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/job_logger.rb:8:in `call' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/processor.rb:126:in `block (2 levels) in dispatch' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/job_retry.rb:74:in `global' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/processor.rb:125:in `block in dispatch' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/logging.rb:48:in `with_context' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/logging.rb:42:in `with_job_hash_context' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/processor.rb:124:in `dispatch' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/processor.rb:163:in `process' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/processor.rb:83:in `process_one' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/processor.rb:71:in `run' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/util.rb:16:in `watchdog' vendor/bundle/ruby/2.7.0/gems/sidekiq-5.2.7/lib/sidekiq/util.rb:25:in `block in safe_thread' ```
4 years ago
  1. # frozen_string_literal: true
  2. require 'ipaddr'
  3. require 'socket'
  4. require 'resolv'
  5. # Monkey-patch the HTTP.rb timeout class to avoid using a timeout block
  6. # around the Socket#open method, since we use our own timeout blocks inside
  7. # that method
  8. class HTTP::Timeout::PerOperation
  9. def connect(socket_class, host, port, nodelay = false)
  10. @socket = socket_class.open(host, port)
  11. @socket.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, 1) if nodelay
  12. end
  13. end
  14. class Request
  15. REQUEST_TARGET = '(request-target)'
  16. # We enforce a 5s timeout on DNS resolving, 5s timeout on socket opening
  17. # and 5s timeout on the TLS handshake, meaning the worst case should take
  18. # about 15s in total
  19. TIMEOUT = { connect: 5, read: 10, write: 10 }.freeze
  20. include RoutingHelper
  21. def initialize(verb, url, **options)
  22. raise ArgumentError if url.blank?
  23. @verb = verb
  24. @url = Addressable::URI.parse(url).normalize
  25. @http_client = options.delete(:http_client)
  26. @allow_local = options.delete(:allow_local)
  27. @options = options.merge(socket_class: use_proxy? || @allow_local ? ProxySocket : Socket)
  28. @options = @options.merge(proxy_url) if use_proxy?
  29. @headers = {}
  30. raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if block_hidden_service?
  31. set_common_headers!
  32. set_digest! if options.key?(:body)
  33. end
  34. def on_behalf_of(actor, sign_with: nil)
  35. raise ArgumentError, 'actor must not be nil' if actor.nil?
  36. @actor = actor
  37. @keypair = sign_with.present? ? OpenSSL::PKey::RSA.new(sign_with) : @actor.keypair
  38. self
  39. end
  40. def add_headers(new_headers)
  41. @headers.merge!(new_headers)
  42. self
  43. end
  44. def perform
  45. begin
  46. response = http_client.public_send(@verb, @url.to_s, @options.merge(headers: headers))
  47. rescue => e
  48. raise e.class, "#{e.message} on #{@url}", e.backtrace[0]
  49. end
  50. begin
  51. # If we are using a persistent connection, we have to
  52. # read every response to be able to move forward at all.
  53. # However, simply calling #to_s or #flush may not be safe,
  54. # as the response body, if malicious, could be too big
  55. # for our memory. So we use the #body_with_limit method
  56. response.body_with_limit if http_client.persistent?
  57. yield response if block_given?
  58. ensure
  59. http_client.close unless http_client.persistent?
  60. end
  61. end
  62. def headers
  63. (@actor ? @headers.merge('Signature' => signature) : @headers).without(REQUEST_TARGET)
  64. end
  65. class << self
  66. def valid_url?(url)
  67. begin
  68. parsed_url = Addressable::URI.parse(url)
  69. rescue Addressable::URI::InvalidURIError
  70. return false
  71. end
  72. %w(http https).include?(parsed_url.scheme) && parsed_url.host.present?
  73. end
  74. def http_client
  75. HTTP.use(:auto_inflate).timeout(TIMEOUT.dup).follow(max_hops: 3)
  76. end
  77. end
  78. private
  79. def set_common_headers!
  80. @headers[REQUEST_TARGET] = "#{@verb} #{@url.path}"
  81. @headers['User-Agent'] = Mastodon::Version.user_agent
  82. @headers['Host'] = @url.host
  83. @headers['Date'] = Time.now.utc.httpdate
  84. @headers['Accept-Encoding'] = 'gzip' if @verb != :head
  85. end
  86. def set_digest!
  87. @headers['Digest'] = "SHA-256=#{Digest::SHA256.base64digest(@options[:body])}"
  88. end
  89. def signature
  90. algorithm = 'rsa-sha256'
  91. signature = Base64.strict_encode64(@keypair.sign(OpenSSL::Digest.new('SHA256'), signed_string))
  92. "keyId=\"#{key_id}\",algorithm=\"#{algorithm}\",headers=\"#{signed_headers.keys.join(' ').downcase}\",signature=\"#{signature}\""
  93. end
  94. def signed_string
  95. signed_headers.map { |key, value| "#{key.downcase}: #{value}" }.join("\n")
  96. end
  97. def signed_headers
  98. @headers.without('User-Agent', 'Accept-Encoding')
  99. end
  100. def key_id
  101. ActivityPub::TagManager.instance.key_uri_for(@actor)
  102. end
  103. def http_client
  104. @http_client ||= Request.http_client
  105. end
  106. def use_proxy?
  107. proxy_url.present?
  108. end
  109. def proxy_url
  110. if hidden_service? && Rails.configuration.x.http_client_hidden_proxy.present?
  111. Rails.configuration.x.http_client_hidden_proxy
  112. else
  113. Rails.configuration.x.http_client_proxy
  114. end
  115. end
  116. def block_hidden_service?
  117. !Rails.configuration.x.access_to_hidden_service && hidden_service?
  118. end
  119. def hidden_service?
  120. /\.(onion|i2p)$/.match?(@url.host)
  121. end
  122. module ClientLimit
  123. def truncated_body(limit = 1.megabyte)
  124. if charset.nil?
  125. encoding = Encoding::BINARY
  126. else
  127. begin
  128. encoding = Encoding.find(charset)
  129. rescue ArgumentError
  130. encoding = Encoding::BINARY
  131. end
  132. end
  133. contents = String.new(encoding: encoding)
  134. while (chunk = readpartial)
  135. contents << chunk
  136. chunk.clear
  137. break if contents.bytesize > limit
  138. end
  139. contents
  140. end
  141. def body_with_limit(limit = 1.megabyte)
  142. raise Mastodon::LengthValidationError if content_length.present? && content_length > limit
  143. contents = truncated_body(limit)
  144. raise Mastodon::LengthValidationError if contents.bytesize > limit
  145. contents
  146. end
  147. end
  148. if ::HTTP::Response.methods.include?(:body_with_limit) && !Rails.env.production?
  149. abort 'HTTP::Response#body_with_limit is already defined, the monkey patch will not be applied'
  150. else
  151. class ::HTTP::Response
  152. include Request::ClientLimit
  153. end
  154. end
  155. class Socket < TCPSocket
  156. class << self
  157. def open(host, *args)
  158. outer_e = nil
  159. port = args.first
  160. addresses = []
  161. begin
  162. addresses = [IPAddr.new(host)]
  163. rescue IPAddr::InvalidAddressError
  164. Resolv::DNS.open do |dns|
  165. dns.timeouts = 5
  166. addresses = dns.getaddresses(host)
  167. addresses = addresses.filter { |addr| addr.is_a?(Resolv::IPv6) }.take(2) + addresses.filter { |addr| !addr.is_a?(Resolv::IPv6) }.take(2)
  168. end
  169. end
  170. socks = []
  171. addr_by_socket = {}
  172. addresses.each do |address|
  173. check_private_address(address, host)
  174. sock = ::Socket.new(address.is_a?(Resolv::IPv6) ? ::Socket::AF_INET6 : ::Socket::AF_INET, ::Socket::SOCK_STREAM, 0)
  175. sockaddr = ::Socket.pack_sockaddr_in(port, address.to_s)
  176. sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1)
  177. sock.connect_nonblock(sockaddr)
  178. # If that hasn't raised an exception, we somehow managed to connect
  179. # immediately, close pending sockets and return immediately
  180. socks.each(&:close)
  181. return sock
  182. rescue IO::WaitWritable
  183. socks << sock
  184. addr_by_socket[sock] = sockaddr
  185. rescue => e
  186. outer_e = e
  187. end
  188. until socks.empty?
  189. _, available_socks, = IO.select(nil, socks, nil, Request::TIMEOUT[:connect])
  190. if available_socks.nil?
  191. socks.each(&:close)
  192. raise HTTP::TimeoutError, "Connect timed out after #{Request::TIMEOUT[:connect]} seconds"
  193. end
  194. available_socks.each do |sock|
  195. socks.delete(sock)
  196. begin
  197. sock.connect_nonblock(addr_by_socket[sock])
  198. rescue Errno::EISCONN
  199. # Do nothing
  200. rescue => e
  201. sock.close
  202. outer_e = e
  203. next
  204. end
  205. socks.each(&:close)
  206. return sock
  207. end
  208. end
  209. if outer_e
  210. raise outer_e
  211. else
  212. raise SocketError, "No address for #{host}"
  213. end
  214. end
  215. alias new open
  216. def check_private_address(address, host)
  217. addr = IPAddr.new(address.to_s)
  218. return if private_address_exceptions.any? { |range| range.include?(addr) }
  219. raise Mastodon::PrivateNetworkAddressError, host if PrivateAddressCheck.private_address?(addr)
  220. end
  221. def private_address_exceptions
  222. @private_address_exceptions = (ENV['ALLOWED_PRIVATE_ADDRESSES'] || '').split(',').map { |addr| IPAddr.new(addr) }
  223. end
  224. end
  225. end
  226. class ProxySocket < Socket
  227. class << self
  228. def check_private_address(_address, _host)
  229. # Accept connections to private addresses as HTTP proxies will usually
  230. # be on local addresses
  231. nil
  232. end
  233. end
  234. end
  235. private_constant :ClientLimit, :Socket, :ProxySocket
  236. end