async-dns 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 82551b95c437170352dfde80210008c1a9df9bebcbf750b1d5b51e064f0b8f43
4
- data.tar.gz: e02fdbd72153bee022c157d9855a60a10db6da9b7067a5159db089ebc8fd63b4
3
+ metadata.gz: 87d280ed7be9505a629d09e7ffad5135723a80cee1ac849fa8d19597a833eb09
4
+ data.tar.gz: d2a037310e59fbefb715cbf6c369be55f2eb567636066605b82ede388886ec04
5
5
  SHA512:
6
- metadata.gz: c4de0e486a37755904384a41685aabd850a9a86f41bb7371f1c2986779bbad06c4bbb3488e2f9e47724bb11832867efbc04c3d1676fa28c737266416b8336215
7
- data.tar.gz: 7093b96f0f8bcf457ac88e03fff520298a359ed55d99f69b05453410d0e89703e7e6090720583ac223bf29b3f0e9300f31e46eab91f86f862ccf671cbe7ea824
6
+ metadata.gz: 685fdcd3685e465742bd2c47d53937a332739c9bc337ac3542d30d2d5fd6e4f213482498b0780be7a666b7c5956596dfdb51a6b213687772c4d3e52c3222d298
7
+ data.tar.gz: 68af6b3a768695e02a5be1a71a162ba528ff68b01988aa67c3c41552b67af5c6d06054515ec20920ac959fe2cf551919535fff9fbcde30c08aa5406b53b8f285
checksums.yaml.gz.sig CHANGED
Binary file
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2017-2025, by Samuel Williams.
5
+
6
+ module Async
7
+ module DNS
8
+ # Provides a local in-memory cache for DNS resources.
9
+ class Cache
10
+ Entry = Struct.new(:timestamp, :name, :resource_class, :resource) do
11
+ def age(now)
12
+ now - timestamp
13
+ end
14
+
15
+ def fresh?(now = Async::Clock.now)
16
+ if ttl = resource.ttl
17
+ self.age(now) <= ttl
18
+ else
19
+ true
20
+ end
21
+ end
22
+ end
23
+
24
+ # Create a new cache.
25
+ def initialize
26
+ @store = {}
27
+ end
28
+
29
+ # Fetch a resource from the cache, or if it is not present, yield to the block to fetch it.
30
+ #
31
+ # @parameter name [String] The name of the resource.
32
+ # @parameter resource_classes [Array(Class(Resolv::DNS::Resource))] The classes of the resources to fetch.
33
+ # @yields {|name, resource_class| ...} The block to fetch the resource, it should call {#store} to store the resource in the cache.
34
+ def fetch(name, resource_classes)
35
+ now = Async::Clock.now
36
+
37
+ resource_classes.map do |resource_class|
38
+ key = [name, resource_class]
39
+
40
+ if entries = @store[key]
41
+ entries.delete_if do |entry|
42
+ !entry.fresh?(now)
43
+ end
44
+ else
45
+ entries = (@store[key] = [])
46
+ end
47
+
48
+ if entries.empty?
49
+ yield(name, resource_class)
50
+ end
51
+
52
+ entries
53
+ end.flatten.map(&:resource)
54
+ end
55
+
56
+ # Store a resource in the cache.
57
+ #
58
+ # @parameter name [String] The name of the resource.
59
+ # @parameter resource_class [Class(Resolv::DNS::Resource)] The class of the resource.
60
+ # @parameter resource [Resolv::DNS::Resource] The resource to store.
61
+ def store(name, resource_class, resource)
62
+ key = [name, resource_class]
63
+ entries = (@store[key] ||= [])
64
+
65
+ entries << Entry.new(Async::Clock.now, name, resource_class, resource)
66
+ end
67
+ end
68
+ end
69
+ end
@@ -1,22 +1,7 @@
1
- # Copyright, 2009, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
- #
3
- # Permission is hereby granted, free of charge, to any person obtaining a copy
4
- # of this software and associated documentation files (the "Software"), to deal
5
- # in the Software without restriction, including without limitation the rights
6
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
- # copies of the Software, and to permit persons to whom the Software is
8
- # furnished to do so, subject to the following conditions:
9
- #
10
- # The above copyright notice and this permission notice shall be included in
11
- # all copies or substantial portions of the Software.
12
- #
13
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
- # THE SOFTWARE.
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2015-2024, by Samuel Williams.
20
5
 
