2017-07-14 20:41:49 +02:00
# frozen_string_literal: true
2018-02-25 03:16:11 +09:00
require 'ipaddr'
require 'socket'
2018-11-22 20:12:04 +01:00
require 'resolv'
2018-02-25 03:16:11 +09:00
2023-07-18 20:51:20 +02:00
# Use our own timeout class to avoid using HTTP.rb's timeout block
2018-11-27 19:46:05 +01:00
# around the Socket#open method, since we use our own timeout blocks inside
# that method
2023-07-06 15:06:23 +02:00
#
# Also changes how the read timeout behaves so that it is cumulative (closer
# to HTTP::Timeout::Global, but still having distinct timeouts for other
# operation types)
2023-07-18 20:51:20 +02:00
class PerOperationWithDeadline < HTTP :: Timeout :: PerOperation
READ_DEADLINE = 30
def initialize ( * args )
super
@read_deadline = options . fetch ( :read_deadline , READ_DEADLINE )
end
2018-11-27 19:46:05 +01:00
def connect ( socket_class , host , port , nodelay = false )
@socket = socket_class . open ( host , port )
@socket . setsockopt ( Socket :: IPPROTO_TCP , Socket :: TCP_NODELAY , 1 ) if nodelay
end
2023-07-06 15:06:23 +02:00
# Reset deadline when the connection is re-used for different requests
def reset_counter
@deadline = nil
end
# Read data from the socket
def readpartial ( size , buffer = nil )
2023-07-18 20:51:20 +02:00
@deadline || = Process . clock_gettime ( Process :: CLOCK_MONOTONIC ) + @read_deadline
2023-07-06 15:06:23 +02:00
timeout = false
loop do
result = @socket . read_nonblock ( size , buffer , exception : false )
return :eof if result . nil?
remaining_time = @deadline - Process . clock_gettime ( Process :: CLOCK_MONOTONIC )
2023-07-18 20:51:20 +02:00
raise HTTP :: TimeoutError , " Read timed out after #{ @read_timeout } seconds " if timeout
raise HTTP :: TimeoutError , " Read timed out after a total of #{ @read_deadline } seconds " if remaining_time < = 0
2023-07-06 15:06:23 +02:00
return result if result != :wait_readable
# marking the socket for timeout. Why is this not being raised immediately?
# it seems there is some race-condition on the network level between calling
# #read_nonblock and #wait_readable, in which #read_nonblock signalizes waiting
# for reads, and when waiting for x seconds, it returns nil suddenly without completing
# the x seconds. In a normal case this would be a timeout on wait/read, but it can
# also mean that the socket has been closed by the server. Therefore we "mark" the
# socket for timeout and try to read more bytes. If it returns :eof, it's all good, no
# timeout. Else, the first timeout was a proper timeout.
# This hack has to be done because io/wait#wait_readable doesn't provide a value for when
# the socket is closed by the server, and HTTP::Parser doesn't provide the limit for the chunks.
2023-07-18 20:51:20 +02:00
timeout = true unless @socket . to_io . wait_readable ( [ remaining_time , @read_timeout ] . min )
2023-07-06 15:06:23 +02:00
end
end
2018-11-27 19:46:05 +01:00
end
2017-07-14 20:41:49 +02:00
class Request
2019-07-02 00:34:38 +02:00
# We enforce a 5s timeout on DNS resolving, 5s timeout on socket opening
# and 5s timeout on the TLS handshake, meaning the worst case should take
# about 15s in total
2023-07-18 20:51:20 +02:00
TIMEOUT = { connect_timeout : 5 , read_timeout : 10 , write_timeout : 10 , read_deadline : 30 } . freeze
2019-07-02 00:34:38 +02:00
2017-07-14 20:41:49 +02:00
include RoutingHelper
2017-12-06 19:41:57 +09:00
def initialize ( verb , url , ** options )
2018-05-02 15:44:22 +02:00
raise ArgumentError if url . blank?
2019-07-02 00:34:38 +02:00
@verb = verb
2023-09-19 16:52:52 +02:00
@url = Addressable :: URI . parse ( url ) . normalize
2019-07-02 00:34:38 +02:00
@http_client = options . delete ( :http_client )
2022-12-15 17:04:38 +01:00
@allow_local = options . delete ( :allow_local )
2024-09-20 12:10:09 +02:00
@full_path = ! options . delete ( :omit_query_string )
2025-01-28 15:44:27 +01:00
@options = {
follow : {
max_hops : 3 ,
on_redirect : - > ( response , request ) { re_sign_on_redirect ( response , request ) } ,
} ,
socket_class : use_proxy? || @allow_local ? ProxySocket : Socket ,
} . merge ( options )
2022-08-25 11:41:14 +09:00
@options = @options . merge ( proxy_url ) if use_proxy?
2019-07-02 00:34:38 +02:00
@headers = { }
2017-07-14 20:41:49 +02:00
2025-01-28 15:44:27 +01:00
@signing = nil
2018-04-25 09:14:49 +09:00
raise Mastodon :: HostValidationError , 'Instance does not support hidden service connections' if block_hidden_service?
2018-05-02 15:44:22 +02:00
2017-07-14 20:41:49 +02:00
set_common_headers!
2017-08-09 23:54:14 +02:00
set_digest! if options . key? ( :body )
2017-07-14 20:41:49 +02:00
end
2022-09-21 22:45:57 +02:00
def on_behalf_of ( actor , sign_with : nil )
raise ArgumentError , 'actor must not be nil' if actor . nil?
2017-08-09 23:54:14 +02:00
2025-01-28 15:44:27 +01:00
key_id = ActivityPub :: TagManager . instance . key_uri_for ( actor )
keypair = sign_with . present? ? OpenSSL :: PKey :: RSA . new ( sign_with ) : actor . keypair
@signing = HttpSignatureDraft . new ( keypair , key_id , full_path : @full_path )
2017-08-09 23:54:14 +02:00
self
2017-07-14 20:41:49 +02:00
end
def add_headers ( new_headers )
@headers . merge! ( new_headers )
2017-08-09 23:54:14 +02:00
self
2017-07-14 20:41:49 +02:00
end
def perform
2018-03-24 20:49:54 +09:00
begin
2023-08-18 08:18:40 +02:00
response = http_client . request ( @verb , @url . to_s , @options . merge ( headers : headers ) )
2018-03-24 20:49:54 +09:00
rescue = > e
2020-01-10 15:15:03 -10:00
raise e . class , " #{ e . message } on #{ @url } " , e . backtrace [ 0 ]
2018-03-24 20:49:54 +09:00
end
begin
2019-07-02 00:34:38 +02:00
yield response if block_given?
2018-03-24 20:49:54 +09:00
ensure
2025-01-09 09:35:35 +01:00
response . truncated_body if http_client . persistent? && ! response . connection . finished_request?
http_client . close unless http_client . persistent? && response . connection . finished_request?
2018-03-24 20:49:54 +09:00
end
2017-07-14 20:41:49 +02:00
end
def headers
2025-01-28 15:44:27 +01:00
( @signing ? @headers . merge ( 'Signature' = > signature ) : @headers )
2017-07-14 20:41:49 +02:00
end
2018-12-26 19:15:53 +01:00
class << self
def valid_url? ( url )
begin
parsed_url = Addressable :: URI . parse ( url )
rescue Addressable :: URI :: InvalidURIError
return false
end
%w( http https ) . include? ( parsed_url . scheme ) && parsed_url . host . present?
end
2019-07-02 00:34:38 +02:00
def http_client
2025-01-28 15:44:27 +01:00
HTTP . use ( :auto_inflate )
2019-07-02 00:34:38 +02:00
end
2018-12-26 19:15:53 +01:00
end
2017-07-14 20:41:49 +02:00
private
def set_common_headers!
2018-05-18 08:47:22 +09:00
@headers [ 'User-Agent' ] = Mastodon :: Version . user_agent
2018-05-10 21:36:12 +09:00
@headers [ 'Host' ] = @url . host
@headers [ 'Date' ] = Time . now . utc . httpdate
@headers [ 'Accept-Encoding' ] = 'gzip' if @verb != :head
2017-07-14 20:41:49 +02:00
end
2017-08-09 23:54:14 +02:00
def set_digest!
@headers [ 'Digest' ] = " SHA-256= #{ Digest :: SHA256 . base64digest ( @options [ :body ] ) } "
end
2025-01-28 15:44:27 +01:00
def signature
@signing . sign ( @headers . without ( 'User-Agent' , 'Accept-Encoding' ) , @verb , @url )
2024-01-03 12:29:26 +01:00
end
2025-01-28 15:44:27 +01:00
def re_sign_on_redirect ( _response , request )
# Delete existing signature if there is one, since it will be invalid
request . headers . delete ( 'Signature' )
2017-07-14 20:41:49 +02:00
2025-01-28 15:44:27 +01:00
return unless @signing . present? && @verb == :get
2017-07-14 20:41:49 +02:00
2025-01-28 15:44:27 +01:00
signed_headers = request . headers . to_h . slice ( * @headers . keys )
unless @headers . keys . all? { | key | signed_headers . key? ( key ) }
# We have lost some headers in the process, so don't sign the new
# request, in order to avoid issuing a valid signature with fewer
# conditions than expected.
2017-07-14 20:41:49 +02:00
2025-01-28 15:44:27 +01:00
Rails . logger . warn { " Some headers ( #{ @headers . keys - signed_headers . keys } ) have been lost on redirect from {@uri} to #{ request . uri } , this should not happen. Skipping signatures " }
return
end
2017-07-14 20:41:49 +02:00
2025-01-28 15:44:27 +01:00
signature_value = @signing . sign ( signed_headers . without ( 'User-Agent' , 'Accept-Encoding' ) , @verb , Addressable :: URI . parse ( request . uri ) )
request . headers [ 'Signature' ] = signature_value
2017-08-09 23:54:14 +02:00
end
2017-07-14 20:41:49 +02:00
def http_client
2019-07-02 00:34:38 +02:00
@http_client || = Request . http_client
2017-07-14 20:41:49 +02:00
end
2018-02-25 03:16:11 +09:00
2018-04-25 09:14:49 +09:00
def use_proxy?
2022-08-25 11:41:14 +09:00
proxy_url . present?
end
def proxy_url
if hidden_service? && Rails . configuration . x . http_client_hidden_proxy . present?
Rails . configuration . x . http_client_hidden_proxy
else
Rails . configuration . x . http_client_proxy
end
2018-04-25 09:14:49 +09:00
end
def block_hidden_service?
2022-08-25 11:41:14 +09:00
! Rails . configuration . x . access_to_hidden_service && hidden_service?
end
def hidden_service?
/ \ .(onion|i2p)$ / . match? ( @url . host )
2018-04-25 09:14:49 +09:00
end
2018-03-26 21:02:10 +09:00
module ClientLimit
2023-01-11 21:59:13 +01:00
def truncated_body ( limit = 1 . megabyte )
2018-03-26 21:02:10 +09:00
if charset . nil?
encoding = Encoding :: BINARY
else
begin
encoding = Encoding . find ( charset )
rescue ArgumentError
encoding = Encoding :: BINARY
end
end
contents = String . new ( encoding : encoding )
while ( chunk = readpartial )
contents << chunk
chunk . clear
2023-01-11 21:59:13 +01:00
break if contents . bytesize > limit
2018-03-26 21:02:10 +09:00
end
contents
end
2023-01-11 21:59:13 +01:00
def body_with_limit ( limit = 1 . megabyte )
2024-08-06 10:23:48 +02:00
require_limit_not_exceeded! ( limit )
2023-01-11 21:59:13 +01:00
contents = truncated_body ( limit )
2024-08-06 10:23:48 +02:00
raise Mastodon :: LengthValidationError , " Body size exceeds limit of #{ limit } " if contents . bytesize > limit
2023-02-20 00:58:28 -05:00
2023-01-11 21:59:13 +01:00
contents
end
2024-08-06 10:23:48 +02:00
def require_limit_not_exceeded! ( limit )
raise Mastodon :: LengthValidationError , " Content-Length #{ content_length } exceeds limit of #{ limit } " if content_length . present? && content_length > limit
end
2018-03-26 21:02:10 +09:00
end
2022-11-07 19:00:27 -08:00
if :: HTTP :: Response . methods . include? ( :body_with_limit ) && ! Rails . env . production?
abort 'HTTP::Response#body_with_limit is already defined, the monkey patch will not be applied'
else
class :: HTTP :: Response
include Request :: ClientLimit
end
end
2018-02-25 03:16:11 +09:00
class Socket < TCPSocket
class << self
def open ( host , * args )
2018-03-20 04:06:08 -04:00
outer_e = nil
2019-07-02 00:34:38 +02:00
port = args . first
2018-11-22 20:12:04 +01:00
2024-12-09 09:52:49 -05:00
addresses = [ ] # rubocop:disable Lint/UselessAssignment # TODO: https://github.com/rubocop/rubocop/issues/13395
2019-07-07 02:05:38 +02:00
begin
addresses = [ IPAddr . new ( host ) ]
rescue IPAddr :: InvalidAddressError
Resolv :: DNS . open do | dns |
dns . timeouts = 5
2022-11-08 16:36:26 +01:00
addresses = dns . getaddresses ( host )
addresses = addresses . filter { | addr | addr . is_a? ( Resolv :: IPv6 ) } . take ( 2 ) + addresses . filter { | addr | ! addr . is_a? ( Resolv :: IPv6 ) } . take ( 2 )
2019-07-07 02:05:38 +02:00
end
end
2018-11-22 20:12:04 +01:00
2019-09-05 05:32:53 +02:00
socks = [ ]
addr_by_socket = { }
2019-07-07 02:05:38 +02:00
addresses . each do | address |
2023-02-18 17:09:40 -05:00
check_private_address ( address , host )
2019-07-07 02:05:38 +02:00
2023-02-18 17:09:40 -05:00
sock = :: Socket . new ( address . is_a? ( Resolv :: IPv6 ) ? :: Socket :: AF_INET6 : :: Socket :: AF_INET , :: Socket :: SOCK_STREAM , 0 )
sockaddr = :: Socket . pack_sockaddr_in ( port , address . to_s )
2019-07-07 02:05:38 +02:00
2023-02-18 17:09:40 -05:00
sock . setsockopt ( :: Socket :: IPPROTO_TCP , :: Socket :: TCP_NODELAY , 1 )
2018-11-22 20:12:04 +01:00
2023-02-18 17:09:40 -05:00
sock . connect_nonblock ( sockaddr )
2019-07-07 02:05:38 +02:00
2023-02-18 17:09:40 -05:00
# If that hasn't raised an exception, we somehow managed to connect
# immediately, close pending sockets and return immediately
socks . each ( & :close )
return sock
rescue IO :: WaitWritable
socks << sock
addr_by_socket [ sock ] = sockaddr
rescue = > e
outer_e = e
2018-03-20 04:06:08 -04:00
end
2018-11-22 20:12:04 +01:00
2019-09-05 05:32:53 +02:00
until socks . empty?
2023-07-22 20:42:31 +02:00
_ , available_socks , = IO . select ( nil , socks , nil , Request :: TIMEOUT [ :connect_timeout ] )
2019-09-05 05:32:53 +02:00
if available_socks . nil?
socks . each ( & :close )
2023-07-22 20:42:31 +02:00
raise HTTP :: TimeoutError , " Connect timed out after #{ Request :: TIMEOUT [ :connect_timeout ] } seconds "
2019-09-05 05:32:53 +02:00
end
available_socks . each do | sock |
socks . delete ( sock )
begin
sock . connect_nonblock ( addr_by_socket [ sock ] )
rescue Errno :: EISCONN
2020-07-15 02:05:07 +09:00
# Do nothing
2019-09-05 05:32:53 +02:00
rescue = > e
sock . close
outer_e = e
next
end
socks . each ( & :close )
return sock
end
end
2018-11-27 18:13:36 +01:00
if outer_e
raise outer_e
else
raise SocketError , " No address for #{ host } "
end
2018-02-25 03:16:11 +09:00
end
alias new open
2018-04-25 09:14:49 +09:00
2022-09-20 23:30:26 +02:00
def check_private_address ( address , host )
2020-09-26 20:57:39 +02:00
addr = IPAddr . new ( address . to_s )
2023-03-16 04:33:38 +01:00
2024-11-12 14:00:52 +01:00
return if Rails . env . development? || Rails . configuration . x . private_address_exceptions . any? { | range | range . include? ( addr ) }
2023-03-16 04:33:38 +01:00
2022-09-20 23:30:26 +02:00
raise Mastodon :: PrivateNetworkAddressError , host if PrivateAddressCheck . private_address? ( addr )
2020-09-26 20:57:39 +02:00
end
2019-07-07 02:05:38 +02:00
end
end
class ProxySocket < Socket
class << self
2022-11-12 04:31:03 +08:00
def check_private_address ( _address , _host )
2019-07-07 02:05:38 +02:00
# Accept connections to private addresses as HTTP proxies will usually
# be on local addresses
nil
2018-04-25 09:14:49 +09:00
end
2018-02-25 03:16:11 +09:00
end
end
2019-07-07 02:05:38 +02:00
private_constant :ClientLimit , :Socket , :ProxySocket
2017-07-14 20:41:49 +02:00
end