Fix support for HTTP proxies (#11245)

* Disable incorrect check for hidden services in Socket

Hidden services can only be accessed with an HTTP proxy, in which
case the host seen by the Socket class will be the proxy, not the
target host.

Hidden services are already filtered in `Request#initialize`.

* Use our Socket class to connect to HTTP proxies

Avoid the timeout logic being bypassed

* Add support for IP addresses in Request::Socket

* Refactor a bit, no need to keep the DNS resolver around
This commit is contained in:
ThibG 2019-07-07 02:05:38 +02:00 committed by Thibaut Girka
parent 7039dca12c
commit a0b614f10a

View file

@ -30,7 +30,8 @@ class Request
@verb = verb @verb = verb
@url = Addressable::URI.parse(url).normalize @url = Addressable::URI.parse(url).normalize
@http_client = options.delete(:http_client) @http_client = options.delete(:http_client)
@options = options.merge(use_proxy? ? Rails.configuration.x.http_client_proxy : { socket_class: Socket }) @options = options.merge(socket_class: use_proxy? ? ProxySocket : Socket)
@options = @options.merge(Rails.configuration.x.http_client_proxy) if use_proxy?
@headers = {} @headers = {}
raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if block_hidden_service? raise Mastodon::HostValidationError, 'Instance does not support hidden service connections' if block_hidden_service?
@ -177,47 +178,49 @@ class Request
class Socket < TCPSocket class Socket < TCPSocket
class << self class << self
def open(host, *args) def open(host, *args)
return super(host, *args) if thru_hidden_service?(host)
outer_e = nil outer_e = nil
port = args.first port = args.first
Resolv::DNS.open do |dns| addresses = []
dns.timeouts = 5 begin
addresses = [IPAddr.new(host)]
rescue IPAddr::InvalidAddressError
Resolv::DNS.open do |dns|
dns.timeouts = 5
addresses = dns.getaddresses(host).take(2)
end
end
addresses = dns.getaddresses(host).take(2) addresses.each do |address|
begin
check_private_address(address)
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)
sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1)
addresses.each do |address|
begin begin
raise Mastodon::HostValidationError if PrivateAddressCheck.private_address?(IPAddr.new(address.to_s)) sock.connect_nonblock(sockaddr)
rescue IO::WaitWritable
sock = ::Socket.new(address.is_a?(Resolv::IPv6) ? ::Socket::AF_INET6 : ::Socket::AF_INET, ::Socket::SOCK_STREAM, 0) if IO.select(nil, [sock], nil, Request::TIMEOUT[:connect])
sockaddr = ::Socket.pack_sockaddr_in(port, address.to_s) begin
sock.connect_nonblock(sockaddr)
sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, 1) rescue Errno::EISCONN
# Yippee!
begin rescue
sock.connect_nonblock(sockaddr)
rescue IO::WaitWritable
if IO.select(nil, [sock], nil, Request::TIMEOUT[:connect])
begin
sock.connect_nonblock(sockaddr)
rescue Errno::EISCONN
# Yippee!
rescue
sock.close
raise
end
else
sock.close sock.close
raise HTTP::TimeoutError, "Connect timed out after #{Request::TIMEOUT[:connect]} seconds" raise
end end
else
sock.close
raise HTTP::TimeoutError, "Connect timed out after #{Request::TIMEOUT[:connect]} seconds"
end end
return sock
rescue => e
outer_e = e
end end
return sock
rescue => e
outer_e = e
end end
end end
@ -230,11 +233,21 @@ class Request
alias new open alias new open
def thru_hidden_service?(host) def check_private_address(address)
Rails.configuration.x.access_to_hidden_service && /\.(onion|i2p)$/.match(host) raise Mastodon::HostValidationError if PrivateAddressCheck.private_address?(IPAddr.new(address.to_s))
end end
end end
end end
private_constant :ClientLimit, :Socket class ProxySocket < Socket
class << self
def check_private_address(_address)
# Accept connections to private addresses as HTTP proxies will usually
# be on local addresses
nil
end
end
end
private_constant :ClientLimit, :Socket, :ProxySocket
end end