21
6
  module Async::DNS
22
7
  # Produces an array of arrays of binary data with each sub-array a maximum of chunk_size bytes.
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Released under the MIT License.
4
+ # Copyright, 2024-2025, by Samuel Williams.
5
+
6
+ module Async
7
+ module DNS
8
+ # DNS endpoint helpers.
9
+ module Endpoint
10
+ # Get a list of standard nameserver connections which can be used for querying any standard servers that the system has been configured with.
11
+ def self.for(nameservers, port: 53, **options)
12
+ connections = []
13
+
14
+ Array(nameservers).each do |host|
15
+ connections << IO::Endpoint.udp(host, port, **options)
16
+ connections << IO::Endpoint.tcp(host, port, **options)
17
+ end
18
+
19
+ return IO::Endpoint.composite(*connections)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -1,27 +1,15 @@
1
- # Copyright, 2009, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
- #
3
- # Permission is hereby granted, free of charge, to any person obtaining a copy
4
- # of this software and associated documentation files (the "Software"), to deal
5
- # in the Software without restriction, including without limitation the rights
6
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
- # copies of the Software, and to permit persons to whom the Software is
8
- # furnished to do so, subject to the following conditions:
9
- #
10
- # The above copyright notice and this permission notice shall be included in
11
- # all copies or substantial portions of the Software.
12
- #
13
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
- # THE SOFTWARE.
1
+ # frozen_string_literal: true
20
2
 
21
- require 'resolv'
3
+ # Released under the MIT License.
4
+ # Copyright, 2015-2024, by Samuel Williams.
22
5
 
6
+ require "resolv"
7
+
8
+ # @namespace
23
9
  class Resolv
10
+ # @namespace
24
11
  class DNS
12
+ # Extensions to the `Resolv::DNS::Message` class.
25
13
  class Message
26
14
  # Merge the given message with this message. A number of heuristics are applied in order to ensure that the result makes sense. For example, If the current message is not recursive but is being merged with a message that was recursive, this bit is maintained. If either message is authoritive, then the result is also authoritive.
27
15
  #
@@ -46,19 +34,17 @@ class Resolv
46
34
  end
47
35
  end
48
36
 
37
+ # Represents a failure to construct a fullly qualified name due to a mismatched origin.
49
38
  class OriginError < ArgumentError
50
39
  end
51
40
 
41
+ # Extensions to the `Resolv::DNS::Name` class.
52
42
  class Name
53
- def to_s
54
- "#{@labels.join('.')}#{@absolute ? '.' : ''}"
55
- end
56
-
57
- def inspect
58
- "#<#{self.class}: #{self.to_s}>"
59
- end
60
-
61
- # Return the name, typically absolute, with the specified origin as a suffix. If the origin is nil, don't change the name, but change it to absolute (as specified).
43
+ # Computes the name, typically absolute, with the specified origin as a suffix. If the origin is nil, don't change the name, but change it to absolute (as specified).
44
+ #
45
+ # @parameter origin [Array | String] The origin to append to the name.
46
+ # @parameter absolute [Boolean] If true, the name will be made absolute.
47
+ # @returns The name, with the origin suffix.
62
48
  def with_origin(origin, absolute = true)
63
49
  return self.class.new(@labels, absolute) if origin == nil
64
50
 
@@ -67,7 +53,12 @@ class Resolv
67
53
  return self.class.new(@labels + origin, absolute)
68
54
  end
69
55
 
70
- # Return the name, typically relative, without the specified origin suffix. If the origin is nil, don't change the name, but change it to absolute (as specified).
56
+ # Compute the name, typically relative, without the specified origin suffix. If the origin is nil, don't change the name, but change it to absolute (as specified).
57
+ #
58
+ # @parameter origin [Array | String] The origin to remove from the name.
59
+ # @parameter absolute [Boolean] If true, the name will be made absolute.
60
+ # @returns The name, without the origin suffix.
61
+ # @raises [OriginError] If the name does not end with the specified origin.
71
62
  def without_origin(origin, absolute = false)
