File: base.rb

package info (click to toggle)
ruby-httpx 1.7.2-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 1,816 kB
  • sloc: ruby: 12,209; makefile: 4
file content (136 lines) | stat: -rw-r--r-- 3,755 bytes parent folder | download
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