diff options
Diffstat (limited to 'ext/socket/lib')
| -rw-r--r-- | ext/socket/lib/socket.rb | 819 |
1 files changed, 384 insertions, 435 deletions
diff --git a/ext/socket/lib/socket.rb b/ext/socket/lib/socket.rb index e953077fe6..36fcceaee9 100644 --- a/ext/socket/lib/socket.rb +++ b/ext/socket/lib/socket.rb @@ -62,7 +62,7 @@ class Addrinfo break when :wait_writable sock.wait_writable(timeout) or - raise Errno::ETIMEDOUT, 'user specified timeout' + raise Errno::ETIMEDOUT, "user specified timeout for #{self.ip_address}:#{self.ip_port}" end while true else sock.connect(self) @@ -599,6 +599,7 @@ class Socket < BasicSocket __accept_nonblock(exception) end + # :stopdoc: RESOLUTION_DELAY = 0.05 private_constant :RESOLUTION_DELAY @@ -614,14 +615,9 @@ class Socket < BasicSocket HOSTNAME_RESOLUTION_QUEUE_UPDATED = 0 private_constant :HOSTNAME_RESOLUTION_QUEUE_UPDATED - IPV6_ADRESS_FORMAT = /(?i)(?:(?:[0-9A-F]{1,4}:){7}(?:[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){6}(?:[0-9A-F]{1,4}::[0-9A-F]{1,4}|:(?:[0-9A-F]{1,4}:){1,5}[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){5}(?::[0-9A-F]{1,4}::[0-9A-F]{1,4}|:(?:[0-9A-F]{1,4}:){1,4}[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){4}(?::[0-9A-F]{1,4}::[0-9A-F]{1,4}|:(?:[0-9A-F]{1,4}:){1,3}[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){3}(?::[0-9A-F]{1,4}::[0-9A-F]{1,4}|:(?:[0-9A-F]{1,4}:){1,2}[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){2}(?::[0-9A-F]{1,4}::[0-9A-F]{1,4}|:(?:[0-9A-F]{1,4}:)[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){1}(?::[0-9A-F]{1,4}::[0-9A-F]{1,4}|::(?:[0-9A-F]{1,4}:){1,5}[0-9A-F]{1,4}|:)|::(?:[0-9A-F]{1,4}::[0-9A-F]{1,4}|:(?:[0-9A-F]{1,4}:){1,6}[0-9A-F]{1,4}|:))(?:%.+)?/ - private_constant :IPV6_ADRESS_FORMAT - - @tcp_fast_fallback = true - - class << self - attr_accessor :tcp_fast_fallback - end + IPV6_ADDRESS_FORMAT = /\A(?i:(?:(?:[0-9A-F]{1,4}:){7}(?:[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){6}(?:[0-9A-F]{1,4}|:(?:[0-9A-F]{1,4}:){1,5}[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){5}(?:(?::[0-9A-F]{1,4}){1,2}|:(?:[0-9A-F]{1,4}:){1,4}[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){4}(?:(?::[0-9A-F]{1,4}){1,3}|:(?:[0-9A-F]{1,4}:){1,3}[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){3}(?:(?::[0-9A-F]{1,4}){1,4}|:(?:[0-9A-F]{1,4}:){1,2}[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){2}(?:(?::[0-9A-F]{1,4}){1,5}|:(?:[0-9A-F]{1,4}:)[0-9A-F]{1,4}|:)|(?:[0-9A-F]{1,4}:){1}(?:(?::[0-9A-F]{1,4}){1,6}|:(?:[0-9A-F]{1,4}:){0,5}[0-9A-F]{1,4}|:)|(?:::(?:[0-9A-F]{1,4}:){0,7}[0-9A-F]{1,4}|::)))(?:%.+)?\z/ + private_constant :IPV6_ADDRESS_FORMAT + # :startdoc: # :call-seq: # Socket.tcp(host, port, local_host=nil, local_port=nil, [opts]) {|socket| ... } @@ -629,13 +625,28 @@ class Socket < BasicSocket # # creates a new socket object connected to host:port using TCP/IP. # + # Starting from Ruby 3.4, this method operates according to the + # Happy Eyeballs Version 2 ({RFC 8305}[https://datatracker.ietf.org/doc/html/rfc8305]) + # algorithm by default. + # + # For details on Happy Eyeballs Version 2, + # see {Socket.tcp_fast_fallback=}[rdoc-ref:Socket.tcp_fast_fallback=]. + # + # To make it behave the same as in Ruby 3.3 and earlier, + # explicitly specify the option fast_fallback:false. + # Or, setting Socket.tcp_fast_fallback=false will disable + # Happy Eyeballs Version 2 not only for this method but for all Socket globally. + # # If local_host:local_port is given, # the socket is bound to it. # # The optional last argument _opts_ is options represented by a hash. # _opts_ may have following options: # - # [:connect_timeout] specify the timeout in seconds. + # [:resolv_timeout] Specifies the timeout in seconds from when the hostname resolution starts. + # [:connect_timeout] This method sequentially attempts connecting to all candidate destination addresses.<br>The +connect_timeout+ specifies the timeout in seconds from the start of the connection attempt to the last candidate.<br>By default, all connection attempts continue until the timeout occurs.<br>When +fast_fallback:false+ is explicitly specified,<br>a timeout is set for each connection attempt and any connection attempt that exceeds its timeout will be canceled. + # [:open_timeout] Specifies the timeout in seconds from the start of the method execution.<br>If this timeout is reached while there are still addresses that have not yet been attempted for connection, no further attempts will be made.<br>If this option is specified together with other timeout options, an +ArgumentError+ will be raised. + # [:fast_fallback] Enables the Happy Eyeballs Version 2 algorithm (enabled by default). # # If a block is given, the block is called with the socket. # The value of the block is returned. @@ -648,537 +659,472 @@ class Socket < BasicSocket # sock.close_write # puts sock.read # } - def self.tcp(host, port, local_host = nil, local_port = nil, connect_timeout: nil, resolv_timeout: nil, fast_fallback: tcp_fast_fallback, &block) # :yield: socket - unless fast_fallback - return tcp_without_fast_fallback(host, port, local_host, local_port, connect_timeout:, resolv_timeout:, &block) + def self.tcp(host, port, local_host = nil, local_port = nil, connect_timeout: nil, resolv_timeout: nil, open_timeout: nil, fast_fallback: tcp_fast_fallback, &) # :yield: socket + if open_timeout && (connect_timeout || resolv_timeout) + raise ArgumentError, "Cannot specify open_timeout along with connect_timeout or resolv_timeout" end - # Happy Eyeballs' states - # - :start - # - :v6c - # - :v4w - # - :v4c - # - :v46c - # - :v46w - # - :success - # - :failure - # - :timeout - - specified_family_name = nil - hostname_resolution_threads = [] - hostname_resolution_queue = nil - hostname_resolution_waiting = nil - hostname_resolution_expires_at = nil - selectable_addrinfos = SelectableAddrinfos.new - connecting_sockets = ConnectingSockets.new - local_addrinfos = [] - connection_attempt_delay_expires_at = nil - connection_attempt_started_at = nil - state = :start - connected_socket = nil - last_error = nil - is_windows_environment ||= (RUBY_PLATFORM =~ /mswin|mingw|cygwin/) + sock = if fast_fallback && !(host && ip_address?(host)) && !(local_port && local_port.to_i != 0) + tcp_with_fast_fallback(host, port, local_host, local_port, connect_timeout:, resolv_timeout:, open_timeout:) + else + tcp_without_fast_fallback(host, port, local_host, local_port, connect_timeout:, resolv_timeout:, open_timeout:) + end - ret = loop do - case state - when :start - specified_family_name, next_state = host && specified_family_name_and_next_state(host) + if block_given? + begin + yield sock + ensure + sock.close + end + else + sock + end + end - if local_host && local_port - specified_family_name, next_state = specified_family_name_and_next_state(local_host) unless specified_family_name - local_addrinfos = Addrinfo.getaddrinfo(local_host, local_port, ADDRESS_FAMILIES[specified_family_name], :STREAM, timeout: resolv_timeout) - end + # :stopdoc: + def self.tcp_with_fast_fallback(host, port, local_host = nil, local_port = nil, connect_timeout: nil, resolv_timeout: nil, open_timeout: nil) + if local_host || local_port + local_addrinfos = Addrinfo.getaddrinfo(local_host, local_port, nil, :STREAM, timeout: open_timeout || resolv_timeout) + resolving_family_names = local_addrinfos.map { |lai| ADDRESS_FAMILIES.key(lai.afamily) }.uniq + else + local_addrinfos = [] + resolving_family_names = ADDRESS_FAMILIES.keys + end - if specified_family_name - addrinfos = Addrinfo.getaddrinfo(host, port, ADDRESS_FAMILIES[specified_family_name], :STREAM, timeout: resolv_timeout) - selectable_addrinfos.add(specified_family_name, addrinfos) - hostname_resolution_queue = NoHostnameResolutionQueue.new - state = next_state - next - end + hostname_resolution_threads = [] + resolution_store = HostnameResolutionStore.new(resolving_family_names) + connecting_sockets = {} + is_windows_environment ||= (RUBY_PLATFORM =~ /mswin|mingw|cygwin/) - resolving_family_names = ADDRESS_FAMILIES.keys - hostname_resolution_queue = HostnameResolutionQueue.new(resolving_family_names.size) - hostname_resolution_waiting = hostname_resolution_queue.waiting_pipe - hostname_resolution_started_at = current_clocktime if resolv_timeout - hostname_resolution_args = [host, port, hostname_resolution_queue] - - hostname_resolution_threads.concat( - resolving_family_names.map { |family| - thread_args = [family, *hostname_resolution_args] - thread = Thread.new(*thread_args) { |*thread_args| hostname_resolution(*thread_args) } - Thread.pass - thread - } - ) - - hostname_resolution_retry_count = resolving_family_names.size - 1 - hostname_resolution_expires_at = hostname_resolution_started_at + resolv_timeout if resolv_timeout - - while hostname_resolution_retry_count >= 0 - remaining = resolv_timeout ? second_to_timeout(hostname_resolution_started_at + resolv_timeout) : nil - hostname_resolved, _, = IO.select(hostname_resolution_waiting, nil, nil, remaining) - - unless hostname_resolved - state = :timeout - break - end + now = current_clock_time + starts_at = now + resolution_delay_expires_at = nil + connection_attempt_delay_expires_at = nil + user_specified_connect_timeout_at = nil + user_specified_open_timeout_at = open_timeout ? now + open_timeout : nil + last_error = nil + last_error_from_thread = false + + if resolving_family_names.size == 1 + family_name = resolving_family_names.first + addrinfos = Addrinfo.getaddrinfo(host, port, ADDRESS_FAMILIES[:family_name], :STREAM, timeout: open_timeout || resolv_timeout) + resolution_store.add_resolved(family_name, addrinfos) + hostname_resolution_result = nil + hostname_resolution_notifier = nil + user_specified_resolv_timeout_at = nil + else + hostname_resolution_result = HostnameResolutionResult.new(resolving_family_names.size) + hostname_resolution_notifier = hostname_resolution_result.notifier + + hostname_resolution_threads.concat( + resolving_family_names.map { |family| + thread_args = [family, host, port, hostname_resolution_result] + thread = Thread.new(*thread_args) { |*thread_args| resolve_hostname(*thread_args) } + Thread.pass + thread + } + ) + user_specified_resolv_timeout_at = resolv_timeout ? now + resolv_timeout : Float::INFINITY + end - family_name, res = hostname_resolution_queue.get + loop do + if resolution_store.any_addrinfos? && + !resolution_delay_expires_at && + !connection_attempt_delay_expires_at + while (addrinfo = resolution_store.get_addrinfo) + if local_addrinfos.any? + local_addrinfo = local_addrinfos.find { |lai| lai.afamily == addrinfo.afamily } + + if local_addrinfo.nil? + if resolution_store.any_addrinfos? + # Try other Addrinfo in next "while" + next + elsif connecting_sockets.any? || resolution_store.any_unresolved_family? + # Exit this "while" and wait for connections to be established or hostname resolution in next loop + # Or exit this "while" and wait for hostname resolution in next loop + break + else + raise SocketError.new 'no appropriate local address' + end + end + end - if res.is_a? Exception - unless (Socket.const_defined?(:EAI_ADDRFAMILY)) && - (res.is_a?(Socket::ResolutionError)) && - (res.error_code == Socket::EAI_ADDRFAMILY) - last_error = res + begin + if resolution_store.any_addrinfos? || + connecting_sockets.any? || + resolution_store.any_unresolved_family? + socket = Socket.new(addrinfo.pfamily, addrinfo.socktype, addrinfo.protocol) + socket.bind(local_addrinfo) if local_addrinfo + result = socket.connect_nonblock(addrinfo, exception: false) + else + timeout = + if open_timeout + t = open_timeout - (current_clock_time - starts_at) + t.negative? ? 0 : t + else + connect_timeout + end + result = socket = local_addrinfo ? + addrinfo.connect_from(local_addrinfo, timeout:) : + addrinfo.connect(timeout:) end - if hostname_resolution_retry_count.zero? - state = :failure + if result == :wait_writable + connection_attempt_delay_expires_at = now + CONNECTION_ATTEMPT_DELAY + if resolution_store.empty_addrinfos? + user_specified_connect_timeout_at = connect_timeout ? now + connect_timeout : Float::INFINITY + end + + connecting_sockets[socket] = addrinfo break + else + return socket # connection established + end + rescue SystemCallError => e + socket&.close + last_error = e + + if resolution_store.any_addrinfos? + # Try other Addrinfo in next "while" + next + elsif connecting_sockets.any? || resolution_store.any_unresolved_family? + # Exit this "while" and wait for connections to be established or hostname resolution in next loop + # Or exit this "while" and wait for hostname resolution in next loop + break + else + raise last_error end - hostname_resolution_retry_count -= 1 - else - state = case family_name - when :ipv6 then :v6c - when :ipv4 then hostname_resolution_queue.closed? ? :v4c : :v4w - end - selectable_addrinfos.add(family_name, res) - last_error = nil - break end end + end - next - when :v4w - ipv6_resolved, _, = IO.select(hostname_resolution_waiting, nil, nil, RESOLUTION_DELAY) - - if ipv6_resolved - family_name, res = hostname_resolution_queue.get - selectable_addrinfos.add(family_name, res) unless res.is_a? Exception - state = :v46c + ends_at = + if resolution_store.any_addrinfos? + [(resolution_delay_expires_at || connection_attempt_delay_expires_at), + user_specified_open_timeout_at].compact.min + elsif user_specified_open_timeout_at + user_specified_open_timeout_at else - state = :v4c + [user_specified_resolv_timeout_at, user_specified_connect_timeout_at].compact.max end - next - when :v4c, :v6c, :v46c - connection_attempt_started_at = current_clocktime unless connection_attempt_started_at - addrinfo = selectable_addrinfos.get - - if local_addrinfos.any? - local_addrinfo = local_addrinfos.find { |lai| lai.afamily == addrinfo.afamily } - - if local_addrinfo.nil? - if selectable_addrinfos.empty? && connecting_sockets.empty? && hostname_resolution_queue.closed? - last_error = SocketError.new 'no appropriate local address' - state = :failure - elsif selectable_addrinfos.any? - # Try other Addrinfo in next loop + hostname_resolved, writable_sockets, except_sockets = IO.select( + hostname_resolution_notifier, + connecting_sockets.keys, + # Use errorfds to wait for non-blocking connect failures on Windows + is_windows_environment ? connecting_sockets.keys : nil, + second_to_timeout(current_clock_time, ends_at), + ) + now = current_clock_time + resolution_delay_expires_at = nil if expired?(now, resolution_delay_expires_at) + connection_attempt_delay_expires_at = nil if expired?(now, connection_attempt_delay_expires_at) + + if writable_sockets&.any? + while (writable_socket = writable_sockets.pop) + is_connected = is_windows_environment || ( + sockopt = writable_socket.getsockopt(Socket::SOL_SOCKET, Socket::SO_ERROR) + sockopt.int.zero? + ) + + if is_connected + connecting_sockets.delete writable_socket + return writable_socket + else + failed_ai = connecting_sockets.delete writable_socket + writable_socket.close + ip_address = failed_ai.ipv6? ? "[#{failed_ai.ip_address}]" : failed_ai.ip_address + last_error = SystemCallError.new("connect(2) for #{ip_address}:#{failed_ai.ip_port}", sockopt.int) + + if writable_sockets.any? || connecting_sockets.any? + # Try other writable socket in next "while" + # Or exit this "while" and wait for connections to be established or hostname resolution in next loop + elsif resolution_store.any_addrinfos? || resolution_store.any_unresolved_family? + # Exit this "while" and try other connection attempt + # Or exit this "while" and wait for hostname resolution in next loop + connection_attempt_delay_expires_at = nil + user_specified_connect_timeout_at = nil else - if resolv_timeout && hostname_resolution_queue.opened? && - (current_clocktime >= hostname_resolution_expires_at) - state = :timeout - else - # Wait for connection to be established or hostname resolution in next loop - connection_attempt_delay_expires_at = nil - state = :v46w - end + raise last_error end - next end end + end - connection_attempt_delay_expires_at = current_clocktime + CONNECTION_ATTEMPT_DELAY - - begin - result = if specified_family_name && selectable_addrinfos.empty? && - connecting_sockets.empty? && hostname_resolution_queue.closed? - local_addrinfo ? - addrinfo.connect_from(local_addrinfo, timeout: connect_timeout) : - addrinfo.connect(timeout: connect_timeout) - else - socket = Socket.new(addrinfo.pfamily, addrinfo.socktype, addrinfo.protocol) - socket.bind(local_addrinfo) if local_addrinfo - socket.connect_nonblock(addrinfo, exception: false) - end - - case result - when 0 - connected_socket = socket - state = :success - when Socket - connected_socket = result - state = :success - when :wait_writable - connecting_sockets.add(socket, addrinfo) - state = :v46w - end - rescue SystemCallError => e - last_error = e - socket.close if socket && !socket.closed? - - if selectable_addrinfos.empty? && connecting_sockets.empty? && hostname_resolution_queue.closed? - state = :failure - elsif selectable_addrinfos.any? - # Try other Addrinfo in next loop + if except_sockets&.any? + except_sockets.each do |except_socket| + failed_ai = connecting_sockets.delete except_socket + sockopt = except_socket.getsockopt(Socket::SOL_SOCKET, Socket::SO_ERROR) + except_socket.close + ip_address = failed_ai.ipv6? ? "[#{failed_ai.ip_address}]" : failed_ai.ip_address + last_error = SystemCallError.new("connect(2) for #{ip_address}:#{failed_ai.ip_port}", sockopt.int) + + if except_sockets.any? || connecting_sockets.any? + # Cleanup other except socket in next "each" + # Or exit this "while" and wait for connections to be established or hostname resolution in next loop + elsif resolution_store.any_addrinfos? || resolution_store.any_unresolved_family? + # Exit this "while" and try other connection attempt + # Or exit this "while" and wait for hostname resolution in next loop + connection_attempt_delay_expires_at = nil + user_specified_connect_timeout_at = nil else - if resolv_timeout && hostname_resolution_queue.opened? && - (current_clocktime >= hostname_resolution_expires_at) - state = :timeout - else - # Wait for connection to be established or hostname resolution in next loop - connection_attempt_delay_expires_at = nil - state = :v46w - end + raise last_error end end + end - next - when :v46w - if connect_timeout && second_to_timeout(connection_attempt_started_at + connect_timeout).zero? - state = :timeout - next - end - - remaining = second_to_timeout(connection_attempt_delay_expires_at) - hostname_resolution_waiting = hostname_resolution_waiting && hostname_resolution_queue.closed? ? nil : hostname_resolution_waiting - hostname_resolved, connectable_sockets, = IO.select(hostname_resolution_waiting, connecting_sockets.all, nil, remaining) - - if connectable_sockets&.any? - while (connectable_socket = connectable_sockets.pop) - is_connected = - if is_windows_environment - sockopt = connectable_socket.getsockopt(Socket::SOL_SOCKET, Socket::SO_CONNECT_TIME) - sockopt.unpack('i').first >= 0 - else - sockopt = connectable_socket.getsockopt(Socket::SOL_SOCKET, Socket::SO_ERROR) - sockopt.int.zero? - end - - if is_connected - connected_socket = connectable_socket - connecting_sockets.delete connectable_socket - connectable_sockets.each do |other_connectable_socket| - other_connectable_socket.close unless other_connectable_socket.closed? - end - state = :success - break - else - failed_ai = connecting_sockets.delete connectable_socket - inspected_ip_address = failed_ai.ipv6? ? "[#{failed_ai.ip_address}]" : failed_ai.ip_address - last_error = SystemCallError.new("connect(2) for #{inspected_ip_address}:#{failed_ai.ip_port}", sockopt.int) - connectable_socket.close unless connectable_socket.closed? + if hostname_resolved&.any? + while (family_and_result = hostname_resolution_result.get) + family_name, result = family_and_result - next if connectable_sockets.any? + if result.is_a? Exception + resolution_store.add_error(family_name, result) - if selectable_addrinfos.empty? && connecting_sockets.empty? && hostname_resolution_queue.closed? - state = :failure - elsif selectable_addrinfos.any? - # Wait for connection attempt delay timeout in next loop - else - if resolv_timeout && hostname_resolution_queue.opened? && - (current_clocktime >= hostname_resolution_expires_at) - state = :timeout - else - # Wait for connection to be established or hostname resolution in next loop - connection_attempt_delay_expires_at = nil - end + unless (Socket.const_defined?(:EAI_ADDRFAMILY)) && + (result.is_a?(Socket::ResolutionError)) && + (result.error_code == Socket::EAI_ADDRFAMILY) + other = family_name == :ipv6 ? :ipv4 : :ipv6 + if !resolution_store.resolved?(other) || !resolution_store.resolved_successfully?(other) + last_error = result + last_error_from_thread = true end end - end - elsif hostname_resolved&.any? - family_name, res = hostname_resolution_queue.get - selectable_addrinfos.add(family_name, res) unless res.is_a? Exception - else - if selectable_addrinfos.empty? && connecting_sockets.empty? && hostname_resolution_queue.closed? - state = :failure - elsif selectable_addrinfos.any? - # Try other Addrinfo in next loop - state = :v46c else - if resolv_timeout && hostname_resolution_queue.opened? && - (current_clocktime >= hostname_resolution_expires_at) - state = :timeout - else - # Wait for connection to be established or hostname resolution in next loop - connection_attempt_delay_expires_at = nil - end + resolution_store.add_resolved(family_name, result) end end - next - when :success - break connected_socket - when :failure - raise last_error - when :timeout - raise Errno::ETIMEDOUT, 'user specified timeout' + if resolution_store.resolved?(:ipv4) + if resolution_store.resolved?(:ipv6) + hostname_resolution_notifier = nil + resolution_delay_expires_at = nil + user_specified_resolv_timeout_at = nil + elsif resolution_store.resolved_successfully?(:ipv4) + resolution_delay_expires_at = now + RESOLUTION_DELAY + end + end end - end - if block_given? - begin - yield ret - ensure - ret.close - end - else - ret - end - ensure - if fast_fallback - hostname_resolution_threads.each do |thread| - thread&.exit + if expired?(now, user_specified_open_timeout_at) + raise(IO::TimeoutError, "user specified timeout for #{host}:#{port}") end - hostname_resolution_queue&.close_all + if resolution_store.empty_addrinfos? + if connecting_sockets.empty? && resolution_store.resolved_all_families? + if last_error_from_thread + raise last_error.class, last_error.message, cause: last_error + else + raise last_error + end + end - connecting_sockets.each do |connecting_socket| - connecting_socket.close unless connecting_socket.closed? + if (expired?(now, user_specified_resolv_timeout_at) || resolution_store.resolved_all_families?) && + (expired?(now, user_specified_connect_timeout_at) || connecting_sockets.empty?) + raise(IO::TimeoutError, "user specified timeout for #{host}:#{port}") + end end end - end - - def self.specified_family_name_and_next_state(hostname) - if hostname.match?(IPV6_ADRESS_FORMAT) then [:ipv6, :v6c] - elsif hostname.match?(/^([0-9]{1,3}\.){3}[0-9]{1,3}$/) then [:ipv4, :v4c] - end - end - private_class_method :specified_family_name_and_next_state - - def self.hostname_resolution(family, host, port, hostname_resolution_queue) - begin - resolved_addrinfos = Addrinfo.getaddrinfo(host, port, ADDRESS_FAMILIES[family], :STREAM) - hostname_resolution_queue.add_resolved(family, resolved_addrinfos) - rescue => e - hostname_resolution_queue.add_error(family, e) + ensure + hostname_resolution_threads.each do |thread| + thread.exit end - end - private_class_method :hostname_resolution - def self.second_to_timeout(ends_at) - return 0 unless ends_at - - remaining = (ends_at - current_clocktime) - remaining.negative? ? 0 : remaining - end - private_class_method :second_to_timeout + hostname_resolution_result&.close - def self.current_clocktime - Process.clock_gettime(Process::CLOCK_MONOTONIC) + connecting_sockets.each_key do |connecting_socket| + connecting_socket.close + end end - private_class_method :current_clocktime + private_class_method :tcp_with_fast_fallback - class SelectableAddrinfos - PRIORITY_ON_V6 = [:ipv6, :ipv4] - PRIORITY_ON_V4 = [:ipv4, :ipv6] - - def initialize - @addrinfo_dict = {} - @last_family = nil - end + def self.tcp_without_fast_fallback(host, port, local_host, local_port, connect_timeout:, resolv_timeout:, open_timeout:) + last_error = nil + ret = nil - def add(family_name, addrinfos) - @addrinfo_dict[family_name] = addrinfos + local_addr_list = nil + if local_host != nil || local_port != nil + local_addr_list = Addrinfo.getaddrinfo(local_host, local_port, nil, :STREAM, nil, timeout: open_timeout || resolv_timeout) end - def get - return nil if empty? + timeout = open_timeout ? open_timeout : resolv_timeout + starts_at = current_clock_time - if @addrinfo_dict.size == 1 - @addrinfo_dict.each { |_, addrinfos| return addrinfos.shift } + Addrinfo.foreach(host, port, nil, :STREAM, timeout:) {|ai| + if local_addr_list + local_addr = local_addr_list.find {|local_ai| local_ai.afamily == ai.afamily } + next unless local_addr + else + local_addr = nil end + begin + timeout = + if open_timeout + t = open_timeout - (current_clock_time - starts_at) + t.negative? ? 0 : t + else + connect_timeout + end - precedences = case @last_family - when :ipv4, nil then PRIORITY_ON_V6 - when :ipv6 then PRIORITY_ON_V4 - end - - precedences.each do |family_name| - addrinfo = @addrinfo_dict[family_name]&.shift - next unless addrinfo - - @last_family = family_name - return addrinfo + sock = local_addr ? + ai.connect_from(local_addr, timeout:) : + ai.connect(timeout:) + rescue SystemCallError + last_error = $! + next + end + ret = sock + break + } + unless ret + if last_error + raise last_error + else + raise SocketError, "no appropriate local address" end end - def empty? - @addrinfo_dict.all? { |_, addrinfos| addrinfos.empty? } - end - - def any? - !empty? - end + ret end - private_constant :SelectableAddrinfos - - class NoHostnameResolutionQueue - def waiting_pipe - nil - end + private_class_method :tcp_without_fast_fallback - def add_resolved(_, _) - raise StandardError, "This #{self.class} cannot respond to:" - end + def self.ip_address?(hostname) + hostname.match?(IPV6_ADDRESS_FORMAT) || hostname.match?(/\A([0-9]{1,3}\.){3}[0-9]{1,3}\z/) + end + private_class_method :ip_address? - def add_error(_, _) - raise StandardError, "This #{self.class} cannot respond to:" + def self.resolve_hostname(family, host, port, hostname_resolution_result) + begin + resolved_addrinfos = Addrinfo.getaddrinfo(host, port, ADDRESS_FAMILIES[family], :STREAM) + hostname_resolution_result.add(family, resolved_addrinfos) + rescue => e + hostname_resolution_result.add(family, e) end + end + private_class_method :resolve_hostname - def get - nil - end + def self.current_clock_time + Process.clock_gettime(Process::CLOCK_MONOTONIC) + end + private_class_method :current_clock_time - def opened? - false - end + def self.second_to_timeout(started_at, ends_at) + return nil if ends_at == Float::INFINITY || ends_at.nil? - def closed? - true - end + remaining = (ends_at - started_at) + remaining.negative? ? 0 : remaining + end + private_class_method :second_to_timeout - def close_all - # Do nothing - end + def self.expired?(started_at, ends_at) + second_to_timeout(started_at, ends_at)&.zero? end - private_constant :NoHostnameResolutionQueue + private_class_method :expired? - class HostnameResolutionQueue + class HostnameResolutionResult def initialize(size) @size = size @taken_count = 0 @rpipe, @wpipe = IO.pipe - @queue = Queue.new + @results = [] @mutex = Mutex.new end - def waiting_pipe + def notifier [@rpipe] end - def add_resolved(family, resolved_addrinfos) + def add(family, result) @mutex.synchronize do - @queue.push [family, resolved_addrinfos] - @wpipe.putc HOSTNAME_RESOLUTION_QUEUE_UPDATED - end - end - - def add_error(family, error) - @mutex.synchronize do - @queue.push [family, error] + @results.push [family, result] @wpipe.putc HOSTNAME_RESOLUTION_QUEUE_UPDATED end end def get - return nil if @queue.empty? + return nil if @results.empty? res = nil @mutex.synchronize do @rpipe.getbyte - res = @queue.pop + res = @results.shift end @taken_count += 1 - close_all if @taken_count == @size + close if @taken_count == @size res end - def closed? - @rpipe.closed? + def close + @rpipe.close + @wpipe.close end + end + private_constant :HostnameResolutionResult - def opened? - !closed? - end + class HostnameResolutionStore + PRIORITY_ON_V6 = [:ipv6, :ipv4].freeze + PRIORITY_ON_V4 = [:ipv4, :ipv6].freeze - def close_all - @queue.close unless @queue.closed? - @rpipe.close unless @rpipe.closed? - @wpipe.close unless @wpipe.closed? + def initialize(family_names) + @family_names = family_names + @addrinfo_dict = {} + @error_dict = {} + @last_family = nil end - end - private_constant :HostnameResolutionQueue - class ConnectingSockets - def initialize - @socket_dict = {} + def add_resolved(family_name, addrinfos) + @addrinfo_dict[family_name] = addrinfos end - def all - @socket_dict.keys + def add_error(family_name, error) + @addrinfo_dict[family_name] = [] + @error_dict[family_name] = error end - def add(socket, addrinfo) - @socket_dict[socket] = addrinfo - end + def get_addrinfo + precedences = + case @last_family + when :ipv4, nil then PRIORITY_ON_V6 + when :ipv6 then PRIORITY_ON_V4 + end + + precedences.each do |family_name| + addrinfo = @addrinfo_dict[family_name]&.shift + next unless addrinfo + + @last_family = family_name + return addrinfo + end - def delete(socket) - @socket_dict.delete socket + nil end - def empty? - @socket_dict.empty? + def empty_addrinfos? + @addrinfo_dict.all? { |_, addrinfos| addrinfos.empty? } end - def each - @socket_dict.keys.each do |socket| - yield socket - end + def any_addrinfos? + !empty_addrinfos? end - end - private_constant :ConnectingSockets - def self.tcp_without_fast_fallback(host, port, local_host, local_port, connect_timeout:, resolv_timeout:, &block) - last_error = nil - ret = nil + def resolved?(family) + @addrinfo_dict.has_key? family + end - local_addr_list = nil - if local_host != nil || local_port != nil - local_addr_list = Addrinfo.getaddrinfo(local_host, local_port, nil, :STREAM, nil) + def resolved_successfully?(family) + resolved?(family) && !@error_dict[family] end - Addrinfo.foreach(host, port, nil, :STREAM, timeout: resolv_timeout) {|ai| - if local_addr_list - local_addr = local_addr_list.find {|local_ai| local_ai.afamily == ai.afamily } - next unless local_addr - else - local_addr = nil - end - begin - sock = local_addr ? - ai.connect_from(local_addr, timeout: connect_timeout) : - ai.connect(timeout: connect_timeout) - rescue SystemCallError - last_error = $! - next - end - ret = sock - break - } - unless ret - if last_error - raise last_error - else - raise SocketError, "no appropriate local address" - end + def resolved_all_families? + (@family_names - @addrinfo_dict.keys).empty? end - if block_given? - begin - yield ret - ensure - ret.close - end - else - ret + + def any_unresolved_family? + !resolved_all_families? end end - private_class_method :tcp_without_fast_fallback + private_constant :HostnameResolutionStore - # :stopdoc: def self.ip_sockets_port0(ai_list, reuseaddr) sockets = [] begin @@ -1211,9 +1157,7 @@ class Socket < BasicSocket end sockets end - class << self - private :ip_sockets_port0 - end + private_class_method :ip_sockets_port0 def self.tcp_server_sockets_port0(host) ai_list = Addrinfo.getaddrinfo(host, 0, nil, :STREAM, nil, Socket::AI_PASSIVE) @@ -1641,13 +1585,18 @@ class Socket < BasicSocket end end - class << self - private - - def unix_socket_abstract_name?(path) - /linux/ =~ RUBY_PLATFORM && /\A(\0|\z)/ =~ path + # :stopdoc: + if RUBY_PLATFORM.include?("linux") + def self.unix_socket_abstract_name?(path) + path.empty? or path.start_with?("\0") + end + else + def self.unix_socket_abstract_name?(path) + false end end + private_class_method :unix_socket_abstract_name? + # :startdoc: # creates a UNIX socket server on _path_. # It calls the block for each socket accepted. @@ -1687,7 +1636,7 @@ class Socket < BasicSocket # Returns 0 if successful, otherwise an exception is raised. # # === Parameter - # # +remote_sockaddr+ - the +struct+ sockaddr contained in a string or Addrinfo object + # * +remote_sockaddr+ - the +struct+ sockaddr contained in a string or Addrinfo object # # === Example: # # Pull down Google's web page @@ -1722,7 +1671,7 @@ class Socket < BasicSocket # return the symbol +:wait_writable+ instead. # # === See - # # Socket#connect + # * Socket#connect def connect_nonblock(addr, exception: true) __connect_nonblock(addr, exception) end |