72
63
  return self.class.new(@labels, absolute) if origin == nil
73
64
 
@@ -81,55 +72,4 @@ class Resolv
81
72
  end
82
73
  end
83
74
  end
84
-
85
- if RUBY_VERSION == "2.3.0"
86
- # Clearly, the Ruby 2.3.0 release was throughly tested.
87
- class IPv6
88
- def self.create(arg)
89
- case arg
90
- when IPv6
91
- return arg
92
- when String
93
- address = ''.b
94
- if Regex_8Hex =~ arg
95
- arg.scan(/[0-9A-Fa-f]+/) {|hex| address << [hex.hex].pack('n')}
96
- elsif Regex_CompressedHex =~ arg
97
- prefix = $1
98
- suffix = $2
99
- a1 = ''.b
100
- a2 = ''.b
101
- prefix.scan(/[0-9A-Fa-f]+/) {|hex| a1 << [hex.hex].pack('n')}
102
- suffix.scan(/[0-9A-Fa-f]+/) {|hex| a2 << [hex.hex].pack('n')}
103
- omitlen = 16 - a1.length - a2.length
104
- address << a1 << "\0" * omitlen << a2
105
- elsif Regex_6Hex4Dec =~ arg
106
- prefix, a, b, c, d = $1, $2.to_i, $3.to_i, $4.to_i, $5.to_i
107
- if (0..255) === a && (0..255) === b && (0..255) === c && (0..255) === d
108
- prefix.scan(/[0-9A-Fa-f]+/) {|hex| address << [hex.hex].pack('n')}
109
- address << [a, b, c, d].pack('CCCC')
110
- else
111
- raise ArgumentError.new("not numeric IPv6 address: " + arg)
112
- end
113
- elsif Regex_CompressedHex4Dec =~ arg
114
- prefix, suffix, a, b, c, d = $1, $2, $3.to_i, $4.to_i, $5.to_i, $6.to_i
115
- if (0..255) === a && (0..255) === b && (0..255) === c && (0..255) === d
116
- a1 = ''.b
117
- a2 = ''.b
118
- prefix.scan(/[0-9A-Fa-f]+/) {|hex| a1 << [hex.hex].pack('n')}
119
- suffix.scan(/[0-9A-Fa-f]+/) {|hex| a2 << [hex.hex].pack('n')}
120
- omitlen = 12 - a1.length - a2.length
121
- address << a1 << "\0" * omitlen << a2 << [a, b, c, d].pack('CCCC')
122
- else
123
- raise ArgumentError.new("not numeric IPv6 address: " + arg)
124
- end
125
- else
126
- raise ArgumentError.new("not numeric IPv6 address: " + arg)
127
- end
128
- return IPv6.new(address)
129
- else
130
- raise ArgumentError.new("cannot interpret as IPv6 address: #{arg.inspect}")
131
- end
132
- end
133
- end
134
- end
135
- end
75
+ end
@@ -1,25 +1,11 @@
1
- # Copyright, 2009, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
- #
3
- # Permission is hereby granted, free of charge, to any person obtaining a copy
4
- # of this software and associated documentation files (the "Software"), to deal
5
- # in the Software without restriction, including without limitation the rights
6
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
- # copies of the Software, and to permit persons to whom the Software is
8
- # furnished to do so, subject to the following conditions:
9
- #
10
- # The above copyright notice and this permission notice shall be included in
11
- # all copies or substantial portions of the Software.
12
- #
13
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
- # THE SOFTWARE.
1
+ # frozen_string_literal: true
20
2
 
21
- require_relative '../chunked'
3
+ # Released under the MIT License.
4
+ # Copyright, 2015-2024, by Samuel Williams.
22
5
 
6
+ require_relative "../chunked"
7
+
8
+ # Extensions for the String class.
23
9
  class String
24
10
  # Chunk a string which is required for the TEXT `resource_class`.
25
11
  def chunked(chunk_size = 255)
