async-dns 1.2.6 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ea5c9c51849ba12863030e14261a5475bfa1d1ab52e39a2c845dafdf277683d7
4
- data.tar.gz: ac45a456ef1cc82a2bd01046a549e37cb72908c42d5429bc6d6818b02ae63845
3
+ metadata.gz: 87d280ed7be9505a629d09e7ffad5135723a80cee1ac849fa8d19597a833eb09
4
+ data.tar.gz: d2a037310e59fbefb715cbf6c369be55f2eb567636066605b82ede388886ec04
5
5
  SHA512:
6
- metadata.gz: b673b004c18bd79e2cea44fe66e3236e0843d3ad46ed68cec7db26302f6baa34593ae62f7f0eb826be87abe518bdb0c21035098881224aa4132239cc66091e4d
7
- data.tar.gz: 46a55367035e353d9494cba6433bbd063c01d96675de811af4f5f6fa98d499b1e0e3cb3f0edf7773af34742c799308d5767abcd53a4002658e4f8de11eda04eb
6
+ metadata.gz: 685fdcd3685e465742bd2c47d53937a332739c9bc337ac3542d30d2d5fd6e4f213482498b0780be7a666b7c5956596dfdb51a6b213687772c4d3e52c3222d298
7
+ data.tar.gz: 68af6b3a768695e02a5be1a71a162ba528ff68b01988aa67c3c41552b67af5c6d06054515ec20920ac959fe2cf551919535fff9fbcde30c08aa5406b53b8f285
checksums.yaml.gz.sig ADDED
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