toolmantim-zeroconf 0.0.2
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.
- data/README.rdoc +44 -0
- data/Rakefile +71 -0
- data/lib/dnssd.rb +137 -0
- data/lib/net/dns.rb +49 -0
- data/lib/net/dns/mdns-sd.rb +240 -0
- data/lib/net/dns/mdns.rb +1189 -0
- data/lib/net/dns/resolv-mdns.rb +230 -0
- data/lib/net/dns/resolv-replace.rb +66 -0
- data/lib/net/dns/resolv.rb +2012 -0
- data/lib/net/dns/resolvx.rb +219 -0
- data/lib/zeroconf.rb +15 -0
- data/lib/zeroconf/common.rb +0 -0
- data/lib/zeroconf/ext.rb +7 -0
- data/lib/zeroconf/pure.rb +13 -0
- data/lib/zeroconf/version.rb +3 -0
- data/originals/dnssd-0.6.0/COPYING +56 -0
- data/originals/dnssd-0.6.0/README +50 -0
- data/originals/net-mdns-0.4/COPYING +58 -0
- data/originals/net-mdns-0.4/README +21 -0
- data/originals/net-mdns-0.4/TODO +278 -0
- data/samples/exhttp.rb +50 -0
- data/samples/exhttpv1.rb +29 -0
- data/samples/exwebrick.rb +56 -0
- data/samples/mdns-watch.rb +132 -0
- data/samples/mdns.rb +238 -0
- data/samples/test_dns.rb +128 -0
- data/samples/v1demo.rb +167 -0
- data/samples/v1mdns.rb +111 -0
- data/test/stress/stress_register.rb +48 -0
- data/test/test_browse.rb +24 -0
- data/test/test_highlevel_api.rb +35 -0
- data/test/test_register.rb +32 -0
- data/test/test_resolve.rb +23 -0
- data/test/test_resolve_ichat.rb +56 -0
- data/test/test_textrecord.rb +58 -0
- metadata +93 -0
@@ -0,0 +1,230 @@
|
|
1
|
+
=begin
|
2
|
+
Copyright (C) 2005 Sam Roberts
|
3
|
+
|
4
|
+
This library is free software; you can redistribute it and/or modify it
|
5
|
+
under the same terms as the ruby language itself, see the file COPYING for
|
6
|
+
details.
|
7
|
+
=end
|
8
|
+
|
9
|
+
require 'net/dns/resolvx'
|
10
|
+
require 'net/dns/mdns'
|
11
|
+
|
12
|
+
class Resolv
|
13
|
+
# == Address Lookups
|
14
|
+
# Requiring 'net/dns/mdns-resolv' causes a Resolv::MDNS resolver to be added
|
15
|
+
# to list of default resolvers queried when using Resolv#getaddress, and the
|
16
|
+
# other Resolv module methods.
|
17
|
+
#
|
18
|
+
# It can be used by doing:
|
19
|
+
# require 'net/dns/resolv-mdns'
|
20
|
+
# Resolv.getaddress('localhost') # resolved using Resolv::Hosts("/etc/hosts")
|
21
|
+
# Resolv.getaddress('www.example.com') # resolved using Resolv::DNS
|
22
|
+
# Resolv.getaddress('example.local') # resolved using Resolv::MDNS
|
23
|
+
#
|
24
|
+
# Using this approach means that both global DNS names and local names can be
|
25
|
+
# resolved. When doing this, you may also consider doing:
|
26
|
+
#
|
27
|
+
# require 'net/dns/resolv-mdns'
|
28
|
+
# require 'net/dns/resolv-replace'
|
29
|
+
#
|
30
|
+
# This has the effect of replacing the default ruby implementation of address
|
31
|
+
# lookup using the C library in IPSocket, TCPSocket, UDPSocket, and
|
32
|
+
# SOCKSocket with Resolv.getaddress. Since 'net/dns/resolv-mdns' has been
|
33
|
+
# required Resolv.getaddress and the standard libraries TCP/IP classes will
|
34
|
+
# use mDNS for name lookups in the .local mDNS domain, without even knowing
|
35
|
+
# it themselves.
|
36
|
+
#
|
37
|
+
# NOTE: the version of resolv.rb and resolv-replace.rb in net-mdns are based
|
38
|
+
# on the head of ruby 1.8.x cvs + bug fixes required by net-mdns and not
|
39
|
+
# present in the cvs. They must be used in place of the standard library's
|
40
|
+
# resolv implementation!
|
41
|
+
#
|
42
|
+
# == Service Discovery (DNS-SD)
|
43
|
+
#
|
44
|
+
# Service discovery consists of 2 stages:
|
45
|
+
# - enumerating the names of the instances of the service
|
46
|
+
# - resolving the instance names
|
47
|
+
#
|
48
|
+
# The Net::DNS::MDNSSD API is better documented and easier to use for DNS-SD.
|
49
|
+
# Still, here's some information on using the Resolv APIs for DNS-SD, and
|
50
|
+
# examples of doing so are:
|
51
|
+
# - link:exhttpv1.txt
|
52
|
+
# - link:v1mdns.txt
|
53
|
+
# - link:v1demo.txt.
|
54
|
+
#
|
55
|
+
# = Service Enumeration
|
56
|
+
#
|
57
|
+
# To do this query the pointer records (Resolv::DNS::Resource::IN::PTR) for
|
58
|
+
# names of the form _svc._prot.local. The values of svc and prot for common
|
59
|
+
# services can be found at http://www.dns-sd.org/ServiceTypes.html.
|
60
|
+
# The first label of the name returned is suitable for display to people, and
|
61
|
+
# should be unique in the network.
|
62
|
+
#
|
63
|
+
# = Service Resolution
|
64
|
+
#
|
65
|
+
# In order to resolve a service name query the service record
|
66
|
+
# (Resolv::DNS::Resource::IN::SRV) for the name. The service record contains
|
67
|
+
# a host and port to connect to. The host name will have to be resolved to an
|
68
|
+
# address. This can be done explicitly using mDNS or, if resolv-replace has
|
69
|
+
# been required, it will be done by the standard library. In addition, some
|
70
|
+
# services put "extra" information about the service in a text
|
71
|
+
# (Resolv::DNS::Resource::IN::TXT) record associated with the service name.
|
72
|
+
# The format of the text record is service-specific.
|
73
|
+
class MDNS
|
74
|
+
|
75
|
+
# How many seconds to wait before assuming all responses have been seen.
|
76
|
+
DefaultTimeout = 2
|
77
|
+
|
78
|
+
# See Resolv::DNS#new.
|
79
|
+
def initialize(config_info=nil)
|
80
|
+
@mutex = Mutex.new
|
81
|
+
@config = DNS::Config.new(config_info)
|
82
|
+
@initialized = nil
|
83
|
+
end
|
84
|
+
|
85
|
+
def lazy_initialize # :nodoc:
|
86
|
+
@mutex.synchronize do
|
87
|
+
unless @initialized
|
88
|
+
@config.lazy_initialize
|
89
|
+
@initialized = true
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
# See Resolv::DNS#getaddress.
|
95
|
+
def getaddress(name)
|
96
|
+
each_address(name) {|address| return address}
|
97
|
+
raise ResolvError.new("mDNS result has no information for #{name}")
|
98
|
+
end
|
99
|
+
|
100
|
+
# See Resolv::DNS#getaddresss.
|
101
|
+
def getaddresses(name)
|
102
|
+
ret = []
|
103
|
+
each_address(name) {|address| ret << address}
|
104
|
+
return ret
|
105
|
+
end
|
106
|
+
|
107
|
+
# See Resolv::DNS#each_address.
|
108
|
+
def each_address(name)
|
109
|
+
each_resource(name, DNS::Resource::IN::A) {|resource| yield resource.address}
|
110
|
+
end
|
111
|
+
|
112
|
+
# See Resolv::DNS#getname.
|
113
|
+
def getname(address)
|
114
|
+
each_name(address) {|name| return name}
|
115
|
+
raise ResolvError.new("mDNS result has no information for #{address}")
|
116
|
+
end
|
117
|
+
|
118
|
+
# See Resolv::DNS#getnames.
|
119
|
+
def getnames(address)
|
120
|
+
ret = []
|
121
|
+
each_name(address) {|name| ret << name}
|
122
|
+
return ret
|
123
|
+
end
|
124
|
+
|
125
|
+
# See Resolv::DNS#each_name.
|
126
|
+
def each_name(address)
|
127
|
+
case address
|
128
|
+
when DNS::Name
|
129
|
+
ptr = address
|
130
|
+
when IPv4::Regex
|
131
|
+
ptr = IPv4.create(address).to_name
|
132
|
+
when IPv6::Regex
|
133
|
+
ptr = IPv6.create(address).to_name
|
134
|
+
else
|
135
|
+
raise ResolvError.new("cannot interpret as address: #{address}")
|
136
|
+
end
|
137
|
+
each_resource(ptr, DNS::Resource::IN::PTR) {|resource| yield resource.name}
|
138
|
+
end
|
139
|
+
|
140
|
+
# See Resolv::DNS#getresource.
|
141
|
+
def getresource(name, typeclass)
|
142
|
+
each_resource(name, typeclass) {|resource| return resource}
|
143
|
+
raise ResolvError.new("mDNS result has no information for #{name}")
|
144
|
+
end
|
145
|
+
|
146
|
+
# See Resolv::DNS#getresources.
|
147
|
+
def getresources(name, typeclass)
|
148
|
+
ret = []
|
149
|
+
each_resource(name, typeclass) {|resource| ret << resource}
|
150
|
+
return ret
|
151
|
+
end
|
152
|
+
|
153
|
+
def generate_candidates(name) # :nodoc:
|
154
|
+
# Names ending in .local MUST be resolved using mDNS. Other names may be, but
|
155
|
+
# SHOULD NOT be, so a local machine can't spoof a non-local address.
|
156
|
+
#
|
157
|
+
# Reverse lookups in the domain '.254.169.in-addr.arpa' should also be resolved
|
158
|
+
# using mDNS.
|
159
|
+
#
|
160
|
+
# TODO - those are the IPs auto-allocated with ZeroConf. In my (common)
|
161
|
+
# situation, I have a net of OS X machines behind and ADSL firewall box,
|
162
|
+
# and all IPs were allocated in 192.168.123.*. I can do mDNS queries to
|
163
|
+
# get these addrs, but I can't do an mDNS query to reverse lookup the
|
164
|
+
# addrs. There are security reasons to not allow all addrs to be reversed
|
165
|
+
# on the local network, but maybe it wouldn't be so bad if MDNS was after
|
166
|
+
# DNS, so it only did it for addrs that were unmatched by DNS?
|
167
|
+
#
|
168
|
+
# Or perhaps IP addrs in the netmask of the ifx should be considered local,
|
169
|
+
# and mDNS allowed on them?
|
170
|
+
#
|
171
|
+
# If the search domains includes .local, we can add .local to it only if
|
172
|
+
# it has no dots and wasn't absolute.
|
173
|
+
lazy_initialize
|
174
|
+
dotlocal = DNS::Name.create('local')
|
175
|
+
search_dotlocal = @config.search.map.include?( dotlocal.to_a )
|
176
|
+
name = DNS::Name.create(name)
|
177
|
+
if name.absolute?
|
178
|
+
name = name
|
179
|
+
elsif name.length == 1 && search_dotlocal
|
180
|
+
name = name + dotlocal
|
181
|
+
elsif name.length > 1
|
182
|
+
name = name
|
183
|
+
else
|
184
|
+
name = nil
|
185
|
+
end
|
186
|
+
if name.subdomain_of?('local') || name.subdomain_of?('254.169.in-addr.arpa')
|
187
|
+
name.absolute = true
|
188
|
+
name
|
189
|
+
else
|
190
|
+
nil
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# See Resolv::DNS#eachresource.
|
195
|
+
def each_resource(name, typeclass)
|
196
|
+
name = generate_candidates(name)
|
197
|
+
|
198
|
+
query = Net::DNS::MDNS::Query.new(name, typeclass)
|
199
|
+
|
200
|
+
begin
|
201
|
+
# We want all the answers we can get, within the timeout period.
|
202
|
+
begin
|
203
|
+
timeout(DefaultTimeout) do
|
204
|
+
query.each do |answers|
|
205
|
+
answers.each do |an|
|
206
|
+
yield an.data
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
rescue TimeoutError
|
211
|
+
end
|
212
|
+
ensure
|
213
|
+
query.stop
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
Default = Resolv::MDNS.new
|
218
|
+
|
219
|
+
# Return the default MDNS Resolver. This is what is used when
|
220
|
+
# Resolv.getaddress and friends are called. Use it unless you need to
|
221
|
+
# specify config_info to Resolv::MDNS.new.
|
222
|
+
def self.default
|
223
|
+
Default
|
224
|
+
end
|
225
|
+
|
226
|
+
end
|
227
|
+
|
228
|
+
DefaultResolver.resolvers.push( Resolv::MDNS.default )
|
229
|
+
end
|
230
|
+
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# net/dns/resolv-replace.rb is a copy of resolv-replace.rb from the ruby
|
2
|
+
# library, where it is maintained by Tanaka Akira. See net/dns/resolv.rb for
|
3
|
+
# more information.
|
4
|
+
require 'socket'
|
5
|
+
require 'net/dns/resolv'
|
6
|
+
|
7
|
+
class << IPSocket
|
8
|
+
alias original_resolv_getaddress getaddress
|
9
|
+
def getaddress(host)
|
10
|
+
return original_resolv_getaddress(host) if Fixnum === host
|
11
|
+
begin
|
12
|
+
return Resolv.getaddress(host).to_s
|
13
|
+
rescue Resolv::ResolvError
|
14
|
+
raise SocketError, "Hostname not known: #{host}"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
class TCPSocket
|
20
|
+
alias original_resolv_initialize initialize
|
21
|
+
def initialize(host, serv, *rest)
|
22
|
+
rest[0] = IPSocket.getaddress(rest[0]) unless rest.empty?
|
23
|
+
original_resolv_initialize(IPSocket.getaddress(host), serv, *rest)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class UDPSocket
|
28
|
+
alias original_resolv_bind bind
|
29
|
+
def bind(host, port)
|
30
|
+
original_resolv_bind(IPSocket.getaddress(host), port)
|
31
|
+
end
|
32
|
+
|
33
|
+
alias original_resolv_connect connect
|
34
|
+
def connect(host, port)
|
35
|
+
original_resolv_connect(IPSocket.getaddress(host), port)
|
36
|
+
end
|
37
|
+
|
38
|
+
alias original_resolv_send send
|
39
|
+
def send(mesg, flags, *rest)
|
40
|
+
if rest.length == 2
|
41
|
+
host, port = rest
|
42
|
+
begin
|
43
|
+
addrs = Resolv.getaddresses(host)
|
44
|
+
rescue Resolv::ResolvError
|
45
|
+
raise SocketError, "Hostname not known: #{host}"
|
46
|
+
end
|
47
|
+
err = nil
|
48
|
+
addrs[0...-1].each {|addr|
|
49
|
+
begin
|
50
|
+
return original_resolv_send(mesg, flags, addr, port)
|
51
|
+
rescue SystemCallError
|
52
|
+
end
|
53
|
+
}
|
54
|
+
original_resolv_send(mesg, flags, addrs[-1], port)
|
55
|
+
else
|
56
|
+
original_resolv_send(mesg, flags, *rest)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class SOCKSSocket
|
62
|
+
alias original_resolv_initialize initialize
|
63
|
+
def initialize(host, serv)
|
64
|
+
original_resolv_initialize(IPSocket.getaddress(host), port)
|
65
|
+
end
|
66
|
+
end if defined? SOCKSSocket
|
@@ -0,0 +1,2012 @@
|
|
1
|
+
# net/dns/resolv.rb is a copy of resolv.rb from the ruby library, where it is
|
2
|
+
# maintained by Tanaka Akira.
|
3
|
+
#
|
4
|
+
# It contains modifications I found necessary, some of which have been accepted
|
5
|
+
# into ruby 1.8's cvs, and others that I hope will be accepted.
|
6
|
+
#
|
7
|
+
# net/dns/resolvx.rb contains extensions to resolv.rb (as opposed to modifications),
|
8
|
+
# some of these may also be worth accepting into the standard library.
|
9
|
+
#
|
10
|
+
# Note that until net/dns/resolv.rb is accepted AND released in ruby 1.8.x's
|
11
|
+
# resolv.rb I still need a copy in net-mdns. Without it, it would be necessary
|
12
|
+
# to install ruby from CVS in order to use net-mdns.
|
13
|
+
#
|
14
|
+
# = Bug fixes/Required changes
|
15
|
+
# - resolv-replace.rb: IPSocket#getaddress fails when passed a Fixnum, such as when
|
16
|
+
# calling UDPSocket#bind(Socket:INADDR_ANY, 5353)
|
17
|
+
# - MessageEncoder#put_string: would silently create a garbage record if string was
|
18
|
+
# longer than 255 characters.
|
19
|
+
# - TXT.new: correctly deal with TXT records longer than 255 characters.
|
20
|
+
# - TXT#data: correctly concatenate strings into a TXT record longer than 255 characters.
|
21
|
+
# - Message#encode/Message#decode: question and answer arrays now contain the
|
22
|
+
# mDNS unicast and cacheflush bit, respectively. All APIs, including
|
23
|
+
# #each_question and #each_answer, are backwards compatible.
|
24
|
+
# - A.new(A#address) failed because IPv4.create() wouldn't accept an address in the
|
25
|
+
# form of A#address (4 bytes in network byte order).
|
26
|
+
#
|
27
|
+
# = Ease-of-use changes
|
28
|
+
#
|
29
|
+
# - partial rdocifying
|
30
|
+
# - Str#inspect: difficult to notice whitespace at beginning of string, added quotes.
|
31
|
+
# - Name#==: allow arg to be String or Name, as does Name#create.
|
32
|
+
# - Name#subdomain_of?: allow arg to be String or Name, as does Name#create.
|
33
|
+
# - Name#subdomain_of?: disregard absolute, it doesn't make sense that:
|
34
|
+
# www.example.com subdomain_of? www.example.com. => false
|
35
|
+
# www.example.com subdomain_of? www.example.com => true
|
36
|
+
# If you can't compare a variable thing to a known thing.. how can you compare a
|
37
|
+
# variable thing to a variable thing?
|
38
|
+
#
|
39
|
+
# I had a lot of bugs using Name comparison related to trailing dots. Name#==
|
40
|
+
# is almost impossible to use correctly when comparing against an other which
|
41
|
+
# is a Name or a String, and may have come from a DNS Message (in which case it
|
42
|
+
# will be absolute), or from input from a user, in which case they probably did
|
43
|
+
# not type the trailing dot.
|
44
|
+
|
45
|
+
=begin
|
46
|
+
= resolv library
|
47
|
+
resolv.rb is a resolver library written in Ruby.
|
48
|
+
Since it is written in Ruby, it is thread-aware.
|
49
|
+
I.e. it can resolv many hostnames concurrently.
|
50
|
+
|
51
|
+
It is possible to lookup various resources of DNS using DNS module directly.
|
52
|
+
|
53
|
+
== example
|
54
|
+
p Resolv.getaddress("www.ruby-lang.org")
|
55
|
+
p Resolv.getname("210.251.121.214")
|
56
|
+
|
57
|
+
Resolv::DNS.open {|dns|
|
58
|
+
p dns.getresources("www.ruby-lang.org", Resolv::DNS::Resource::IN::A).collect {|r| r.address}
|
59
|
+
p dns.getresources("ruby-lang.org", Resolv::DNS::Resource::IN::MX).collect {|r| [r.exchange.to_s, r.preference]}
|
60
|
+
}
|
61
|
+
|
62
|
+
== Resolv class
|
63
|
+
|
64
|
+
=== class methods
|
65
|
+
--- Resolv.getaddress(name)
|
66
|
+
--- Resolv.getaddresses(name)
|
67
|
+
--- Resolv.each_address(name) {|address| ...}
|
68
|
+
They lookups IP addresses of ((|name|)) which represents a hostname
|
69
|
+
as a string by default resolver.
|
70
|
+
|
71
|
+
getaddress returns first entry of lookupped addresses.
|
72
|
+
getaddresses returns lookupped addresses as an array.
|
73
|
+
each_address iterates over lookupped addresses.
|
74
|
+
|
75
|
+
--- Resolv.getname(address)
|
76
|
+
--- Resolv.getnames(address)
|
77
|
+
--- Resolv.each_name(address) {|name| ...}
|
78
|
+
lookups hostnames of ((|address|)) which represents IP address as a string.
|
79
|
+
|
80
|
+
getname returns first entry of lookupped names.
|
81
|
+
getnames returns lookupped names as an array.
|
82
|
+
each_names iterates over lookupped names.
|
83
|
+
|
84
|
+
== Resolv::Hosts class
|
85
|
+
hostname resolver using /etc/hosts format.
|
86
|
+
|
87
|
+
=== class methods
|
88
|
+
--- Resolv::Hosts.new(hosts='/etc/hosts')
|
89
|
+
|
90
|
+
=== methods
|
91
|
+
--- Resolv::Hosts#getaddress(name)
|
92
|
+
--- Resolv::Hosts#getaddresses(name)
|
93
|
+
--- Resolv::Hosts#each_address(name) {|address| ...}
|
94
|
+
address lookup methods.
|
95
|
+
|
96
|
+
--- Resolv::Hosts#getname(address)
|
97
|
+
--- Resolv::Hosts#getnames(address)
|
98
|
+
--- Resolv::Hosts#each_name(address) {|name| ...}
|
99
|
+
hostnames lookup methods.
|
100
|
+
|
101
|
+
== Resolv::DNS class
|
102
|
+
DNS stub resolver.
|
103
|
+
|
104
|
+
=== class methods
|
105
|
+
--- Resolv::DNS.new(config_info=nil)
|
106
|
+
|
107
|
+
((|config_info|)) should be nil, a string or a hash.
|
108
|
+
If nil is given, /etc/resolv.conf and platform specific information is used.
|
109
|
+
If a string is given, it should be a filename which format is same as /etc/resolv.conf.
|
110
|
+
If a hash is given, it may contains information for nameserver, search and ndots as follows.
|
111
|
+
|
112
|
+
Resolv::DNS.new({:nameserver=>["210.251.121.21"], :search=>["ruby-lang.org"], :ndots=>1})
|
113
|
+
|
114
|
+
--- Resolv::DNS.open(config_info=nil)
|
115
|
+
--- Resolv::DNS.open(config_info=nil) {|dns| ...}
|
116
|
+
|
117
|
+
=== methods
|
118
|
+
--- Resolv::DNS#close
|
119
|
+
|
120
|
+
--- Resolv::DNS#getaddress(name)
|
121
|
+
--- Resolv::DNS#getaddresses(name)
|
122
|
+
--- Resolv::DNS#each_address(name) {|address| ...}
|
123
|
+
address lookup methods.
|
124
|
+
|
125
|
+
((|name|)) must be a instance of Resolv::DNS::Name or String. Lookupped
|
126
|
+
address is represented as an instance of Resolv::IPv4 or Resolv::IPv6.
|
127
|
+
|
128
|
+
--- Resolv::DNS#getname(address)
|
129
|
+
--- Resolv::DNS#getnames(address)
|
130
|
+
--- Resolv::DNS#each_name(address) {|name| ...}
|
131
|
+
hostnames lookup methods.
|
132
|
+
|
133
|
+
((|address|)) must be a instance of Resolv::IPv4, Resolv::IPv6 or String.
|
134
|
+
Lookupped name is represented as an instance of Resolv::DNS::Name.
|
135
|
+
|
136
|
+
--- Resolv::DNS#getresource(name, typeclass)
|
137
|
+
--- Resolv::DNS#getresources(name, typeclass)
|
138
|
+
--- Resolv::DNS#each_resource(name, typeclass) {|resource| ...}
|
139
|
+
They lookup DNS resources of ((|name|)).
|
140
|
+
((|name|)) must be a instance of Resolv::Name or String.
|
141
|
+
|
142
|
+
((|typeclass|)) should be one of follows:
|
143
|
+
* Resolv::DNS::Resource::IN::ANY
|
144
|
+
* Resolv::DNS::Resource::IN::NS
|
145
|
+
* Resolv::DNS::Resource::IN::CNAME
|
146
|
+
* Resolv::DNS::Resource::IN::SOA
|
147
|
+
* Resolv::DNS::Resource::IN::HINFO
|
148
|
+
* Resolv::DNS::Resource::IN::MINFO
|
149
|
+
* Resolv::DNS::Resource::IN::MX
|
150
|
+
* Resolv::DNS::Resource::IN::TXT
|
151
|
+
* Resolv::DNS::Resource::IN::ANY
|
152
|
+
* Resolv::DNS::Resource::IN::A
|
153
|
+
* Resolv::DNS::Resource::IN::WKS
|
154
|
+
* Resolv::DNS::Resource::IN::PTR
|
155
|
+
* Resolv::DNS::Resource::IN::SRV
|
156
|
+
* Resolv::DNS::Resource::IN::AAAA
|
157
|
+
|
158
|
+
Lookupped resource is represented as an instance of (a subclass of)
|
159
|
+
Resolv::DNS::Resource.
|
160
|
+
(Resolv::DNS::Resource::IN::A, etc.)
|
161
|
+
|
162
|
+
== Resolv::DNS::Resource::IN::NS class
|
163
|
+
--- name
|
164
|
+
== Resolv::DNS::Resource::IN::CNAME class
|
165
|
+
--- name
|
166
|
+
== Resolv::DNS::Resource::IN::SOA class
|
167
|
+
--- mname
|
168
|
+
--- rname
|
169
|
+
--- serial
|
170
|
+
--- refresh
|
171
|
+
--- retry
|
172
|
+
--- expire
|
173
|
+
--- minimum
|
174
|
+
== Resolv::DNS::Resource::IN::HINFO class
|
175
|
+
--- cpu
|
176
|
+
--- os
|
177
|
+
== Resolv::DNS::Resource::IN::MINFO class
|
178
|
+
--- rmailbx
|
179
|
+
--- emailbx
|
180
|
+
== Resolv::DNS::Resource::IN::MX class
|
181
|
+
--- preference
|
182
|
+
--- exchange
|
183
|
+
== Resolv::DNS::Resource::IN::TXT class
|
184
|
+
--- data
|
185
|
+
== Resolv::DNS::Resource::IN::A class
|
186
|
+
--- address
|
187
|
+
== Resolv::DNS::Resource::IN::WKS class
|
188
|
+
--- address
|
189
|
+
--- protocol
|
190
|
+
--- bitmap
|
191
|
+
== Resolv::DNS::Resource::IN::PTR class
|
192
|
+
--- name
|
193
|
+
== Resolv::DNS::Resource::IN::AAAA class
|
194
|
+
--- address
|
195
|
+
|
196
|
+
== Resolv::DNS::Name class
|
197
|
+
|
198
|
+
=== class methods
|
199
|
+
--- Resolv::DNS::Name.create(name)
|
200
|
+
|
201
|
+
=== methods
|
202
|
+
--- Resolv::DNS::Name#to_s
|
203
|
+
|
204
|
+
== Resolv::DNS::Resource class
|
205
|
+
|
206
|
+
== Resolv::IPv4 class
|
207
|
+
=== class methods
|
208
|
+
--- Resolv::IPv4.create(address)
|
209
|
+
|
210
|
+
=== methods
|
211
|
+
--- Resolv::IPv4#to_s
|
212
|
+
--- Resolv::IPv4#to_name
|
213
|
+
|
214
|
+
=== constants
|
215
|
+
--- Resolv::IPv4::Regex
|
216
|
+
regular expression for IPv4 address.
|
217
|
+
|
218
|
+
== Resolv::IPv6 class
|
219
|
+
=== class methods
|
220
|
+
--- Resolv::IPv6.create(address)
|
221
|
+
|
222
|
+
=== methods
|
223
|
+
--- Resolv::IPv6#to_s
|
224
|
+
--- Resolv::IPv6#to_name
|
225
|
+
|
226
|
+
=== constants
|
227
|
+
--- Resolv::IPv6::Regex
|
228
|
+
regular expression for IPv6 address.
|
229
|
+
|
230
|
+
== Bugs
|
231
|
+
* NIS is not supported.
|
232
|
+
* /etc/nsswitch.conf is not supported.
|
233
|
+
* IPv6 is not supported.
|
234
|
+
|
235
|
+
=end
|
236
|
+
|
237
|
+
require 'socket'
|
238
|
+
require 'fcntl'
|
239
|
+
require 'timeout'
|
240
|
+
require 'thread'
|
241
|
+
|
242
|
+
class Resolv
|
243
|
+
def self.getaddress(name)
|
244
|
+
DefaultResolver.getaddress(name)
|
245
|
+
end
|
246
|
+
|
247
|
+
def self.getaddresses(name)
|
248
|
+
DefaultResolver.getaddresses(name)
|
249
|
+
end
|
250
|
+
|
251
|
+
def self.each_address(name, &block)
|
252
|
+
DefaultResolver.each_address(name, &block)
|
253
|
+
end
|
254
|
+
|
255
|
+
def self.getname(address)
|
256
|
+
DefaultResolver.getname(address)
|
257
|
+
end
|
258
|
+
|
259
|
+
def self.getnames(address)
|
260
|
+
DefaultResolver.getnames(address)
|
261
|
+
end
|
262
|
+
|
263
|
+
def self.each_name(address, &proc)
|
264
|
+
DefaultResolver.each_name(address, &proc)
|
265
|
+
end
|
266
|
+
|
267
|
+
def initialize(resolvers=[Hosts.new, DNS.new])
|
268
|
+
@resolvers = resolvers
|
269
|
+
end
|
270
|
+
|
271
|
+
def getaddress(name)
|
272
|
+
each_address(name) {|address| return address}
|
273
|
+
raise ResolvError.new("no address for #{name}")
|
274
|
+
end
|
275
|
+
|
276
|
+
def getaddresses(name)
|
277
|
+
ret = []
|
278
|
+
each_address(name) {|address| ret << address}
|
279
|
+
return ret
|
280
|
+
end
|
281
|
+
|
282
|
+
def each_address(name)
|
283
|
+
if AddressRegex =~ name
|
284
|
+
yield name
|
285
|
+
return
|
286
|
+
end
|
287
|
+
yielded = false
|
288
|
+
@resolvers.each {|r|
|
289
|
+
r.each_address(name) {|address|
|
290
|
+
yield address.to_s
|
291
|
+
yielded = true
|
292
|
+
}
|
293
|
+
return if yielded
|
294
|
+
}
|
295
|
+
end
|
296
|
+
|
297
|
+
def getname(address)
|
298
|
+
each_name(address) {|name| return name}
|
299
|
+
raise ResolvError.new("no name for #{address}")
|
300
|
+
end
|
301
|
+
|
302
|
+
def getnames(address)
|
303
|
+
ret = []
|
304
|
+
each_name(address) {|name| ret << name}
|
305
|
+
return ret
|
306
|
+
end
|
307
|
+
|
308
|
+
def each_name(address)
|
309
|
+
yielded = false
|
310
|
+
@resolvers.each {|r|
|
311
|
+
r.each_name(address) {|name|
|
312
|
+
yield name.to_s
|
313
|
+
yielded = true
|
314
|
+
}
|
315
|
+
return if yielded
|
316
|
+
}
|
317
|
+
end
|
318
|
+
|
319
|
+
class ResolvError < StandardError
|
320
|
+
end
|
321
|
+
|
322
|
+
class ResolvTimeout < TimeoutError
|
323
|
+
end
|
324
|
+
|
325
|
+
# Resolves names and addresses using the hosts file, "/etc/hosts" or
|
326
|
+
# whatever it is on Windows.
|
327
|
+
class Hosts
|
328
|
+
if /mswin32|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM
|
329
|
+
require 'win32/resolv'
|
330
|
+
DefaultFileName = Win32::Resolv.get_hosts_path
|
331
|
+
else
|
332
|
+
DefaultFileName = '/etc/hosts'
|
333
|
+
end
|
334
|
+
|
335
|
+
def initialize(filename = DefaultFileName)
|
336
|
+
@filename = filename
|
337
|
+
@mutex = Mutex.new
|
338
|
+
@initialized = nil
|
339
|
+
end
|
340
|
+
|
341
|
+
def lazy_initialize # :nodoc:
|
342
|
+
@mutex.synchronize {
|
343
|
+
unless @initialized
|
344
|
+
@name2addr = {}
|
345
|
+
@addr2name = {}
|
346
|
+
open(@filename) {|f|
|
347
|
+
f.each {|line|
|
348
|
+
line.sub!(/#.*/, '')
|
349
|
+
addr, hostname, *aliases = line.split(/\s+/)
|
350
|
+
next unless addr
|
351
|
+
addr.untaint
|
352
|
+
hostname.untaint
|
353
|
+
@addr2name[addr] = [] unless @addr2name.include? addr
|
354
|
+
@addr2name[addr] << hostname
|
355
|
+
@addr2name[addr] += aliases
|
356
|
+
@name2addr[hostname] = [] unless @name2addr.include? hostname
|
357
|
+
@name2addr[hostname] << addr
|
358
|
+
aliases.each {|n|
|
359
|
+
n.untaint
|
360
|
+
@name2addr[n] = [] unless @name2addr.include? n
|
361
|
+
@name2addr[n] << addr
|
362
|
+
}
|
363
|
+
}
|
364
|
+
}
|
365
|
+
@name2addr.each {|name, arr| arr.reverse!}
|
366
|
+
@initialized = true
|
367
|
+
end
|
368
|
+
}
|
369
|
+
self
|
370
|
+
end
|
371
|
+
|
372
|
+
def getaddress(name)
|
373
|
+
each_address(name) {|address| return address}
|
374
|
+
raise ResolvError.new("#{@filename} has no name: #{name}")
|
375
|
+
end
|
376
|
+
|
377
|
+
def getaddresses(name)
|
378
|
+
ret = []
|
379
|
+
each_address(name) {|address| ret << address}
|
380
|
+
return ret
|
381
|
+
end
|
382
|
+
|
383
|
+
def each_address(name, &proc)
|
384
|
+
lazy_initialize
|
385
|
+
if @name2addr.include?(name)
|
386
|
+
@name2addr[name].each(&proc)
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
def getname(address)
|
391
|
+
each_name(address) {|name| return name}
|
392
|
+
raise ResolvError.new("#{@filename} has no address: #{address}")
|
393
|
+
end
|
394
|
+
|
395
|
+
def getnames(address)
|
396
|
+
ret = []
|
397
|
+
each_name(address) {|name| ret << name}
|
398
|
+
return ret
|
399
|
+
end
|
400
|
+
|
401
|
+
def each_name(address, &proc)
|
402
|
+
lazy_initialize
|
403
|
+
if @addr2name.include?(address)
|
404
|
+
@addr2name[address].each(&proc)
|
405
|
+
end
|
406
|
+
end
|
407
|
+
end
|
408
|
+
|
409
|
+
class DNS
|
410
|
+
# STD0013 (RFC 1035, etc.)
|
411
|
+
# ftp://ftp.isi.edu/in-notes/iana/assignments/dns-parameters
|
412
|
+
|
413
|
+
Port = 53
|
414
|
+
UDPSize = 512
|
415
|
+
|
416
|
+
DNSThreadGroup = ThreadGroup.new # :nodoc:
|
417
|
+
|
418
|
+
def self.open(*args)
|
419
|
+
dns = new(*args)
|
420
|
+
return dns unless block_given?
|
421
|
+
begin
|
422
|
+
yield dns
|
423
|
+
ensure
|
424
|
+
dns.close
|
425
|
+
end
|
426
|
+
end
|
427
|
+
|
428
|
+
def initialize(config_info=nil)
|
429
|
+
@mutex = Mutex.new
|
430
|
+
@config = Config.new(config_info)
|
431
|
+
@initialized = nil
|
432
|
+
end
|
433
|
+
|
434
|
+
def lazy_initialize # :nodoc:
|
435
|
+
@mutex.synchronize {
|
436
|
+
unless @initialized
|
437
|
+
@config.lazy_initialize
|
438
|
+
|
439
|
+
if nameserver = @config.single?
|
440
|
+
@requester = Requester::ConnectedUDP.new(nameserver)
|
441
|
+
else
|
442
|
+
@requester = Requester::UnconnectedUDP.new
|
443
|
+
end
|
444
|
+
|
445
|
+
@initialized = true
|
446
|
+
end
|
447
|
+
}
|
448
|
+
self
|
449
|
+
end
|
450
|
+
|
451
|
+
def close
|
452
|
+
@mutex.synchronize {
|
453
|
+
if @initialized
|
454
|
+
@requester.close if @requester
|
455
|
+
@requester = nil
|
456
|
+
@initialized = false
|
457
|
+
end
|
458
|
+
}
|
459
|
+
end
|
460
|
+
|
461
|
+
def getaddress(name)
|
462
|
+
each_address(name) {|address| return address}
|
463
|
+
raise ResolvError.new("DNS result has no information for #{name}")
|
464
|
+
end
|
465
|
+
|
466
|
+
def getaddresses(name)
|
467
|
+
ret = []
|
468
|
+
each_address(name) {|address| ret << address}
|
469
|
+
return ret
|
470
|
+
end
|
471
|
+
|
472
|
+
def each_address(name)
|
473
|
+
each_resource(name, Resource::IN::A) {|resource| yield resource.address}
|
474
|
+
end
|
475
|
+
|
476
|
+
def getname(address)
|
477
|
+
each_name(address) {|name| return name}
|
478
|
+
raise ResolvError.new("DNS result has no information for #{address}")
|
479
|
+
end
|
480
|
+
|
481
|
+
def getnames(address)
|
482
|
+
ret = []
|
483
|
+
each_name(address) {|name| ret << name}
|
484
|
+
return ret
|
485
|
+
end
|
486
|
+
|
487
|
+
def each_name(address)
|
488
|
+
case address
|
489
|
+
when Name
|
490
|
+
ptr = address
|
491
|
+
when IPv4::Regex
|
492
|
+
ptr = IPv4.create(address).to_name
|
493
|
+
when IPv6::Regex
|
494
|
+
ptr = IPv6.create(address).to_name
|
495
|
+
else
|
496
|
+
raise ResolvError.new("cannot interpret as address: #{address}")
|
497
|
+
end
|
498
|
+
each_resource(ptr, Resource::IN::PTR) {|resource| yield resource.name}
|
499
|
+
end
|
500
|
+
|
501
|
+
def getresource(name, typeclass)
|
502
|
+
each_resource(name, typeclass) {|resource| return resource}
|
503
|
+
raise ResolvError.new("DNS result has no information for #{name}")
|
504
|
+
end
|
505
|
+
|
506
|
+
def getresources(name, typeclass)
|
507
|
+
ret = []
|
508
|
+
each_resource(name, typeclass) {|resource| ret << resource}
|
509
|
+
return ret
|
510
|
+
end
|
511
|
+
|
512
|
+
def each_resource(name, typeclass, &proc)
|
513
|
+
lazy_initialize
|
514
|
+
q = Queue.new
|
515
|
+
senders = {}
|
516
|
+
begin
|
517
|
+
@config.resolv(name) {|candidate, tout, nameserver|
|
518
|
+
msg = Message.new
|
519
|
+
msg.rd = 1
|
520
|
+
msg.add_question(candidate, typeclass)
|
521
|
+
unless sender = senders[[candidate, nameserver]]
|
522
|
+
sender = senders[[candidate, nameserver]] =
|
523
|
+
@requester.sender(msg, candidate, q, nameserver)
|
524
|
+
end
|
525
|
+
sender.send
|
526
|
+
reply = reply_name = nil
|
527
|
+
timeout(tout, ResolvTimeout) { reply, reply_name = q.pop }
|
528
|
+
case reply.rcode
|
529
|
+
when RCode::NoError
|
530
|
+
extract_resources(reply, reply_name, typeclass, &proc)
|
531
|
+
return
|
532
|
+
when RCode::NXDomain
|
533
|
+
raise Config::NXDomain.new(reply_name.to_s)
|
534
|
+
else
|
535
|
+
raise Config::OtherResolvError.new(reply_name.to_s)
|
536
|
+
end
|
537
|
+
}
|
538
|
+
ensure
|
539
|
+
@requester.delete(q)
|
540
|
+
end
|
541
|
+
end
|
542
|
+
|
543
|
+
def extract_resources(msg, name, typeclass) # :nodoc:
|
544
|
+
if typeclass < Resource::ANY
|
545
|
+
n0 = Name.create(name)
|
546
|
+
msg.each_answer {|n, ttl, data|
|
547
|
+
yield data if n0 == n
|
548
|
+
}
|
549
|
+
end
|
550
|
+
yielded = false
|
551
|
+
n0 = Name.create(name)
|
552
|
+
msg.each_answer {|n, ttl, data|
|
553
|
+
if n0 == n
|
554
|
+
case data
|
555
|
+
when typeclass
|
556
|
+
yield data
|
557
|
+
yielded = true
|
558
|
+
when Resource::CNAME
|
559
|
+
n0 = data.name
|
560
|
+
end
|
561
|
+
end
|
562
|
+
}
|
563
|
+
return if yielded
|
564
|
+
msg.each_answer {|n, ttl, data|
|
565
|
+
if n0 == n
|
566
|
+
case data
|
567
|
+
when typeclass
|
568
|
+
yield data
|
569
|
+
end
|
570
|
+
end
|
571
|
+
}
|
572
|
+
end
|
573
|
+
|
574
|
+
class Requester # :nodoc:
|
575
|
+
def initialize
|
576
|
+
@senders = {}
|
577
|
+
end
|
578
|
+
|
579
|
+
def close
|
580
|
+
thread, sock, @thread, @sock = @thread, @sock
|
581
|
+
begin
|
582
|
+
if thread
|
583
|
+
thread.kill
|
584
|
+
thread.join
|
585
|
+
end
|
586
|
+
ensure
|
587
|
+
sock.close if sock
|
588
|
+
end
|
589
|
+
end
|
590
|
+
|
591
|
+
def delete(arg)
|
592
|
+
case arg
|
593
|
+
when Sender
|
594
|
+
@senders.delete_if {|k, s| s == arg }
|
595
|
+
when Queue
|
596
|
+
@senders.delete_if {|k, s| s.queue == arg }
|
597
|
+
else
|
598
|
+
raise ArgumentError.new("neither Sender or Queue: #{arg}")
|
599
|
+
end
|
600
|
+
end
|
601
|
+
|
602
|
+
class Sender # :nodoc:
|
603
|
+
def initialize(msg, data, sock, queue)
|
604
|
+
@msg = msg
|
605
|
+
@data = data
|
606
|
+
@sock = sock
|
607
|
+
@queue = queue
|
608
|
+
end
|
609
|
+
attr_reader :queue
|
610
|
+
|
611
|
+
def recv(msg)
|
612
|
+
@queue.push([msg, @data])
|
613
|
+
end
|
614
|
+
end
|
615
|
+
|
616
|
+
class UnconnectedUDP < Requester # :nodoc:
|
617
|
+
def initialize
|
618
|
+
super()
|
619
|
+
@sock = UDPSocket.new
|
620
|
+
@sock.fcntl(Fcntl::F_SETFD, 1) if defined? Fcntl::F_SETFD
|
621
|
+
@id = {}
|
622
|
+
@id.default = -1
|
623
|
+
@thread = Thread.new {
|
624
|
+
DNSThreadGroup.add Thread.current
|
625
|
+
loop {
|
626
|
+
reply, from = @sock.recvfrom(UDPSize)
|
627
|
+
msg = begin
|
628
|
+
Message.decode(reply)
|
629
|
+
rescue DecodeError
|
630
|
+
STDERR.print("DNS message decoding error: #{reply.inspect}\n")
|
631
|
+
next
|
632
|
+
end
|
633
|
+
if s = @senders[[[from[3],from[1]],msg.id]]
|
634
|
+
s.recv msg
|
635
|
+
else
|
636
|
+
#STDERR.print("non-handled DNS message: #{msg.inspect} from #{from.inspect}\n")
|
637
|
+
end
|
638
|
+
}
|
639
|
+
}
|
640
|
+
end
|
641
|
+
|
642
|
+
def sender(msg, data, queue, host, port=Port)
|
643
|
+
service = [host, port]
|
644
|
+
id = Thread.exclusive {
|
645
|
+
@id[service] = (@id[service] + 1) & 0xffff
|
646
|
+
}
|
647
|
+
request = msg.encode
|
648
|
+
request[0,2] = [id].pack('n')
|
649
|
+
return @senders[[service, id]] =
|
650
|
+
Sender.new(request, data, @sock, host, port, queue)
|
651
|
+
end
|
652
|
+
|
653
|
+
class Sender < Requester::Sender # :nodoc:
|
654
|
+
def initialize(msg, data, sock, host, port, queue)
|
655
|
+
super(msg, data, sock, queue)
|
656
|
+
@host = host
|
657
|
+
@port = port
|
658
|
+
end
|
659
|
+
|
660
|
+
def send
|
661
|
+
@sock.send(@msg, 0, @host, @port)
|
662
|
+
end
|
663
|
+
end
|
664
|
+
end
|
665
|
+
|
666
|
+
class ConnectedUDP < Requester # :nodoc:
|
667
|
+
def initialize(host, port=Port)
|
668
|
+
super()
|
669
|
+
@host = host
|
670
|
+
@port = port
|
671
|
+
@sock = UDPSocket.new
|
672
|
+
@sock.connect(host, port)
|
673
|
+
@sock.fcntl(Fcntl::F_SETFD, 1) if defined? Fcntl::F_SETFD
|
674
|
+
@id = -1
|
675
|
+
@thread = Thread.new {
|
676
|
+
DNSThreadGroup.add Thread.current
|
677
|
+
loop {
|
678
|
+
reply = @sock.recv(UDPSize)
|
679
|
+
msg = begin
|
680
|
+
Message.decode(reply)
|
681
|
+
rescue DecodeError
|
682
|
+
STDERR.print("DNS message decoding error: #{reply.inspect}")
|
683
|
+
next
|
684
|
+
end
|
685
|
+
if s = @senders[msg.id]
|
686
|
+
s.recv msg
|
687
|
+
else
|
688
|
+
#STDERR.print("non-handled DNS message: #{msg.inspect}")
|
689
|
+
end
|
690
|
+
}
|
691
|
+
}
|
692
|
+
end
|
693
|
+
|
694
|
+
def sender(msg, data, queue, host=@host, port=@port)
|
695
|
+
unless host == @host && port == @port
|
696
|
+
raise RequestError.new("host/port don't match: #{host}:#{port}")
|
697
|
+
end
|
698
|
+
id = Thread.exclusive { @id = (@id + 1) & 0xffff }
|
699
|
+
request = msg.encode
|
700
|
+
request[0,2] = [id].pack('n')
|
701
|
+
return @senders[id] = Sender.new(request, data, @sock, queue)
|
702
|
+
end
|
703
|
+
|
704
|
+
class Sender < Requester::Sender # :nodoc:
|
705
|
+
def send
|
706
|
+
@sock.send(@msg, 0)
|
707
|
+
end
|
708
|
+
end
|
709
|
+
end
|
710
|
+
|
711
|
+
class TCP < Requester # :nodoc:
|
712
|
+
def initialize(host, port=Port)
|
713
|
+
super()
|
714
|
+
@host = host
|
715
|
+
@port = port
|
716
|
+
@sock = TCPSocket.new
|
717
|
+
@sock.connect(host, port)
|
718
|
+
@sock.fcntl(Fcntl::F_SETFD, 1) if defined? Fcntl::F_SETFD
|
719
|
+
@id = -1
|
720
|
+
@senders = {}
|
721
|
+
@thread = Thread.new {
|
722
|
+
DNSThreadGroup.add Thread.current
|
723
|
+
loop {
|
724
|
+
len = @sock.read(2).unpack('n')
|
725
|
+
reply = @sock.read(len)
|
726
|
+
msg = begin
|
727
|
+
Message.decode(reply)
|
728
|
+
rescue DecodeError
|
729
|
+
STDERR.print("DNS message decoding error: #{reply.inspect}")
|
730
|
+
next
|
731
|
+
end
|
732
|
+
if s = @senders[msg.id]
|
733
|
+
s.push msg
|
734
|
+
else
|
735
|
+
#STDERR.print("non-handled DNS message: #{msg.inspect}")
|
736
|
+
end
|
737
|
+
}
|
738
|
+
}
|
739
|
+
end
|
740
|
+
|
741
|
+
def sender(msg, data, queue, host=@host, port=@port)
|
742
|
+
unless host == @host && port == @port
|
743
|
+
raise RequestError.new("host/port don't match: #{host}:#{port}")
|
744
|
+
end
|
745
|
+
id = Thread.exclusive { @id = (@id + 1) & 0xffff }
|
746
|
+
request = msg.encode
|
747
|
+
request[0,2] = [request.length, id].pack('nn')
|
748
|
+
return @senders[id] = Sender.new(request, data, @sock, queue)
|
749
|
+
end
|
750
|
+
|
751
|
+
class Sender < Requester::Sender # :nodoc:
|
752
|
+
def send
|
753
|
+
@sock.print(@msg)
|
754
|
+
@sock.flush
|
755
|
+
end
|
756
|
+
end
|
757
|
+
end
|
758
|
+
|
759
|
+
class RequestError < StandardError
|
760
|
+
end
|
761
|
+
end
|
762
|
+
|
763
|
+
# Encapsulates the resolver configuration information.
|
764
|
+
#
|
765
|
+
# +config_info+ can be nil, a String or a Hash:
|
766
|
+
# - nil is the default, configuration is read from /etc/resolv.conf, or
|
767
|
+
# from Win32::Resolv.get_resolv_info on Windows.
|
768
|
+
# - String is used as the name of the config file to parse instead of
|
769
|
+
# /etc/resolv.conf.
|
770
|
+
# - Hash must map the :nameserver and/or :search symbol keys to a single
|
771
|
+
# String or an array of String to use as the value of those config options.
|
772
|
+
class Config
|
773
|
+
def initialize(config_info=nil)
|
774
|
+
@mutex = Mutex.new
|
775
|
+
@config_info = config_info
|
776
|
+
@initialized = nil
|
777
|
+
end
|
778
|
+
|
779
|
+
def Config.parse_resolv_conf(filename) # :nodoc:
|
780
|
+
nameserver = []
|
781
|
+
search = nil
|
782
|
+
ndots = 1
|
783
|
+
open(filename) {|f|
|
784
|
+
f.each {|line|
|
785
|
+
line.sub!(/[#;].*/, '')
|
786
|
+
keyword, *args = line.split(/\s+/)
|
787
|
+
args.each { |arg|
|
788
|
+
arg.untaint
|
789
|
+
}
|
790
|
+
next unless keyword
|
791
|
+
case keyword
|
792
|
+
when 'nameserver'
|
793
|
+
nameserver += args
|
794
|
+
when 'domain'
|
795
|
+
next if args.empty?
|
796
|
+
search = [args[0]]
|
797
|
+
when 'search'
|
798
|
+
next if args.empty?
|
799
|
+
search = args
|
800
|
+
when 'options'
|
801
|
+
args.each {|arg|
|
802
|
+
case arg
|
803
|
+
when /\Andots:(\d+)\z/
|
804
|
+
ndots = $1.to_i
|
805
|
+
end
|
806
|
+
}
|
807
|
+
end
|
808
|
+
}
|
809
|
+
}
|
810
|
+
return { :nameserver => nameserver, :search => search, :ndots => ndots }
|
811
|
+
end
|
812
|
+
|
813
|
+
def Config.default_config_hash(filename="/etc/resolv.conf") # :nodoc:
|
814
|
+
if File.exist? filename
|
815
|
+
config_hash = Config.parse_resolv_conf(filename)
|
816
|
+
else
|
817
|
+
if /mswin32|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM
|
818
|
+
search, nameserver = Win32::Resolv.get_resolv_info
|
819
|
+
config_hash = {}
|
820
|
+
config_hash[:nameserver] = nameserver if nameserver
|
821
|
+
config_hash[:search] = [search].flatten if search
|
822
|
+
end
|
823
|
+
end
|
824
|
+
config_hash
|
825
|
+
end
|
826
|
+
|
827
|
+
def lazy_initialize # :nodoc:
|
828
|
+
@mutex.synchronize {
|
829
|
+
unless @initialized
|
830
|
+
@nameserver = []
|
831
|
+
@search = nil
|
832
|
+
@ndots = 1
|
833
|
+
case @config_info
|
834
|
+
when nil
|
835
|
+
config_hash = Config.default_config_hash
|
836
|
+
when String
|
837
|
+
config_hash = Config.parse_resolv_conf(@config_info)
|
838
|
+
when Hash
|
839
|
+
config_hash = @config_info.dup
|
840
|
+
if String === config_hash[:nameserver]
|
841
|
+
config_hash[:nameserver] = [config_hash[:nameserver]]
|
842
|
+
end
|
843
|
+
if String === config_hash[:search]
|
844
|
+
config_hash[:search] = [config_hash[:search]]
|
845
|
+
end
|
846
|
+
else
|
847
|
+
raise ArgumentError.new("invalid resolv configuration: #{@config_info.inspect}")
|
848
|
+
end
|
849
|
+
@nameserver = config_hash[:nameserver] if config_hash.include? :nameserver
|
850
|
+
@search = config_hash[:search] if config_hash.include? :search
|
851
|
+
@ndots = config_hash[:ndots] if config_hash.include? :ndots
|
852
|
+
|
853
|
+
@nameserver = ['0.0.0.0'] if @nameserver.empty?
|
854
|
+
if @search
|
855
|
+
@search = @search.map {|arg| Label.split(arg) }
|
856
|
+
else
|
857
|
+
hostname = Socket.gethostname
|
858
|
+
if /\./ =~ hostname
|
859
|
+
@search = [Label.split($')]
|
860
|
+
else
|
861
|
+
@search = [[]]
|
862
|
+
end
|
863
|
+
end
|
864
|
+
|
865
|
+
if !@nameserver.kind_of?(Array) ||
|
866
|
+
!@nameserver.all? {|ns| String === ns }
|
867
|
+
raise ArgumentError.new("invalid nameserver config: #{@nameserver.inspect}")
|
868
|
+
end
|
869
|
+
|
870
|
+
if !@search.kind_of?(Array) ||
|
871
|
+
!@search.all? {|ls| ls.all? {|l| Label::Str === l } }
|
872
|
+
raise ArgumentError.new("invalid search config: #{@search.inspect}")
|
873
|
+
end
|
874
|
+
|
875
|
+
if !@ndots.kind_of?(Integer)
|
876
|
+
raise ArgumentError.new("invalid ndots config: #{@ndots.inspect}")
|
877
|
+
end
|
878
|
+
|
879
|
+
@initialized = true
|
880
|
+
end
|
881
|
+
}
|
882
|
+
self
|
883
|
+
end
|
884
|
+
|
885
|
+
def single?
|
886
|
+
lazy_initialize
|
887
|
+
if @nameserver.length == 1
|
888
|
+
return @nameserver[0]
|
889
|
+
else
|
890
|
+
return nil
|
891
|
+
end
|
892
|
+
end
|
893
|
+
|
894
|
+
def generate_candidates(name) # :nodoc:
|
895
|
+
candidates = nil
|
896
|
+
name = Name.create(name)
|
897
|
+
if name.absolute?
|
898
|
+
candidates = [name]
|
899
|
+
else
|
900
|
+
if @ndots <= name.length - 1
|
901
|
+
candidates = [Name.new(name.to_a)]
|
902
|
+
else
|
903
|
+
candidates = []
|
904
|
+
end
|
905
|
+
candidates.concat(@search.map {|domain| Name.new(name.to_a + domain)})
|
906
|
+
end
|
907
|
+
return candidates
|
908
|
+
end
|
909
|
+
|
910
|
+
InitialTimeout = 5 # :nodoc:
|
911
|
+
|
912
|
+
def generate_timeouts # :nodoc:
|
913
|
+
ts = [InitialTimeout]
|
914
|
+
ts << ts[-1] * 2 / @nameserver.length
|
915
|
+
ts << ts[-1] * 2
|
916
|
+
ts << ts[-1] * 2
|
917
|
+
return ts
|
918
|
+
end
|
919
|
+
|
920
|
+
def resolv(name) # :nodoc:
|
921
|
+
candidates = generate_candidates(name)
|
922
|
+
timeouts = generate_timeouts
|
923
|
+
begin
|
924
|
+
candidates.each {|candidate|
|
925
|
+
begin
|
926
|
+
timeouts.each {|tout|
|
927
|
+
@nameserver.each {|nameserver|
|
928
|
+
begin
|
929
|
+
yield candidate, tout, nameserver
|
930
|
+
rescue ResolvTimeout
|
931
|
+
end
|
932
|
+
}
|
933
|
+
}
|
934
|
+
raise ResolvError.new("DNS resolv timeout: #{name}")
|
935
|
+
rescue NXDomain
|
936
|
+
end
|
937
|
+
}
|
938
|
+
rescue ResolvError
|
939
|
+
end
|
940
|
+
end
|
941
|
+
|
942
|
+
class NXDomain < ResolvError
|
943
|
+
end
|
944
|
+
|
945
|
+
class OtherResolvError < ResolvError
|
946
|
+
end
|
947
|
+
end
|
948
|
+
|
949
|
+
module OpCode # :nodoc:
|
950
|
+
Query = 0
|
951
|
+
IQuery = 1
|
952
|
+
Status = 2
|
953
|
+
Notify = 4
|
954
|
+
Update = 5
|
955
|
+
end
|
956
|
+
|
957
|
+
module RCode # :nodoc:
|
958
|
+
NoError = 0
|
959
|
+
FormErr = 1
|
960
|
+
ServFail = 2
|
961
|
+
NXDomain = 3
|
962
|
+
NotImp = 4
|
963
|
+
Refused = 5
|
964
|
+
YXDomain = 6
|
965
|
+
YXRRSet = 7
|
966
|
+
NXRRSet = 8
|
967
|
+
NotAuth = 9
|
968
|
+
NotZone = 10
|
969
|
+
BADVERS = 16
|
970
|
+
BADSIG = 16
|
971
|
+
BADKEY = 17
|
972
|
+
BADTIME = 18
|
973
|
+
BADMODE = 19
|
974
|
+
BADNAME = 20
|
975
|
+
BADALG = 21
|
976
|
+
end
|
977
|
+
|
978
|
+
class DecodeError < StandardError
|
979
|
+
end
|
980
|
+
|
981
|
+
class EncodeError < StandardError
|
982
|
+
end
|
983
|
+
|
984
|
+
module Label #:nodoc:
|
985
|
+
def self.split(arg)
|
986
|
+
labels = []
|
987
|
+
arg.scan(/[^\.]+/) {labels << Str.new($&)}
|
988
|
+
return labels
|
989
|
+
end
|
990
|
+
|
991
|
+
# A String wrapper that compares (and hashes) case insensitively, used to
|
992
|
+
# represent DNS labels.
|
993
|
+
class Str
|
994
|
+
def initialize(string) # :nodoc:
|
995
|
+
@string = string
|
996
|
+
@downcase = string.downcase
|
997
|
+
end
|
998
|
+
attr_reader :string, :downcase
|
999
|
+
|
1000
|
+
def to_s
|
1001
|
+
return @string
|
1002
|
+
end
|
1003
|
+
|
1004
|
+
def inspect
|
1005
|
+
return "#<#{self.class} #{self.to_s.inspect}>"
|
1006
|
+
end
|
1007
|
+
|
1008
|
+
def ==(other)
|
1009
|
+
return @downcase == other.downcase
|
1010
|
+
end
|
1011
|
+
|
1012
|
+
def eql?(other)
|
1013
|
+
return self == other
|
1014
|
+
end
|
1015
|
+
|
1016
|
+
def hash
|
1017
|
+
return @downcase.hash
|
1018
|
+
end
|
1019
|
+
end
|
1020
|
+
end
|
1021
|
+
|
1022
|
+
# A DNS name is a sequence of labels seperated by (or followed by) a dot
|
1023
|
+
# (".").
|
1024
|
+
#
|
1025
|
+
# The labels are defined to be case-insensitive ("example.COM" is the same
|
1026
|
+
# as "EXAMPLE.com").
|
1027
|
+
#
|
1028
|
+
# Names created from DNS messages names are always absolute. Names created
|
1029
|
+
# from a String are absolute only if they have a trailing dot.
|
1030
|
+
class Name
|
1031
|
+
# Create a Name from a Name (in which case +arg+ is returned
|
1032
|
+
# directly), or from a String (in which case new Name is returned).
|
1033
|
+
def self.create(arg)
|
1034
|
+
case arg
|
1035
|
+
when Name
|
1036
|
+
return arg
|
1037
|
+
when String
|
1038
|
+
return Name.new(Label.split(arg), /\.\z/ =~ arg ? true : false)
|
1039
|
+
else
|
1040
|
+
raise ArgumentError.new("cannot interpret as DNS name: #{arg.inspect}")
|
1041
|
+
end
|
1042
|
+
end
|
1043
|
+
|
1044
|
+
def initialize(labels, absolute=true) # :nodoc:
|
1045
|
+
@labels = labels
|
1046
|
+
@absolute = absolute
|
1047
|
+
end
|
1048
|
+
|
1049
|
+
def inspect
|
1050
|
+
"#<#{self.class}: #{self.to_s}#{@absolute ? '.' : ''}>"
|
1051
|
+
end
|
1052
|
+
|
1053
|
+
# Tests if +self+ is absolute, i.e., had a trailing dot.
|
1054
|
+
# For example:
|
1055
|
+
# example.com.
|
1056
|
+
# is absolute, whereas:
|
1057
|
+
# example.com
|
1058
|
+
# is not. Absolute names will never have the default search domains
|
1059
|
+
# added to them during resolution.
|
1060
|
+
def absolute?
|
1061
|
+
return @absolute
|
1062
|
+
end
|
1063
|
+
|
1064
|
+
# Tests equivalence to +other+, a Name or a String.
|
1065
|
+
#
|
1066
|
+
# Names are equivalent if their labels are equal (comparison is
|
1067
|
+
# case-insensitive) and their "absoluteness" is equal.
|
1068
|
+
#
|
1069
|
+
# p Name.create("example.COM") == "EXAMPLE.com" => true
|
1070
|
+
# p Name.create("example.com.") == "example.com" => false
|
1071
|
+
def ==(other)
|
1072
|
+
other = Name.create(other)
|
1073
|
+
return false unless Name === other
|
1074
|
+
return @labels == other.to_a && @absolute == other.absolute?
|
1075
|
+
end
|
1076
|
+
alias eql? ==
|
1077
|
+
|
1078
|
+
# Tests subdomain-of relation.
|
1079
|
+
#
|
1080
|
+
# domain = Resolv::DNS::Name.create("y.z")
|
1081
|
+
# p Resolv::DNS::Name.create("w.x.y.z").subdomain_of?(domain) #=> true
|
1082
|
+
# p Resolv::DNS::Name.create("x.y.z").subdomain_of?(domain) #=> true
|
1083
|
+
# p Resolv::DNS::Name.create("y.z").subdomain_of?(domain) #=> false
|
1084
|
+
# p Resolv::DNS::Name.create("z").subdomain_of?(domain) #=> false
|
1085
|
+
# p Resolv::DNS::Name.create("x.y.z.").subdomain_of?(domain) #=> false
|
1086
|
+
# p Resolv::DNS::Name.create("w.z").subdomain_of?(domain) #=> false
|
1087
|
+
#
|
1088
|
+
def subdomain_of?(other)
|
1089
|
+
other = Name.create(other)
|
1090
|
+
other_len = other.length
|
1091
|
+
return false if @labels.length <= other_len
|
1092
|
+
return @labels[-other_len, other_len] == other.to_a
|
1093
|
+
end
|
1094
|
+
|
1095
|
+
def hash
|
1096
|
+
return @labels.hash ^ @absolute.hash
|
1097
|
+
end
|
1098
|
+
|
1099
|
+
# Returns the array of labels, each label is a Label::Str.
|
1100
|
+
def to_a
|
1101
|
+
return @labels
|
1102
|
+
end
|
1103
|
+
|
1104
|
+
# Returns the length of the array of labels.
|
1105
|
+
def length
|
1106
|
+
return @labels.length
|
1107
|
+
end
|
1108
|
+
|
1109
|
+
# Returns the +i+th label.
|
1110
|
+
def [](i)
|
1111
|
+
return @labels[i]
|
1112
|
+
end
|
1113
|
+
|
1114
|
+
# Returns the domain name as a string.
|
1115
|
+
#
|
1116
|
+
# The domain name doesn't have a trailing dot even if the name object is
|
1117
|
+
# absolute.
|
1118
|
+
#
|
1119
|
+
# p Resolv::DNS::Name.create("x.y.z.").to_s #=> "x.y.z"
|
1120
|
+
# p Resolv::DNS::Name.create("x.y.z").to_s #=> "x.y.z"
|
1121
|
+
#
|
1122
|
+
def to_s
|
1123
|
+
return @labels.join('.')
|
1124
|
+
end
|
1125
|
+
end
|
1126
|
+
|
1127
|
+
class Message # :nodoc:
|
1128
|
+
@@identifier = -1
|
1129
|
+
|
1130
|
+
def initialize(id = (@@identifier += 1) & 0xffff)
|
1131
|
+
@id = id
|
1132
|
+
@qr = 0
|
1133
|
+
@opcode = 0
|
1134
|
+
@aa = 0
|
1135
|
+
@tc = 0
|
1136
|
+
@rd = 0 # recursion desired
|
1137
|
+
@ra = 0 # recursion available
|
1138
|
+
@rcode = 0
|
1139
|
+
@question = []
|
1140
|
+
@answer = []
|
1141
|
+
@authority = []
|
1142
|
+
@additional = []
|
1143
|
+
end
|
1144
|
+
|
1145
|
+
attr_accessor :id, :qr, :opcode, :aa, :tc, :rd, :ra, :rcode
|
1146
|
+
attr_reader :question, :answer, :authority, :additional
|
1147
|
+
|
1148
|
+
def ==(other)
|
1149
|
+
return @id == other.id &&
|
1150
|
+
@qr == other.qr &&
|
1151
|
+
@opcode == other.opcode &&
|
1152
|
+
@aa == other.aa &&
|
1153
|
+
@tc == other.tc &&
|
1154
|
+
@rd == other.rd &&
|
1155
|
+
@ra == other.ra &&
|
1156
|
+
@rcode == other.rcode &&
|
1157
|
+
@question == other.question &&
|
1158
|
+
@answer == other.answer &&
|
1159
|
+
@authority == other.authority &&
|
1160
|
+
@additional == other.additional
|
1161
|
+
end
|
1162
|
+
|
1163
|
+
def add_question(name, typeclass, unicast = false)
|
1164
|
+
@question << [Name.create(name), typeclass, unicast]
|
1165
|
+
end
|
1166
|
+
|
1167
|
+
# Can accept either |name, typeclass| as block arguments
|
1168
|
+
# (backwards-compatible/DNS-style), or |name, typeclass, unicast|
|
1169
|
+
# (mDNS-style).
|
1170
|
+
def each_question # :yields: name, typeclass, unicast
|
1171
|
+
@question.each {|ary|
|
1172
|
+
yield ary
|
1173
|
+
}
|
1174
|
+
end
|
1175
|
+
|
1176
|
+
def add_answer(name, ttl, data, cacheflush = false)
|
1177
|
+
@answer << [Name.create(name), ttl, data, cacheflush]
|
1178
|
+
end
|
1179
|
+
|
1180
|
+
# Can accept either |name, ttl, data| as block arguments
|
1181
|
+
# (backwards-compatible/DNS-style), or |name, ttl, data, cacheflush|
|
1182
|
+
# (mDNS-style).
|
1183
|
+
def each_answer # :yields: name, ttl, data, cacheflush
|
1184
|
+
@answer.each {|ary|
|
1185
|
+
yield ary
|
1186
|
+
}
|
1187
|
+
end
|
1188
|
+
|
1189
|
+
def add_authority(name, ttl, data)
|
1190
|
+
@authority << [Name.create(name), ttl, data]
|
1191
|
+
end
|
1192
|
+
|
1193
|
+
def each_authority
|
1194
|
+
@authority.each {|name, ttl, data|
|
1195
|
+
yield name, ttl, data
|
1196
|
+
}
|
1197
|
+
end
|
1198
|
+
|
1199
|
+
def add_additional(name, ttl, data)
|
1200
|
+
@additional << [Name.create(name), ttl, data]
|
1201
|
+
end
|
1202
|
+
|
1203
|
+
def each_additional
|
1204
|
+
@additional.each {|name, ttl, data|
|
1205
|
+
yield name, ttl, data
|
1206
|
+
}
|
1207
|
+
end
|
1208
|
+
|
1209
|
+
def each_resource
|
1210
|
+
each_answer {|name, ttl, data| yield name, ttl, data}
|
1211
|
+
each_authority {|name, ttl, data| yield name, ttl, data}
|
1212
|
+
each_additional {|name, ttl, data| yield name, ttl, data}
|
1213
|
+
end
|
1214
|
+
|
1215
|
+
def encode
|
1216
|
+
return MessageEncoder.new {|msg|
|
1217
|
+
msg.put_pack('nnnnnn',
|
1218
|
+
@id,
|
1219
|
+
(@qr & 1) << 15 |
|
1220
|
+
(@opcode & 15) << 11 |
|
1221
|
+
(@aa & 1) << 10 |
|
1222
|
+
(@tc & 1) << 9 |
|
1223
|
+
(@rd & 1) << 8 |
|
1224
|
+
(@ra & 1) << 7 |
|
1225
|
+
(@rcode & 15),
|
1226
|
+
@question.length,
|
1227
|
+
@answer.length,
|
1228
|
+
@authority.length,
|
1229
|
+
@additional.length)
|
1230
|
+
@question.each {|q|
|
1231
|
+
name, typeclass, unicast = q
|
1232
|
+
hibit = unicast ? (1<<15) : 0x00
|
1233
|
+
msg.put_name(name)
|
1234
|
+
msg.put_pack('nn', typeclass::TypeValue, typeclass::ClassValue|hibit)
|
1235
|
+
}
|
1236
|
+
[@answer, @authority, @additional].each {|rr|
|
1237
|
+
rr.each {|r|
|
1238
|
+
name, ttl, data, cacheflush = r
|
1239
|
+
hibit = cacheflush ? (1<<15) : 0x00
|
1240
|
+
msg.put_name(name)
|
1241
|
+
msg.put_pack('nnN', data.class::TypeValue, data.class::ClassValue|hibit, ttl)
|
1242
|
+
msg.put_length16 {data.encode_rdata(msg)}
|
1243
|
+
}
|
1244
|
+
}
|
1245
|
+
}.to_s
|
1246
|
+
end
|
1247
|
+
|
1248
|
+
class MessageEncoder # :nodoc: used to implement Message.encode
|
1249
|
+
def initialize
|
1250
|
+
@data = ''
|
1251
|
+
@names = {}
|
1252
|
+
yield self
|
1253
|
+
end
|
1254
|
+
|
1255
|
+
def to_s
|
1256
|
+
return @data
|
1257
|
+
end
|
1258
|
+
|
1259
|
+
def put_bytes(d)
|
1260
|
+
@data << d
|
1261
|
+
end
|
1262
|
+
|
1263
|
+
def put_pack(template, *d)
|
1264
|
+
@data << d.pack(template)
|
1265
|
+
end
|
1266
|
+
|
1267
|
+
def put_length16
|
1268
|
+
length_index = @data.length
|
1269
|
+
@data << "\0\0"
|
1270
|
+
data_start = @data.length
|
1271
|
+
yield
|
1272
|
+
data_end = @data.length
|
1273
|
+
@data[length_index, 2] = [data_end - data_start].pack("n")
|
1274
|
+
end
|
1275
|
+
|
1276
|
+
def put_string(d)
|
1277
|
+
raise ArgumentError, "strings longer than 255 characters cannot be encoded" if d.length > 255
|
1278
|
+
self.put_pack("C", d.length)
|
1279
|
+
@data << d
|
1280
|
+
end
|
1281
|
+
|
1282
|
+
def put_string_list(ds)
|
1283
|
+
ds.each {|d|
|
1284
|
+
self.put_string(d)
|
1285
|
+
}
|
1286
|
+
end
|
1287
|
+
|
1288
|
+
def put_name(d)
|
1289
|
+
put_labels(d.to_a)
|
1290
|
+
end
|
1291
|
+
|
1292
|
+
def put_labels(d)
|
1293
|
+
d.each_index {|i|
|
1294
|
+
domain = d[i..-1]
|
1295
|
+
if idx = @names[domain]
|
1296
|
+
self.put_pack("n", 0xc000 | idx)
|
1297
|
+
return
|
1298
|
+
else
|
1299
|
+
@names[domain] = @data.length
|
1300
|
+
self.put_label(d[i])
|
1301
|
+
end
|
1302
|
+
}
|
1303
|
+
@data << "\0"
|
1304
|
+
end
|
1305
|
+
|
1306
|
+
def put_label(d)
|
1307
|
+
self.put_string(d.string)
|
1308
|
+
end
|
1309
|
+
end
|
1310
|
+
|
1311
|
+
def Message.decode(m)
|
1312
|
+
o = Message.new(0)
|
1313
|
+
MessageDecoder.new(m) {|msg|
|
1314
|
+
id, flag, qdcount, ancount, nscount, arcount =
|
1315
|
+
msg.get_unpack('nnnnnn')
|
1316
|
+
o.id = id
|
1317
|
+
o.qr = (flag >> 15) & 1
|
1318
|
+
o.opcode = (flag >> 11) & 15
|
1319
|
+
o.aa = (flag >> 10) & 1
|
1320
|
+
o.tc = (flag >> 9) & 1
|
1321
|
+
o.rd = (flag >> 8) & 1
|
1322
|
+
o.ra = (flag >> 7) & 1
|
1323
|
+
o.rcode = flag & 15
|
1324
|
+
(1..qdcount).each {
|
1325
|
+
name, typeclass, unicast = msg.get_question
|
1326
|
+
o.add_question(name, typeclass, unicast)
|
1327
|
+
}
|
1328
|
+
(1..ancount).each {
|
1329
|
+
name, ttl, data, cacheflush = msg.get_rr
|
1330
|
+
o.add_answer(name, ttl, data)
|
1331
|
+
}
|
1332
|
+
(1..nscount).each {
|
1333
|
+
name, ttl, data = msg.get_rr
|
1334
|
+
o.add_authority(name, ttl, data)
|
1335
|
+
}
|
1336
|
+
(1..arcount).each {
|
1337
|
+
name, ttl, data = msg.get_rr
|
1338
|
+
o.add_additional(name, ttl, data)
|
1339
|
+
}
|
1340
|
+
}
|
1341
|
+
return o
|
1342
|
+
end
|
1343
|
+
|
1344
|
+
class MessageDecoder # :nodoc: used to implement Message.decode
|
1345
|
+
def initialize(data)
|
1346
|
+
@data = data
|
1347
|
+
@index = 0
|
1348
|
+
@limit = data.length
|
1349
|
+
yield self
|
1350
|
+
end
|
1351
|
+
|
1352
|
+
def get_length16
|
1353
|
+
len, = self.get_unpack('n')
|
1354
|
+
save_limit = @limit
|
1355
|
+
@limit = @index + len
|
1356
|
+
d = yield(len)
|
1357
|
+
if @index < @limit
|
1358
|
+
raise DecodeError.new("junk exists")
|
1359
|
+
elsif @limit < @index
|
1360
|
+
raise DecodeError.new("limit exceeded")
|
1361
|
+
end
|
1362
|
+
@limit = save_limit
|
1363
|
+
return d
|
1364
|
+
end
|
1365
|
+
|
1366
|
+
def get_bytes(len = @limit - @index)
|
1367
|
+
d = @data[@index, len]
|
1368
|
+
@index += len
|
1369
|
+
return d
|
1370
|
+
end
|
1371
|
+
|
1372
|
+
def get_unpack(template)
|
1373
|
+
len = 0
|
1374
|
+
template.each_byte {|byte|
|
1375
|
+
case byte
|
1376
|
+
when ?c, ?C
|
1377
|
+
len += 1
|
1378
|
+
when ?n
|
1379
|
+
len += 2
|
1380
|
+
when ?N
|
1381
|
+
len += 4
|
1382
|
+
else
|
1383
|
+
raise StandardError.new("unsupported template: '#{byte.chr}' in '#{template}'")
|
1384
|
+
end
|
1385
|
+
}
|
1386
|
+
raise DecodeError.new("limit exceeded") if @limit < @index + len
|
1387
|
+
arr = @data.unpack("@#{@index}#{template}")
|
1388
|
+
@index += len
|
1389
|
+
return arr
|
1390
|
+
end
|
1391
|
+
|
1392
|
+
def get_string
|
1393
|
+
len = @data[@index]
|
1394
|
+
raise DecodeError.new("limit exceeded") if @limit < @index + 1 + len
|
1395
|
+
d = @data[@index + 1, len]
|
1396
|
+
@index += 1 + len
|
1397
|
+
return d
|
1398
|
+
end
|
1399
|
+
|
1400
|
+
def get_string_list
|
1401
|
+
strings = []
|
1402
|
+
while @index < @limit
|
1403
|
+
strings << self.get_string
|
1404
|
+
end
|
1405
|
+
strings
|
1406
|
+
end
|
1407
|
+
|
1408
|
+
def get_name
|
1409
|
+
return Name.new(self.get_labels)
|
1410
|
+
end
|
1411
|
+
|
1412
|
+
def get_labels(limit=nil)
|
1413
|
+
limit = @index if !limit || @index < limit
|
1414
|
+
d = []
|
1415
|
+
while true
|
1416
|
+
case @data[@index]
|
1417
|
+
when 0
|
1418
|
+
@index += 1
|
1419
|
+
return d
|
1420
|
+
when 192..255
|
1421
|
+
idx = self.get_unpack('n')[0] & 0x3fff
|
1422
|
+
if limit <= idx
|
1423
|
+
raise DecodeError.new("non-backward name pointer")
|
1424
|
+
end
|
1425
|
+
save_index = @index
|
1426
|
+
@index = idx
|
1427
|
+
d += self.get_labels(limit)
|
1428
|
+
@index = save_index
|
1429
|
+
return d
|
1430
|
+
else
|
1431
|
+
d << self.get_label
|
1432
|
+
end
|
1433
|
+
end
|
1434
|
+
return d
|
1435
|
+
end
|
1436
|
+
|
1437
|
+
def get_label
|
1438
|
+
return Label::Str.new(self.get_string)
|
1439
|
+
end
|
1440
|
+
|
1441
|
+
def get_question
|
1442
|
+
name = self.get_name
|
1443
|
+
type, klass = self.get_unpack("nn")
|
1444
|
+
typeclass = Resource.get_class(type, klass % 0x8000)
|
1445
|
+
unicast = (klass >> 15) == 1
|
1446
|
+
return name, typeclass, unicast
|
1447
|
+
end
|
1448
|
+
|
1449
|
+
def get_rr
|
1450
|
+
name = self.get_name
|
1451
|
+
type, klass, ttl = self.get_unpack('nnN')
|
1452
|
+
typeclass = Resource.get_class(type, klass % 0x8000)
|
1453
|
+
data = self.get_length16 {typeclass.decode_rdata(self)}
|
1454
|
+
cacheflush = (klass >> 15) == 1
|
1455
|
+
return name, ttl, data, cacheflush
|
1456
|
+
end
|
1457
|
+
end
|
1458
|
+
end
|
1459
|
+
|
1460
|
+
class Query # :nodoc:
|
1461
|
+
def encode_rdata(msg)
|
1462
|
+
raise EncodeError.new("#{self.class} is query.")
|
1463
|
+
end
|
1464
|
+
|
1465
|
+
def self.decode_rdata(msg)
|
1466
|
+
raise DecodeError.new("#{self.class} is query.")
|
1467
|
+
end
|
1468
|
+
end
|
1469
|
+
|
1470
|
+
class Resource < Query
|
1471
|
+
ClassHash = {} # :nodoc:
|
1472
|
+
|
1473
|
+
def encode_rdata(msg) # :nodoc:
|
1474
|
+
raise NotImplementedError.new
|
1475
|
+
end
|
1476
|
+
|
1477
|
+
def self.decode_rdata(msg) # :nodoc:
|
1478
|
+
raise NotImplementedError.new
|
1479
|
+
end
|
1480
|
+
|
1481
|
+
def ==(other)
|
1482
|
+
return self.class == other.class &&
|
1483
|
+
self.instance_variables == other.instance_variables &&
|
1484
|
+
self.instance_variables.collect {|name| self.instance_eval name} ==
|
1485
|
+
other.instance_variables.collect {|name| other.instance_eval name}
|
1486
|
+
end
|
1487
|
+
|
1488
|
+
def eql?(other)
|
1489
|
+
return self == other
|
1490
|
+
end
|
1491
|
+
|
1492
|
+
def hash
|
1493
|
+
h = 0
|
1494
|
+
self.instance_variables.each {|name|
|
1495
|
+
h ^= self.instance_eval("#{name}.hash")
|
1496
|
+
}
|
1497
|
+
return h
|
1498
|
+
end
|
1499
|
+
|
1500
|
+
def self.get_class(type_value, class_value) # :nodoc:
|
1501
|
+
return ClassHash[[type_value, class_value]] ||
|
1502
|
+
Generic.create(type_value, class_value)
|
1503
|
+
end
|
1504
|
+
|
1505
|
+
class Generic < Resource
|
1506
|
+
def initialize(data)
|
1507
|
+
@data = data
|
1508
|
+
end
|
1509
|
+
attr_reader :data
|
1510
|
+
|
1511
|
+
def encode_rdata(msg) # :nodoc:
|
1512
|
+
msg.put_bytes(data)
|
1513
|
+
end
|
1514
|
+
|
1515
|
+
def self.decode_rdata(msg) # :nodoc:
|
1516
|
+
return self.new(msg.get_bytes)
|
1517
|
+
end
|
1518
|
+
|
1519
|
+
def self.create(type_value, class_value) # :nodoc:
|
1520
|
+
c = Class.new(Generic)
|
1521
|
+
c.const_set(:TypeValue, type_value)
|
1522
|
+
c.const_set(:ClassValue, class_value)
|
1523
|
+
Generic.const_set("Type#{type_value}_Class#{class_value}", c)
|
1524
|
+
ClassHash[[type_value, class_value]] = c
|
1525
|
+
return c
|
1526
|
+
end
|
1527
|
+
end
|
1528
|
+
|
1529
|
+
class DomainName < Resource
|
1530
|
+
def initialize(name)
|
1531
|
+
@name = name
|
1532
|
+
end
|
1533
|
+
attr_reader :name
|
1534
|
+
|
1535
|
+
def encode_rdata(msg) # :nodoc:
|
1536
|
+
msg.put_name(@name)
|
1537
|
+
end
|
1538
|
+
|
1539
|
+
def self.decode_rdata(msg) # :nodoc:
|
1540
|
+
return self.new(msg.get_name)
|
1541
|
+
end
|
1542
|
+
end
|
1543
|
+
|
1544
|
+
# Standard (class generic) RRs
|
1545
|
+
ClassValue = nil
|
1546
|
+
|
1547
|
+
class NS < DomainName
|
1548
|
+
TypeValue = 2
|
1549
|
+
end
|
1550
|
+
|
1551
|
+
class CNAME < DomainName
|
1552
|
+
TypeValue = 5
|
1553
|
+
end
|
1554
|
+
|
1555
|
+
class SOA < Resource
|
1556
|
+
TypeValue = 6
|
1557
|
+
|
1558
|
+
def initialize(mname, rname, serial, refresh, retry_, expire, minimum)
|
1559
|
+
@mname = mname
|
1560
|
+
@rname = rname
|
1561
|
+
@serial = serial
|
1562
|
+
@refresh = refresh
|
1563
|
+
@retry = retry_
|
1564
|
+
@expire = expire
|
1565
|
+
@minimum = minimum
|
1566
|
+
end
|
1567
|
+
attr_reader :mname, :rname, :serial, :refresh, :retry, :expire, :minimum
|
1568
|
+
|
1569
|
+
def encode_rdata(msg) # :nodoc:
|
1570
|
+
msg.put_name(@mname)
|
1571
|
+
msg.put_name(@rname)
|
1572
|
+
msg.put_pack('NNNNN', @serial, @refresh, @retry, @expire, @minimum)
|
1573
|
+
end
|
1574
|
+
|
1575
|
+
def self.decode_rdata(msg) # :nodoc:
|
1576
|
+
mname = msg.get_name
|
1577
|
+
rname = msg.get_name
|
1578
|
+
serial, refresh, retry_, expire, minimum = msg.get_unpack('NNNNN')
|
1579
|
+
return self.new(
|
1580
|
+
mname, rname, serial, refresh, retry_, expire, minimum)
|
1581
|
+
end
|
1582
|
+
end
|
1583
|
+
|
1584
|
+
class PTR < DomainName
|
1585
|
+
TypeValue = 12
|
1586
|
+
end
|
1587
|
+
|
1588
|
+
class HINFO < Resource
|
1589
|
+
TypeValue = 13
|
1590
|
+
|
1591
|
+
def initialize(cpu, os)
|
1592
|
+
@cpu = cpu
|
1593
|
+
@os = os
|
1594
|
+
end
|
1595
|
+
attr_reader :cpu, :os
|
1596
|
+
|
1597
|
+
def encode_rdata(msg) # :nodoc:
|
1598
|
+
msg.put_string(@cpu)
|
1599
|
+
msg.put_string(@os)
|
1600
|
+
end
|
1601
|
+
|
1602
|
+
def self.decode_rdata(msg) # :nodoc:
|
1603
|
+
cpu = msg.get_string
|
1604
|
+
os = msg.get_string
|
1605
|
+
return self.new(cpu, os)
|
1606
|
+
end
|
1607
|
+
end
|
1608
|
+
|
1609
|
+
class MINFO < Resource
|
1610
|
+
TypeValue = 14
|
1611
|
+
|
1612
|
+
def initialize(rmailbx, emailbx)
|
1613
|
+
@rmailbx = rmailbx
|
1614
|
+
@emailbx = emailbx
|
1615
|
+
end
|
1616
|
+
attr_reader :rmailbx, :emailbx
|
1617
|
+
|
1618
|
+
def encode_rdata(msg) # :nodoc:
|
1619
|
+
msg.put_name(@rmailbx)
|
1620
|
+
msg.put_name(@emailbx)
|
1621
|
+
end
|
1622
|
+
|
1623
|
+
def self.decode_rdata(msg) # :nodoc:
|
1624
|
+
rmailbx = msg.get_string
|
1625
|
+
emailbx = msg.get_string
|
1626
|
+
return self.new(rmailbx, emailbx)
|
1627
|
+
end
|
1628
|
+
end
|
1629
|
+
|
1630
|
+
class MX < Resource
|
1631
|
+
TypeValue= 15
|
1632
|
+
|
1633
|
+
def initialize(preference, exchange)
|
1634
|
+
@preference = preference
|
1635
|
+
@exchange = exchange
|
1636
|
+
end
|
1637
|
+
attr_reader :preference, :exchange
|
1638
|
+
|
1639
|
+
def encode_rdata(msg) # :nodoc:
|
1640
|
+
msg.put_pack('n', @preference)
|
1641
|
+
msg.put_name(@exchange)
|
1642
|
+
end
|
1643
|
+
|
1644
|
+
def self.decode_rdata(msg) # :nodoc:
|
1645
|
+
preference, = msg.get_unpack('n')
|
1646
|
+
exchange = msg.get_name
|
1647
|
+
return self.new(preference, exchange)
|
1648
|
+
end
|
1649
|
+
end
|
1650
|
+
|
1651
|
+
class TXT < Resource
|
1652
|
+
TypeValue = 16
|
1653
|
+
|
1654
|
+
# TXT resource records must have one or more character strings, but the
|
1655
|
+
# string may be zero-length.
|
1656
|
+
#
|
1657
|
+
# If only the +first_string+ is supplied, it may be longer than 255
|
1658
|
+
# characters (internally, it will split into multiple
|
1659
|
+
# character-strings). If multiple strings are supplied, each string
|
1660
|
+
# must not be longer than 255 characters.
|
1661
|
+
def initialize(first_string = '', *rest_strings)
|
1662
|
+
if first_string.length > 255
|
1663
|
+
raise ArgumentError, 'TXT strings are longer than 255 characters' if rest_strings.first
|
1664
|
+
|
1665
|
+
@strings = []
|
1666
|
+
first_string.scan(/.{1,255}/) { |s| @strings << s }
|
1667
|
+
else
|
1668
|
+
@strings = [first_string, *rest_strings].compact
|
1669
|
+
end
|
1670
|
+
end
|
1671
|
+
|
1672
|
+
# Returns an array of all the strings making up the resource data.
|
1673
|
+
# There may be multiple strings if this is a mDNS record or if the
|
1674
|
+
# resource data is longer than 255 bytes. In the case of mDNS, each
|
1675
|
+
# individual string has the form: "key=value".
|
1676
|
+
attr_reader :strings
|
1677
|
+
|
1678
|
+
# Returns the resource data as a single string. DNS uses multiple
|
1679
|
+
# character strings to represent the data of a TXT record if the
|
1680
|
+
# data is longer than 255 characters.
|
1681
|
+
def data
|
1682
|
+
@strings.join
|
1683
|
+
end
|
1684
|
+
|
1685
|
+
def encode_rdata(msg) # :nodoc:
|
1686
|
+
msg.put_string_list(@strings)
|
1687
|
+
end
|
1688
|
+
|
1689
|
+
def self.decode_rdata(msg) # :nodoc:
|
1690
|
+
strings = msg.get_string_list
|
1691
|
+
return self.new(*strings)
|
1692
|
+
end
|
1693
|
+
end
|
1694
|
+
|
1695
|
+
class ANY < Query
|
1696
|
+
TypeValue = 255 # :nodoc:
|
1697
|
+
end
|
1698
|
+
|
1699
|
+
ClassInsensitiveTypes = [ # :nodoc:
|
1700
|
+
NS, CNAME, SOA, PTR, HINFO, MINFO, MX, TXT, ANY
|
1701
|
+
]
|
1702
|
+
|
1703
|
+
# ARPA Internet specific RRs
|
1704
|
+
module IN
|
1705
|
+
ClassValue = 1
|
1706
|
+
|
1707
|
+
ClassInsensitiveTypes.each {|s|
|
1708
|
+
c = Class.new(s)
|
1709
|
+
c.const_set(:TypeValue, s::TypeValue)
|
1710
|
+
c.const_set(:ClassValue, ClassValue)
|
1711
|
+
ClassHash[[s::TypeValue, ClassValue]] = c
|
1712
|
+
self.const_set(s.name.sub(/.*::/, ''), c)
|
1713
|
+
}
|
1714
|
+
|
1715
|
+
class A < Resource
|
1716
|
+
ClassHash[[TypeValue = 1, ClassValue = ClassValue]] = self
|
1717
|
+
|
1718
|
+
def initialize(address)
|
1719
|
+
@address = IPv4.create(address)
|
1720
|
+
end
|
1721
|
+
attr_reader :address
|
1722
|
+
|
1723
|
+
def encode_rdata(msg) # :nodoc:
|
1724
|
+
msg.put_bytes(@address.address)
|
1725
|
+
end
|
1726
|
+
|
1727
|
+
def self.decode_rdata(msg) # :nodoc:
|
1728
|
+
return self.new(IPv4.new(msg.get_bytes(4)))
|
1729
|
+
end
|
1730
|
+
end
|
1731
|
+
|
1732
|
+
class WKS < Resource
|
1733
|
+
ClassHash[[TypeValue = 11, ClassValue = ClassValue]] = self
|
1734
|
+
|
1735
|
+
def initialize(address, protocol, bitmap)
|
1736
|
+
@address = IPv4.create(address)
|
1737
|
+
@protocol = protocol
|
1738
|
+
@bitmap = bitmap
|
1739
|
+
end
|
1740
|
+
attr_reader :address, :protocol, :bitmap
|
1741
|
+
|
1742
|
+
def encode_rdata(msg) # :nodoc:
|
1743
|
+
msg.put_bytes(@address.address)
|
1744
|
+
msg.put_pack("n", @protocol)
|
1745
|
+
msg.put_bytes(@bitmap)
|
1746
|
+
end
|
1747
|
+
|
1748
|
+
def self.decode_rdata(msg) # :nodoc:
|
1749
|
+
address = IPv4.new(msg.get_bytes(4))
|
1750
|
+
protocol, = msg.get_unpack("n")
|
1751
|
+
bitmap = msg.get_bytes
|
1752
|
+
return self.new(address, protocol, bitmap)
|
1753
|
+
end
|
1754
|
+
end
|
1755
|
+
|
1756
|
+
class AAAA < Resource
|
1757
|
+
ClassHash[[TypeValue = 28, ClassValue = ClassValue]] = self
|
1758
|
+
|
1759
|
+
def initialize(address)
|
1760
|
+
@address = IPv6.create(address)
|
1761
|
+
end
|
1762
|
+
attr_reader :address
|
1763
|
+
|
1764
|
+
def encode_rdata(msg) # :nodoc:
|
1765
|
+
msg.put_bytes(@address.address)
|
1766
|
+
end
|
1767
|
+
|
1768
|
+
def self.decode_rdata(msg) # :nodoc:
|
1769
|
+
return self.new(IPv6.new(msg.get_bytes(16)))
|
1770
|
+
end
|
1771
|
+
end
|
1772
|
+
|
1773
|
+
# SRV resource record defined in RFC 2782
|
1774
|
+
#
|
1775
|
+
# These records identify the hostname and port that a service is
|
1776
|
+
# available at.
|
1777
|
+
#
|
1778
|
+
# The format is:
|
1779
|
+
# _Service._Proto.Name TTL Class SRV Priority Weight Port Target
|
1780
|
+
#
|
1781
|
+
# The fields specific to SRV are defined in RFC 2782 as meaning:
|
1782
|
+
# - +priority+ The priority of this target host. A client MUST attempt
|
1783
|
+
# to contact the target host with the lowest-numbered priority it can
|
1784
|
+
# reach; target hosts with the same priority SHOULD be tried in an
|
1785
|
+
# order defined by the weight field. The range is 0-65535. Note that
|
1786
|
+
# it is not widely implemented and is commonly set to zero.
|
1787
|
+
#
|
1788
|
+
# - +weight+ A server selection mechanism. The weight field specifies
|
1789
|
+
# a relative weight for entries with the same priority. Larger weights
|
1790
|
+
# SHOULD be given a proportionately higher probability of being
|
1791
|
+
# selected. The range of this number is 0-65535. Domain administrators
|
1792
|
+
# SHOULD use Weight 0 when there isn't any server selection to do, to
|
1793
|
+
# make the RR easier to read for humans (less noisy). Note that it is
|
1794
|
+
# not widely implemented and should be set to zero.
|
1795
|
+
#
|
1796
|
+
# - +port+ The port on this target host of this service. The range is 0-
|
1797
|
+
# 65535.
|
1798
|
+
#
|
1799
|
+
# - +target+ The domain name of the target host. A target of "." means
|
1800
|
+
# that the service is decidedly not available at this domain.
|
1801
|
+
class SRV < Resource
|
1802
|
+
ClassHash[[TypeValue = 33, ClassValue = ClassValue]] = self # :nodoc:
|
1803
|
+
|
1804
|
+
# Create a SRV resource record.
|
1805
|
+
#
|
1806
|
+
# TODO - switch args to (target, port, priority = 0, weight = 0)? It
|
1807
|
+
# would be more convenient to use.
|
1808
|
+
def initialize(priority, weight, port, target)
|
1809
|
+
@priority = priority.to_int
|
1810
|
+
@weight = weight.to_int
|
1811
|
+
@port = port.to_int
|
1812
|
+
@target = Name.create(target)
|
1813
|
+
end
|
1814
|
+
|
1815
|
+
attr_reader :target, :port, :priority, :weight
|
1816
|
+
|
1817
|
+
def encode_rdata(msg) # :nodoc:
|
1818
|
+
msg.put_pack("n", @priority)
|
1819
|
+
msg.put_pack("n", @weight)
|
1820
|
+
msg.put_pack("n", @port)
|
1821
|
+
msg.put_name(@target)
|
1822
|
+
end
|
1823
|
+
|
1824
|
+
def self.decode_rdata(msg) # :nodoc:
|
1825
|
+
priority, = msg.get_unpack("n")
|
1826
|
+
weight, = msg.get_unpack("n")
|
1827
|
+
port, = msg.get_unpack("n")
|
1828
|
+
target = msg.get_name
|
1829
|
+
return self.new(priority, weight, port, target)
|
1830
|
+
end
|
1831
|
+
end
|
1832
|
+
|
1833
|
+
end
|
1834
|
+
end
|
1835
|
+
end
|
1836
|
+
|
1837
|
+
# Obsoleted by ipaddr.rb?
|
1838
|
+
class IPv4 # :nodoc:
|
1839
|
+
Regex = /\A(\d+)\.(\d+)\.(\d+)\.(\d+)\z/
|
1840
|
+
|
1841
|
+
def self.create(arg)
|
1842
|
+
if(arg.kind_of?(String) && arg.length == 4)
|
1843
|
+
return self.new(arg)
|
1844
|
+
end
|
1845
|
+
case arg
|
1846
|
+
when IPv4
|
1847
|
+
return arg
|
1848
|
+
when Regex
|
1849
|
+
if (0..255) === (a = $1.to_i) &&
|
1850
|
+
(0..255) === (b = $2.to_i) &&
|
1851
|
+
(0..255) === (c = $3.to_i) &&
|
1852
|
+
(0..255) === (d = $4.to_i)
|
1853
|
+
return self.new([a, b, c, d].pack("CCCC"))
|
1854
|
+
else
|
1855
|
+
raise ArgumentError.new("IPv4 address with invalid value: " + arg)
|
1856
|
+
end
|
1857
|
+
else
|
1858
|
+
raise ArgumentError.new("cannot interpret as IPv4 address: #{arg.inspect}")
|
1859
|
+
end
|
1860
|
+
end
|
1861
|
+
|
1862
|
+
def initialize(address)
|
1863
|
+
unless address.kind_of?(String) && address.length == 4
|
1864
|
+
raise ArgumentError.new('IPv4 address must be 4 bytes')
|
1865
|
+
end
|
1866
|
+
@address = address
|
1867
|
+
end
|
1868
|
+
attr_reader :address
|
1869
|
+
|
1870
|
+
def to_s
|
1871
|
+
return sprintf("%d.%d.%d.%d", *@address.unpack("CCCC"))
|
1872
|
+
end
|
1873
|
+
|
1874
|
+
def inspect
|
1875
|
+
return "#<#{self.class} #{self.to_s}>"
|
1876
|
+
end
|
1877
|
+
|
1878
|
+
def to_name
|
1879
|
+
return DNS::Name.create(
|
1880
|
+
'%d.%d.%d.%d.in-addr.arpa.' % @address.unpack('CCCC').reverse)
|
1881
|
+
end
|
1882
|
+
|
1883
|
+
def ==(other)
|
1884
|
+
return @address == other.address
|
1885
|
+
end
|
1886
|
+
|
1887
|
+
def eql?(other)
|
1888
|
+
return self == other
|
1889
|
+
end
|
1890
|
+
|
1891
|
+
def hash
|
1892
|
+
return @address.hash
|
1893
|
+
end
|
1894
|
+
end
|
1895
|
+
|
1896
|
+
# Obsoleted by ipaddr.rb?
|
1897
|
+
class IPv6 # :nodoc:
|
1898
|
+
Regex_8Hex = /\A
|
1899
|
+
(?:[0-9A-Fa-f]{1,4}:){7}
|
1900
|
+
[0-9A-Fa-f]{1,4}
|
1901
|
+
\z/x
|
1902
|
+
|
1903
|
+
Regex_CompressedHex = /\A
|
1904
|
+
((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::
|
1905
|
+
((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?)
|
1906
|
+
\z/x
|
1907
|
+
|
1908
|
+
Regex_6Hex4Dec = /\A
|
1909
|
+
((?:[0-9A-Fa-f]{1,4}:){6,6})
|
1910
|
+
(\d+)\.(\d+)\.(\d+)\.(\d+)
|
1911
|
+
\z/x
|
1912
|
+
|
1913
|
+
Regex_CompressedHex4Dec = /\A
|
1914
|
+
((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) ::
|
1915
|
+
((?:[0-9A-Fa-f]{1,4}:)*)
|
1916
|
+
(\d+)\.(\d+)\.(\d+)\.(\d+)
|
1917
|
+
\z/x
|
1918
|
+
|
1919
|
+
Regex = /
|
1920
|
+
(?:#{Regex_8Hex}) |
|
1921
|
+
(?:#{Regex_CompressedHex}) |
|
1922
|
+
(?:#{Regex_6Hex4Dec}) |
|
1923
|
+
(?:#{Regex_CompressedHex4Dec})/x
|
1924
|
+
|
1925
|
+
def self.create(arg)
|
1926
|
+
case arg
|
1927
|
+
when IPv6
|
1928
|
+
return arg
|
1929
|
+
when String
|
1930
|
+
address = ''
|
1931
|
+
if Regex_8Hex =~ arg
|
1932
|
+
arg.scan(/[0-9A-Fa-f]+/) {|hex| address << [hex.hex].pack('n')}
|
1933
|
+
elsif Regex_CompressedHex =~ arg
|
1934
|
+
prefix = $1
|
1935
|
+
suffix = $2
|
1936
|
+
a1 = ''
|
1937
|
+
a2 = ''
|
1938
|
+
prefix.scan(/[0-9A-Fa-f]+/) {|hex| a1 << [hex.hex].pack('n')}
|
1939
|
+
suffix.scan(/[0-9A-Fa-f]+/) {|hex| a2 << [hex.hex].pack('n')}
|
1940
|
+
omitlen = 16 - a1.length - a2.length
|
1941
|
+
address << a1 << "\0" * omitlen << a2
|
1942
|
+
elsif Regex_6Hex4Dec =~ arg
|
1943
|
+
prefix, a, b, c, d = $1, $2.to_i, $3.to_i, $4.to_i, $5.to_i
|
1944
|
+
if (0..255) === a && (0..255) === b && (0..255) === c && (0..255) === d
|
1945
|
+
prefix.scan(/[0-9A-Fa-f]+/) {|hex| address << [hex.hex].pack('n')}
|
1946
|
+
address << [a, b, c, d].pack('CCCC')
|
1947
|
+
else
|
1948
|
+
raise ArgumentError.new("not numeric IPv6 address: " + arg)
|
1949
|
+
end
|
1950
|
+
elsif Regex_CompressedHex4Dec =~ arg
|
1951
|
+
prefix, suffix, a, b, c, d = $1, $2, $3.to_i, $4.to_i, $5.to_i, $6.to_i
|
1952
|
+
if (0..255) === a && (0..255) === b && (0..255) === c && (0..255) === d
|
1953
|
+
a1 = ''
|
1954
|
+
a2 = ''
|
1955
|
+
prefix.scan(/[0-9A-Fa-f]+/) {|hex| a1 << [hex.hex].pack('n')}
|
1956
|
+
suffix.scan(/[0-9A-Fa-f]+/) {|hex| a2 << [hex.hex].pack('n')}
|
1957
|
+
omitlen = 12 - a1.length - a2.length
|
1958
|
+
address << a1 << "\0" * omitlen << a2 << [a, b, c, d].pack('CCCC')
|
1959
|
+
else
|
1960
|
+
raise ArgumentError.new("not numeric IPv6 address: " + arg)
|
1961
|
+
end
|
1962
|
+
else
|
1963
|
+
raise ArgumentError.new("not numeric IPv6 address: " + arg)
|
1964
|
+
end
|
1965
|
+
return IPv6.new(address)
|
1966
|
+
else
|
1967
|
+
raise ArgumentError.new("cannot interpret as IPv6 address: #{arg.inspect}")
|
1968
|
+
end
|
1969
|
+
end
|
1970
|
+
|
1971
|
+
def initialize(address)
|
1972
|
+
unless address.kind_of?(String) && address.length == 16
|
1973
|
+
raise ArgumentError.new('IPv6 address must be 16 bytes')
|
1974
|
+
end
|
1975
|
+
@address = address
|
1976
|
+
end
|
1977
|
+
attr_reader :address
|
1978
|
+
|
1979
|
+
def to_s
|
1980
|
+
address = sprintf("%X:%X:%X:%X:%X:%X:%X:%X", *@address.unpack("nnnnnnnn"))
|
1981
|
+
unless address.sub!(/(^|:)0(:0)+(:|$)/, '::')
|
1982
|
+
address.sub!(/(^|:)0(:|$)/, '::')
|
1983
|
+
end
|
1984
|
+
return address
|
1985
|
+
end
|
1986
|
+
|
1987
|
+
def inspect
|
1988
|
+
return "#<#{self.class} #{self.to_s}>"
|
1989
|
+
end
|
1990
|
+
|
1991
|
+
def to_name
|
1992
|
+
# ip6.arpa should be searched too. [RFC3152]
|
1993
|
+
return DNS::Name.new(
|
1994
|
+
@address.unpack("H32")[0].split(//).reverse + ['ip6', 'int'])
|
1995
|
+
end
|
1996
|
+
|
1997
|
+
def ==(other)
|
1998
|
+
return @address == other.address
|
1999
|
+
end
|
2000
|
+
|
2001
|
+
def eql?(other)
|
2002
|
+
return self == other
|
2003
|
+
end
|
2004
|
+
|
2005
|
+
def hash
|
2006
|
+
return @address.hash
|
2007
|
+
end
|
2008
|
+
end
|
2009
|
+
|
2010
|
+
DefaultResolver = self.new
|
2011
|
+
AddressRegex = /(?:#{IPv4::Regex})|(?:#{IPv6::Regex})/
|
2012
|
+
end
|