@@ -1,39 +1,43 @@
1
- # Copyright, 2009, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
2
- #
3
- # Permission is hereby granted, free of charge, to any person obtaining a copy
4
- # of this software and associated documentation files (the "Software"), to deal
5
- # in the Software without restriction, including without limitation the rights
6
- # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
- # copies of the Software, and to permit persons to whom the Software is
8
- # furnished to do so, subject to the following conditions:
9
- #
10
- # The above copyright notice and this permission notice shall be included in
11
- # all copies or substantial portions of the Software.
12
- #
13
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
- # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
- # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
- # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
- # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
- # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
- # THE SOFTWARE.
1
+ # frozen_string_literal: true
20
2
 
21
- require 'async/io/stream'
3
+ # Released under the MIT License.
4
+ # Copyright, 2015-2025, by Samuel Williams.
5
+ # Copyright, 2021, by Mike Perham.
22
6
 
23
- require_relative 'transport'
7
+ require "resolv"
8
+ require_relative "extensions/resolv"
9
+
10
+ require_relative "transport"
24
11
 
25
12
  module Async::DNS
13
+ # The maximum size of a normal DNS packet (excluding EDNS).
14
+ UDP_REASONABLE_SIZE = 512
15
+
16
+ # The maximum size of a UDP packet.
17
+ UDP_MAXIMUM_SIZE = 2**16
18
+
19
+ # A generic handler for DNS queries.
26
20
  class GenericHandler
21
+ # Create a new handler.
22
+ #
23
+ # @parameter server [Server] The server to handle queries for.
24
+ # @parameter socket [Socket] The socket to read/write data from/to.
27
25
  def initialize(server, socket)
28
26
  @server = server
29
27
  @socket = socket
30
-
31
- @logger = @server.logger || Console.logger
32
28
  end
33
29
 
30
+ # @attribute [Server] The server that will process incoming queries.
34
31
  attr :server
32
+
33
+ # @attribute [Socket] The socket to read/write data from/to.
35
34
  attr :socket
36
35
 
36
+ # Create a new error response.
37
+ #
38
+ # @parameter query [Resolv::DNS::Message] The query that caused the error.
39
+ # @parameter code [Integer] The error code to return.
40
+ # @returns [Resolv::DNS::Message] The error response.
37
41
  def error_response(query = nil, code = Resolv::DNS::RCode::ServFail)
38
42
  # Encoding may fail, so we need to handle this particular case:
39
43
  server_failure = Resolv::DNS::Message::new(query ? query.id : 0)
@@ -50,43 +54,50 @@ module Async::DNS
50
54
  return server_failure
51
55
  end
52
56
 
53
- def process_query(data, options)
54
- @logger.debug "<> Receiving incoming query (#{data.bytesize} bytes) to #{self.class.name}..."
55
- query = nil
56
-
57
+ # Process an incoming query.
58
+ #
59
+ # @parameter data [String] The incoming query data.
60
+ # @parameter options [Hash] Additional options to pass to the server.
61
+ def process_query(data, **options)
62
+ Console.debug "Receiving incoming query (#{data.bytesize} bytes) to #{self.class.name}..."
63
+
57
64
  begin
58
- query = Async::DNS::decode_message(data)
65
+ query = Resolv::DNS::Message.decode(data)
59
66
 
60
- return @server.process_query(query, options)
61
- rescue StandardError => error
62
- @logger.error(self) { error }
67
+ return @server.process_query(query, **options)
68
+ rescue => error
69
+ Console.error(self, "Failed to process query!", error: error)
63
70
 
64
71
  return error_response(query)
65
72
  end
66
73
  end
67
74
  end
68
75
 
69
- # Handling incoming UDP requests, which are single data packets, and pass them on to the given server.
76
+ # Handle incoming UDP requests, which are single data packets, and pass them on to the given server.
70
77
  class DatagramHandler < GenericHandler
71
- def run(task: Async::Task.current)
78
+ # Run the handler, processing incoming UDP requests.
79
+ #
80
+ # @parameter wrapper [Interface(:async)] The parent task to run the handler under.
81
+ def run(wrapper = ::IO::Endpoint::Wrapper.default)
72
82
  while true
73
- input_data, remote_address = @socket.recvmsg(UDP_TRUNCATION_SIZE)
83
+ input_data, remote_address = @socket.recvmsg(UDP_MAXIMUM_SIZE)
74
84
 
75
- task.async do
85
+ wrapper.async do
76
86
  respond(@socket, input_data, remote_address)
