1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136
|
# frozen_string_literal: true
require "resolv"
module HTTPX
module Resolver::Cache
# Base class of the Resolver Cache adapter implementations.
#
# While resolver caches are not required to inherit from this class, it nevertheless provides
# common useful functions for desired functionality, such as singleton object ractor-safe access,
# or a default #resolve implementation which deals with IPs and the system hosts file.
#
class Base
MAX_CACHE_SIZE = 512
CACHE_MUTEX = Thread::Mutex.new
HOSTS = Resolv::Hosts.new
@cache = nil
class << self
attr_reader :hosts_resolver
# returns the singleton instance to be used within the current ractor.
def cache(label)
return Ractor.store_if_absent(:"httpx_resolver_cache_#{label}") { new } if Utils.in_ractor?
@cache ||= CACHE_MUTEX.synchronize do
@cache || new
end
end
end
# resolves +hostname+ into an instance of HTTPX::Resolver::Entry if +hostname+ is an IP,
# or can be found in the cache, or can be found in the system hosts file.
def resolve(hostname)
ip_resolve(hostname) || get(hostname) || hosts_resolve(hostname)
end
private
# tries to convert +hostname+ into an IPAddr, returns <tt>nil</tt> otherwise.
def ip_resolve(hostname)
[Resolver::Entry.new(hostname)]
rescue ArgumentError
end
# matches +hostname+ to entries in the hosts file, returns <tt>nil</nil> if none is
# found, or there is no hosts file.
def hosts_resolve(hostname)
ips = if Utils.in_ractor?
Ractor.store_if_absent(:httpx_hosts_resolver) { Resolv::Hosts.new }
else
HOSTS
end.getaddresses(hostname)
return if ips.empty?
ips.map { |ip| Resolver::Entry.new(ip) }
rescue IOError
end
# not to be used directly!
def _get(hostname, lookups, hostnames, ttl)
return unless lookups.key?(hostname)
entries = lookups[hostname]
return unless entries
entries.delete_if do |address|
address["TTL"] < ttl
end
if entries.empty?
lookups.delete(hostname)
hostnames.delete(hostname)
end
ips = entries.flat_map do |address|
if (als = address["alias"])
_get(als, lookups, hostnames, ttl)
else
Resolver::Entry.new(address["data"], address["TTL"])
end
end.compact
ips unless ips.empty?
end
def _set(hostname, family, entries, lookups, hostnames)
# lru cleanup
while lookups.size >= MAX_CACHE_SIZE
hs = hostnames.shift
lookups.delete(hs)
end
hostnames << hostname
lookups[hostname] ||= [] # when there's no default proc
case family
when Socket::AF_INET6
lookups[hostname].concat(entries)
when Socket::AF_INET
lookups[hostname].unshift(*entries)
end
entries.each do |entry|
name = entry["name"]
next unless name != hostname
lookups[name] ||= []
case family
when Socket::AF_INET6
lookups[name] << entry
when Socket::AF_INET
lookups[name].unshift(entry)
end
end
end
def _evict(hostname, ip, lookups, hostnames)
return unless lookups.key?(hostname)
entries = lookups[hostname]
return unless entries
entries.delete_if { |entry| entry["data"] == ip }
return unless entries.empty?
lookups.delete(hostname)
hostnames.delete(hostname)
end
end
end
end
|