|
1 | 1 | # frozen_string_literal: true |
2 | 2 |
|
| 3 | +require 'resolv' |
| 4 | + |
3 | 5 | module Dalli |
4 | 6 | module Socket |
5 | 7 | module InstanceMethods |
@@ -64,8 +66,7 @@ class TCP < ::Socket |
64 | 66 | attr_accessor :options, :server |
65 | 67 |
|
66 | 68 | def self.open(host, port, server, options = {}) |
67 | | - addr_info = ::Socket.getaddrinfo(host, nil, ::Socket::AF_UNSPEC, ::Socket::SOCK_STREAM) |
68 | | - ai = addr_info.first |
| 69 | + ai = resolve_address(host, options[:socket_timeout]) |
69 | 70 | sock = new(ai[4], ::Socket::SOCK_STREAM, 0) |
70 | 71 |
|
71 | 72 | sock.setsockopt(::Socket::IPPROTO_TCP, ::Socket::TCP_NODELAY, true) |
@@ -93,6 +94,36 @@ def self.open(host, port, server, options = {}) |
93 | 94 | sock&.close rescue nil |
94 | 95 | raise |
95 | 96 | end |
| 97 | + |
| 98 | + # Resolve a hostname to structured address info with timeout protection. |
| 99 | + # getaddrinfo(3) is a blocking C library call that can block indefinitely |
| 100 | + # on unresponsive DNS. For IP addresses (the common case with memcached), |
| 101 | + # getaddrinfo returns immediately without DNS and is safe to call directly. |
| 102 | + # For hostnames, we use Ruby's Resolv library which is pure Ruby and |
| 103 | + # supports timeouts, then pass the resolved IP to getaddrinfo for the |
| 104 | + # structured address info the caller expects. |
| 105 | + def self.resolve_address(host, timeout) |
| 106 | + if ip_address?(host) |
| 107 | + return ::Socket.getaddrinfo(host, nil, ::Socket::AF_UNSPEC, ::Socket::SOCK_STREAM).first |
| 108 | + end |
| 109 | + |
| 110 | + dns = Resolv::DNS.new |
| 111 | + dns.timeouts = timeout |
| 112 | + resolver = Resolv.new([Resolv::Hosts.new, dns]) |
| 113 | + resolved_ip = resolver.getaddress(host).to_s |
| 114 | + ::Socket.getaddrinfo(resolved_ip, nil, ::Socket::AF_UNSPEC, ::Socket::SOCK_STREAM).first |
| 115 | + rescue Resolv::ResolvError => e |
| 116 | + raise SocketError, "getaddrinfo: Name or service not known - #{host} (#{e.message})" |
| 117 | + ensure |
| 118 | + dns&.close |
| 119 | + end |
| 120 | + private_class_method :resolve_address |
| 121 | + |
| 122 | + # Returns true if host is an IP address (v4 or v6) rather than a hostname. |
| 123 | + def self.ip_address?(host) |
| 124 | + host.match?(/\A\d{1,3}(\.\d{1,3}){3}\z/) || host.include?(':') |
| 125 | + end |
| 126 | + private_class_method :ip_address? |
96 | 127 | end |
97 | 128 |
|
98 | 129 | class UNIX < ::Socket |
|
0 commit comments