77
87
  end
78
88
  end
79
89
  end
80
90
 
91
+ # Respond to an incoming query.
81
92
  def respond(socket, input_data, remote_address)
82
93
  response = process_query(input_data, remote_address: remote_address)
83
94
 
84
95
  output_data = response.encode
85
96
 
86
- @logger.debug "<#{response.id}> Writing #{output_data.bytesize} bytes response to client via UDP..."
97
+ Console.debug "Writing #{output_data.bytesize} bytes response to client via UDP...", response_id: response.id
87
98
 
88
- if output_data.bytesize > UDP_TRUNCATION_SIZE
89
- @logger.warn "<#{response.id}> Response via UDP was larger than #{UDP_TRUNCATION_SIZE}!"
99
+ if output_data.bytesize > UDP_REASONABLE_SIZE
100
+ Console.warn "Response via UDP was larger than #{UDP_REASONABLE_SIZE}!", response_id: response.id
90
101
 
91
102
  # Reencode data with truncation flag marked as true:
92
103
  truncation_error = Resolv::DNS::Message.new(response.id)
@@ -97,23 +108,30 @@ module Async::DNS
97
108
 
98
109
  socket.sendmsg(output_data, 0, remote_address)
99
110
  rescue IOError => error
100
- @logger.warn "<> UDP response failed: #{error.inspect}!"
111
+ Console.error(self, "UDP response failed!", error: error)
101
112
  rescue EOFError => error
102
- @logger.warn "<> UDP session ended prematurely: #{error.inspect}!"
103
- rescue DecodeError
104
- @logger.warn "<> Could not decode incoming UDP data!"
113
+ Console.error(self, "UDP session ended prematurely!", error: error)
114
+ rescue Resolv::DNS::DecodeError => error
115
+ Console.error(self, "Could not decode incoming UDP data!", error: error)
105
116
  end
106
117
  end
107
118
 
119
+ # Handle incoming TCP requests, which are stream requests, and pass them on to the given server.
108
120
  class StreamHandler < GenericHandler
109
- def run(backlog = Socket::SOMAXCONN)
110
- @socket.listen(backlog)
111
-
112
- @socket.accept_each do |client, address|
113
- handle_connection(client)
121
+ # Run the handler, processing incoming TCP requests.
122
+ #
123
+ # @parameter wrapper [Interface(:async)] The parent task to run the handler under.
124
+ def run(wrapper = ::IO::Endpoint::Wrapper.default, **options)
125
+ wrapper.accept(@socket, **options) do |peer|
126
+ handle_connection(peer)
114
127
  end
115
128
  end
116
129
 
130
+ # Handle an incoming TCP connection.
131
+ #
132
+ # Reads zero or more queries from the given socket and processes them.
133
+ #
134
+ # @parameter socket [Socket] The incoming TCP connection.
117
135
  def handle_connection(socket)
118
136
  transport = Transport.new(socket)
119
137
 
@@ -121,16 +139,16 @@ module Async::DNS
121
139
  response = process_query(input_data, remote_address: socket.remote_address)
122
140
  length = transport.write_message(response)
123
141
 
124
- @logger.debug "<#{response.id}> Wrote #{length} bytes via TCP..."
142
+ Console.debug "Wrote #{length} bytes via TCP...", response_id: response.id
125
143
  end
126
144
  rescue EOFError => error
127
- @logger.warn "<> Error: TCP session ended prematurely!"
145
+ Console.error(self, "TCP session ended prematurely!", error: error)
128
146
  rescue Errno::ECONNRESET => error
129
- @logger.warn "<> Error: TCP connection reset by peer!"
130
- rescue Errno::EPIPE
131
- @logger.warn "<> Error: TCP session failed due to broken pipe!"
132
- rescue DecodeError
133
- @logger.warn "<> Error: Could not decode incoming TCP data!"
147
+ Console.error(self, "TCP connection reset by peer!", error: error)
148
+ rescue Errno::EPIPE => error
149
+ Console.error(self, "TCP session failed due to broken pipe!", error: error)
150
+ rescue Resolv::DNS::DecodeError => error
151
+ Console.error(self, "Could not decode incoming TCP data!", error: error)
134
152
  end
135
153
  end
136
154
  end