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.

294 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 body_with_limit(limit = 1.megabyte)
  124. raise Mastodon::LengthValidationError if content_length.present? && content_length > limit
  125. if charset.nil?
  126. encoding = Encoding::BINARY
  127. else
  128. begin
  129. encoding = Encoding.find(charset)
  130. rescue ArgumentError
  131. encoding = Encoding::BINARY
  132. end
  133. end
  134. contents = String.new(encoding: encoding)
  135. while (chunk = readpartial)
  136. contents << chunk
  137. chunk.clear
  138. raise Mastodon::LengthValidationError if contents.bytesize > limit
  139. end
  140. contents
  141. end
  142. end
  143. if ::HTTP::Response.methods.include?(:body_with_limit) && !Rails.env.production?
  144. abort 'HTTP::Response#body_with_limit is already defined, the monkey patch will not be applied'
  145. else
  146. class ::HTTP::Response
  147. include Request::ClientLimit
  148. end
  149. end
  150. class Socket < TCPSocket
  151. class << self
  152. def open(host, *args)
  153. outer_e = nil
  154. port = args.first
  155. addresses = []
  156. begin
  157. addresses = [IPAddr.new(host)]
  158. rescue IPAddr::InvalidAddressError
  159. Resolv::DNS.open do |dns|
  160. dns.timeouts = 5
  161. addresses = dns.getaddresses(host)
  162. addresses = addresses.filter { |addr| addr.is_a?(Resolv::IPv6) }.take(2) + addresses.filter { |addr| !addr.is_a?(Resolv::IPv6) }.take(2)
  163. end
  164. end
  165. socks = []
  166. addr_by_socket = {}
  167. addresses.each do |address|
  168. begin
  169. check_private_address(address, host)
  170. sock = ::Socket.new(address.is_a?(Resolv::IPv6) ? ::Socket::AF_INET6 : ::Socket::AF_INET, ::Socket::SOCK_STREAM, 0)
  171. sockaddr = ::Socket.pack_sockaddr_in(port, address.to_s)
  172. sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1)
  173. sock.connect_nonblock(sockaddr)
  174. # If that hasn't raised an exception, we somehow managed to connect
  175. # immediately, close pending sockets and return immediately
  176. socks.each(&:close)
  177. return sock
  178. rescue IO::WaitWritable
  179. socks << sock
  180. addr_by_socket[sock] = sockaddr
  181. rescue => e
  182. outer_e = e
  183. end
  184. end
  185. until socks.empty?
  186. _, available_socks, = IO.select(nil, socks, nil, Request::TIMEOUT[:connect])
  187. if available_socks.nil?
  188. socks.each(&:close)
  189. raise HTTP::TimeoutError, "Connect timed out after #{Request::TIMEOUT[:connect]} seconds"
  190. end
  191. available_socks.each do |sock|
  192. socks.delete(sock)
  193. begin
  194. sock.connect_nonblock(addr_by_socket[sock])
  195. rescue Errno::EISCONN
  196. # Do nothing
  197. rescue => e
  198. sock.close
  199. outer_e = e
  200. next
  201. end
  202. socks.each(&:close)
  203. return sock
  204. end
  205. end
  206. if outer_e
  207. raise outer_e
  208. else
  209. raise SocketError, "No address for #{host}"
  210. end
  211. end
  212. alias new open
  213. def check_private_address(address, host)
  214. addr = IPAddr.new(address.to_s)
  215. return if private_address_exceptions.any? { |range| range.include?(addr) }
  216. raise Mastodon::PrivateNetworkAddressError, host if PrivateAddressCheck.private_address?(addr)
  217. end
  218. def private_address_exceptions
  219. @private_address_exceptions = begin
  220. (ENV['ALLOWED_PRIVATE_ADDRESSES'] || '').split(',').map { |addr| IPAddr.new(addr) }
  221. end
  222. end
  223. end
  224. end
  225. class ProxySocket < Socket
  226. class << self
  227. def check_private_address(_address, _host)
  228. # Accept connections to private addresses as HTTP proxies will usually
  229. # be on local addresses
  230. nil
  231. end
  232. end
  233. end
  234. private_constant :ClientLimit, :Socket, :ProxySocket
  235. end