mirror of
https://github.com/ruby/ruby.git
synced 2022-11-09 12:17:21 -05:00
460 lines
14 KiB
Ruby
460 lines
14 KiB
Ruby
# frozen_string_literal: false
|
|
require 'test/unit'
|
|
require 'resolv'
|
|
require 'socket'
|
|
require 'tempfile'
|
|
|
|
class Object # :nodoc:
|
|
def stub name, val_or_callable, &block
|
|
new_name = "__minitest_stub__#{name}"
|
|
|
|
metaclass = class << self; self; end
|
|
|
|
if respond_to? name and not methods.map(&:to_s).include? name.to_s then
|
|
metaclass.send :define_method, name do |*args|
|
|
super(*args)
|
|
end
|
|
end
|
|
|
|
metaclass.send :alias_method, new_name, name
|
|
|
|
metaclass.send :define_method, name do |*args|
|
|
if val_or_callable.respond_to? :call then
|
|
val_or_callable.call(*args)
|
|
else
|
|
val_or_callable
|
|
end
|
|
end
|
|
|
|
yield self
|
|
ensure
|
|
metaclass.send :undef_method, name
|
|
metaclass.send :alias_method, name, new_name
|
|
metaclass.send :undef_method, new_name
|
|
end unless method_defined?(:stub) # lib/rubygems/test_case.rb also has the same method definition
|
|
end
|
|
|
|
class TestResolvDNS < Test::Unit::TestCase
|
|
def setup
|
|
@save_do_not_reverse_lookup = BasicSocket.do_not_reverse_lookup
|
|
BasicSocket.do_not_reverse_lookup = true
|
|
end
|
|
|
|
def teardown
|
|
BasicSocket.do_not_reverse_lookup = @save_do_not_reverse_lookup
|
|
end
|
|
|
|
def with_udp(host, port)
|
|
u = UDPSocket.new
|
|
begin
|
|
u.bind(host, port)
|
|
yield u
|
|
ensure
|
|
u.close
|
|
end
|
|
end
|
|
|
|
# [ruby-core:65836]
|
|
def test_resolve_with_2_ndots
|
|
conf = Resolv::DNS::Config.new :nameserver => ['127.0.0.1'], :ndots => 2
|
|
assert conf.single?
|
|
|
|
candidates = []
|
|
conf.resolv('example.com') { |candidate, *args|
|
|
candidates << candidate
|
|
raise Resolv::DNS::Config::NXDomain
|
|
}
|
|
n = Resolv::DNS::Name.create 'example.com.'
|
|
assert_equal n, candidates.last
|
|
end
|
|
|
|
def test_query_ipv4_address
|
|
begin
|
|
OpenSSL
|
|
rescue LoadError
|
|
omit 'autoload problem. see [ruby-dev:45021][Bug #5786]'
|
|
end if defined?(OpenSSL)
|
|
|
|
with_udp('127.0.0.1', 0) {|u|
|
|
_, server_port, _, server_address = u.addr
|
|
begin
|
|
client_thread = Thread.new {
|
|
Resolv::DNS.open(:nameserver_port => [[server_address, server_port]]) {|dns|
|
|
dns.getresources("foo.example.org", Resolv::DNS::Resource::IN::A)
|
|
}
|
|
}
|
|
server_thread = Thread.new {
|
|
msg, (_, client_port, _, client_address) = Timeout.timeout(5) {u.recvfrom(4096)}
|
|
id, word2, qdcount, ancount, nscount, arcount = msg.unpack("nnnnnn")
|
|
qr = (word2 & 0x8000) >> 15
|
|
opcode = (word2 & 0x7800) >> 11
|
|
aa = (word2 & 0x0400) >> 10
|
|
tc = (word2 & 0x0200) >> 9
|
|
rd = (word2 & 0x0100) >> 8
|
|
ra = (word2 & 0x0080) >> 7
|
|
z = (word2 & 0x0070) >> 4
|
|
rcode = word2 & 0x000f
|
|
rest = msg[12..-1]
|
|
assert_equal(0, qr) # 0:query 1:response
|
|
assert_equal(0, opcode) # 0:QUERY 1:IQUERY 2:STATUS
|
|
assert_equal(0, aa) # Authoritative Answer
|
|
assert_equal(0, tc) # TrunCation
|
|
assert_equal(1, rd) # Recursion Desired
|
|
assert_equal(0, ra) # Recursion Available
|
|
assert_equal(0, z) # Reserved for future use
|
|
assert_equal(0, rcode) # 0:No-error 1:Format-error 2:Server-failure 3:Name-Error 4:Not-Implemented 5:Refused
|
|
assert_equal(1, qdcount) # number of entries in the question section.
|
|
assert_equal(0, ancount) # number of entries in the answer section.
|
|
assert_equal(0, nscount) # number of entries in the authority records section.
|
|
assert_equal(0, arcount) # number of entries in the additional records section.
|
|
name = [3, "foo", 7, "example", 3, "org", 0].pack("Ca*Ca*Ca*C")
|
|
assert_operator(rest, :start_with?, name)
|
|
rest = rest[name.length..-1]
|
|
assert_equal(4, rest.length)
|
|
qtype, _ = rest.unpack("nn")
|
|
assert_equal(1, qtype) # A
|
|
assert_equal(1, qtype) # IN
|
|
id = id
|
|
qr = 1
|
|
opcode = opcode
|
|
aa = 0
|
|
tc = 0
|
|
rd = rd
|
|
ra = 1
|
|
z = 0
|
|
rcode = 0
|
|
qdcount = 0
|
|
ancount = 1
|
|
nscount = 0
|
|
arcount = 0
|
|
word2 = (qr << 15) |
|
|
(opcode << 11) |
|
|
(aa << 10) |
|
|
(tc << 9) |
|
|
(rd << 8) |
|
|
(ra << 7) |
|
|
(z << 4) |
|
|
rcode
|
|
msg = [id, word2, qdcount, ancount, nscount, arcount].pack("nnnnnn")
|
|
type = 1
|
|
klass = 1
|
|
ttl = 3600
|
|
rdlength = 4
|
|
rdata = [192,0,2,1].pack("CCCC") # 192.0.2.1 (TEST-NET address) RFC 3330
|
|
rr = [name, type, klass, ttl, rdlength, rdata].pack("a*nnNna*")
|
|
msg << rr
|
|
u.send(msg, 0, client_address, client_port)
|
|
}
|
|
result, _ = assert_join_threads([client_thread, server_thread])
|
|
assert_instance_of(Array, result)
|
|
assert_equal(1, result.length)
|
|
rr = result[0]
|
|
assert_instance_of(Resolv::DNS::Resource::IN::A, rr)
|
|
assert_instance_of(Resolv::IPv4, rr.address)
|
|
assert_equal("192.0.2.1", rr.address.to_s)
|
|
assert_equal(3600, rr.ttl)
|
|
end
|
|
}
|
|
end
|
|
|
|
def test_query_ipv4_duplicate_responses
|
|
begin
|
|
OpenSSL
|
|
rescue LoadError
|
|
omit 'autoload problem. see [ruby-dev:45021][Bug #5786]'
|
|
end if defined?(OpenSSL)
|
|
|
|
with_udp('127.0.0.1', 0) {|u|
|
|
_, server_port, _, server_address = u.addr
|
|
begin
|
|
client_thread = Thread.new {
|
|
Resolv::DNS.open(:nameserver_port => [[server_address, server_port]], :search => ['bad1.com', 'bad2.com', 'good.com'], ndots: 5) {|dns|
|
|
dns.getaddress("example")
|
|
}
|
|
}
|
|
server_thread = Thread.new {
|
|
3.times do
|
|
msg, (_, client_port, _, client_address) = Timeout.timeout(5) {u.recvfrom(4096)}
|
|
id, flags, qdcount, ancount, nscount, arcount = msg.unpack("nnnnnn")
|
|
|
|
qr = (flags & 0x8000) >> 15
|
|
opcode = (flags & 0x7800) >> 11
|
|
aa = (flags & 0x0400) >> 10
|
|
tc = (flags & 0x0200) >> 9
|
|
rd = (flags & 0x0100) >> 8
|
|
ra = (flags & 0x0080) >> 7
|
|
z = (flags & 0x0070) >> 4
|
|
rcode = flags & 0x000f
|
|
_rest = msg[12..-1]
|
|
|
|
questions = msg.bytes[12..-1]
|
|
labels = []
|
|
idx = 0
|
|
while idx < questions.length-5
|
|
size = questions[idx]
|
|
labels << questions[idx+1..idx+size].pack('c*')
|
|
idx += size+1
|
|
end
|
|
hostname = labels.join('.')
|
|
|
|
if hostname == "example.good.com"
|
|
id = id
|
|
qr = 1
|
|
opcode = opcode
|
|
aa = 0
|
|
tc = 0
|
|
rd = rd
|
|
ra = 1
|
|
z = 0
|
|
rcode = 0
|
|
qdcount = 1
|
|
ancount = 1
|
|
nscount = 0
|
|
arcount = 0
|
|
word2 = (qr << 15) |
|
|
(opcode << 11) |
|
|
(aa << 10) |
|
|
(tc << 9) |
|
|
(rd << 8) |
|
|
(ra << 7) |
|
|
(z << 4) |
|
|
rcode
|
|
msg = [id, word2, qdcount, ancount, nscount, arcount].pack("nnnnnn")
|
|
msg << questions.pack('c*')
|
|
type = 1
|
|
klass = 1
|
|
ttl = 3600
|
|
rdlength = 4
|
|
rdata = [52,0,2,1].pack("CCCC")
|
|
rr = [0xc00c, type, klass, ttl, rdlength, rdata].pack("nnnNna*")
|
|
msg << rr
|
|
rdata = [52,0,2,2].pack("CCCC")
|
|
rr = [0xc00c, type, klass, ttl, rdlength, rdata].pack("nnnNna*")
|
|
msg << rr
|
|
|
|
u.send(msg, 0, client_address, client_port)
|
|
else
|
|
id = id
|
|
qr = 1
|
|
opcode = opcode
|
|
aa = 0
|
|
tc = 0
|
|
rd = rd
|
|
ra = 1
|
|
z = 0
|
|
rcode = 3
|
|
qdcount = 1
|
|
ancount = 0
|
|
nscount = 0
|
|
arcount = 0
|
|
word2 = (qr << 15) |
|
|
(opcode << 11) |
|
|
(aa << 10) |
|
|
(tc << 9) |
|
|
(rd << 8) |
|
|
(ra << 7) |
|
|
(z << 4) |
|
|
rcode
|
|
msg = [id, word2, qdcount, ancount, nscount, arcount].pack("nnnnnn")
|
|
msg << questions.pack('c*')
|
|
|
|
u.send(msg, 0, client_address, client_port)
|
|
u.send(msg, 0, client_address, client_port)
|
|
end
|
|
end
|
|
}
|
|
result, _ = assert_join_threads([client_thread, server_thread])
|
|
assert_instance_of(Resolv::IPv4, result)
|
|
assert_equal("52.0.2.1", result.to_s)
|
|
end
|
|
}
|
|
end
|
|
|
|
def test_query_ipv4_address_timeout
|
|
with_udp('127.0.0.1', 0) {|u|
|
|
_, port , _, host = u.addr
|
|
start = nil
|
|
rv = Resolv::DNS.open(:nameserver_port => [[host, port]]) {|dns|
|
|
dns.timeouts = 0.1
|
|
start = Time.now
|
|
dns.getresources("foo.example.org", Resolv::DNS::Resource::IN::A)
|
|
}
|
|
t2 = Time.now
|
|
diff = t2 - start
|
|
assert rv.empty?, "unexpected: #{rv.inspect} (expected empty)"
|
|
assert_operator 0.1, :<=, diff
|
|
|
|
rv = Resolv::DNS.open(:nameserver_port => [[host, port]]) {|dns|
|
|
dns.timeouts = [ 0.1, 0.2 ]
|
|
start = Time.now
|
|
dns.getresources("foo.example.org", Resolv::DNS::Resource::IN::A)
|
|
}
|
|
t2 = Time.now
|
|
diff = t2 - start
|
|
assert rv.empty?, "unexpected: #{rv.inspect} (expected empty)"
|
|
assert_operator 0.3, :<=, diff
|
|
}
|
|
end
|
|
|
|
def test_no_server
|
|
omit if /mswin/ =~ RUBY_PLATFORM && ENV.key?('GITHUB_ACTIONS') # not working from the beginning
|
|
u = UDPSocket.new
|
|
u.bind("127.0.0.1", 0)
|
|
_, port, _, host = u.addr
|
|
u.close
|
|
# A race condition here.
|
|
# Another program may use the port.
|
|
# But no way to prevent it.
|
|
begin
|
|
Timeout.timeout(5) do
|
|
Resolv::DNS.open(:nameserver_port => [[host, port]]) {|dns|
|
|
assert_equal([], dns.getresources("test-no-server.example.org", Resolv::DNS::Resource::IN::A))
|
|
}
|
|
end
|
|
rescue Timeout::Error
|
|
if RUBY_PLATFORM.match?(/mingw/)
|
|
# cannot repo locally
|
|
omit 'Timeout Error on MinGW CI'
|
|
else
|
|
raise Timeout::Error
|
|
end
|
|
end
|
|
end
|
|
|
|
def test_invalid_byte_comment
|
|
bug9273 = '[ruby-core:59239] [Bug #9273]'
|
|
Tempfile.create('resolv_test_dns_') do |tmpfile|
|
|
tmpfile.print("\xff\x00\x40")
|
|
tmpfile.close
|
|
assert_nothing_raised(ArgumentError, bug9273) do
|
|
Resolv::DNS::Config.parse_resolv_conf(tmpfile.path)
|
|
end
|
|
end
|
|
end
|
|
|
|
def test_resolv_conf_by_command
|
|
Dir.mktmpdir do |dir|
|
|
Dir.chdir(dir) do
|
|
assert_raise(Errno::ENOENT, Errno::EINVAL) do
|
|
Resolv::DNS::Config.parse_resolv_conf("|echo foo")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def test_dots_diffences
|
|
name1 = Resolv::DNS::Name.create("example.org")
|
|
name2 = Resolv::DNS::Name.create("ex.ampl.eo.rg")
|
|
assert_not_equal(name1, name2, "different dots")
|
|
end
|
|
|
|
def test_case_insensitive_name
|
|
bug10550 = '[ruby-core:66498] [Bug #10550]'
|
|
lower = Resolv::DNS::Name.create("ruby-lang.org")
|
|
upper = Resolv::DNS::Name.create("Ruby-Lang.org")
|
|
assert_equal(lower, upper, bug10550)
|
|
end
|
|
|
|
def test_ipv6_name
|
|
addr = Resolv::IPv6.new("\0"*16)
|
|
labels = addr.to_name.to_a
|
|
expected = (['0'] * 32 + ['ip6', 'arpa']).map {|label| Resolv::DNS::Label::Str.new(label) }
|
|
assert_equal(expected, labels)
|
|
end
|
|
|
|
def test_ipv6_create
|
|
ref = '[Bug #11910] [ruby-core:72559]'
|
|
assert_instance_of Resolv::IPv6, Resolv::IPv6.create('::1'), ref
|
|
assert_instance_of Resolv::IPv6, Resolv::IPv6.create('::1:127.0.0.1'), ref
|
|
end
|
|
|
|
def test_ipv6_to_s
|
|
test_cases = [
|
|
["2001::abcd:abcd:abcd", "2001::ABcd:abcd:ABCD"],
|
|
["2001:db8::1", "2001:db8::0:1"],
|
|
["::", "0:0:0:0:0:0:0:0"],
|
|
["2001::", "2001::0"],
|
|
["2001:db8::1:1:1:1:1", "2001:db8:0:1:1:1:1:1"],
|
|
["1::1:0:0:0:1", "1:0:0:1:0:0:0:1"],
|
|
["1::1:0:0:1", "1:0:0:0:1:0:0:1"],
|
|
]
|
|
|
|
test_cases.each do |expected, ipv6|
|
|
assert_equal expected, Resolv::IPv6.create(ipv6).to_s
|
|
end
|
|
end
|
|
|
|
def test_ipv6_should_be_16
|
|
ref = '[rubygems:1626]'
|
|
|
|
broken_message =
|
|
"\0\0\0\0\0\0\0\0\0\0\0\1" \
|
|
"\x03ns2\bdnsimple\x03com\x00" \
|
|
"\x00\x1C\x00\x01\x00\x02OD" \
|
|
"\x00\x10$\x00\xCB\x00 I\x00\x01\x00\x00\x00\x00"
|
|
|
|
e = assert_raise_with_message(Resolv::DNS::DecodeError, /IPv6 address must be 16 bytes/, ref) do
|
|
Resolv::DNS::Message.decode broken_message
|
|
end
|
|
assert_kind_of(ArgumentError, e.cause)
|
|
end
|
|
|
|
def test_too_big_label_address
|
|
n = 2000
|
|
m = Resolv::DNS::Message::MessageEncoder.new {|msg|
|
|
2.times {
|
|
n.times {|i| msg.put_labels(["foo#{i}"]) }
|
|
}
|
|
}
|
|
Resolv::DNS::Message::MessageDecoder.new(m.to_s) {|msg|
|
|
2.times {
|
|
n.times {|i|
|
|
assert_equal(["foo#{i}"], msg.get_labels.map {|label| label.to_s })
|
|
}
|
|
}
|
|
}
|
|
assert_operator(2**14, :<, m.to_s.length)
|
|
end
|
|
|
|
def assert_no_fd_leak
|
|
socket = assert_throw(self) do |tag|
|
|
Resolv::DNS.stub(:bind_random_port, ->(s, *) {throw(tag, s)}) do
|
|
yield.getname("8.8.8.8")
|
|
end
|
|
end
|
|
|
|
assert_predicate(socket, :closed?, "file descriptor leaked")
|
|
end
|
|
|
|
def test_no_fd_leak_connected
|
|
assert_no_fd_leak {Resolv::DNS.new(nameserver_port: [['127.0.0.1', 53]])}
|
|
end
|
|
|
|
def test_no_fd_leak_unconnected
|
|
assert_no_fd_leak {Resolv::DNS.new}
|
|
end
|
|
|
|
def test_each_name
|
|
dns = Resolv::DNS.new
|
|
def dns.each_resource(name, typeclass)
|
|
yield typeclass.new(name)
|
|
end
|
|
|
|
dns.each_name('127.0.0.1') do |ptr|
|
|
assert_equal('1.0.0.127.in-addr.arpa', ptr.to_s)
|
|
end
|
|
dns.each_name(Resolv::IPv4.create('127.0.0.1')) do |ptr|
|
|
assert_equal('1.0.0.127.in-addr.arpa', ptr.to_s)
|
|
end
|
|
dns.each_name('::1') do |ptr|
|
|
assert_equal('1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa', ptr.to_s)
|
|
end
|
|
dns.each_name(Resolv::IPv6.create('::1')) do |ptr|
|
|
assert_equal('1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.ip6.arpa', ptr.to_s)
|
|
end
|
|
dns.each_name(Resolv::DNS::Name.create('1.0.0.127.in-addr.arpa.')) do |ptr|
|
|
assert_equal('1.0.0.127.in-addr.arpa', ptr.to_s)
|
|
end
|
|
assert_raise(Resolv::ResolvError) { dns.each_name('example.com') }
|
|
end
|
|
end
|