├── Gemfile ├── .gitignore ├── test ├── lib │ ├── helper.rb │ ├── find_executable.rb │ ├── envutil.rb │ └── core_assertions.rb └── resolv │ ├── test_resource.rb │ ├── test_mdns.rb │ ├── test_addr.rb │ └── test_dns.rb ├── bin ├── setup └── console ├── Rakefile ├── .github └── workflows │ └── test.yml ├── resolv.gemspec ├── LICENSE.txt ├── README.md └── lib └── resolv.rb /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gem "rake" 4 | gem "test-unit" 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | -------------------------------------------------------------------------------- /test/lib/helper.rb: -------------------------------------------------------------------------------- 1 | require "test/unit" 2 | require_relative "core_assertions" 3 | 4 | Test::Unit::TestCase.include Test::Unit::CoreAssertions 5 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test/lib" 6 | t.ruby_opts << "-rhelper" 7 | t.test_files = FileList["test/**/test_*.rb"] 8 | end 9 | 10 | task :default => :test 11 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "resolv" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start(__FILE__) 15 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | name: build (${{ matrix.ruby }} / ${{ matrix.os }}) 8 | strategy: 9 | matrix: 10 | ruby: [ '3.0', 2.7, 2.6, 2.5, head ] 11 | os: [ ubuntu-latest, macos-latest ] 12 | runs-on: ${{ matrix.os }} 13 | steps: 14 | - uses: actions/checkout@v2 15 | - name: Set up Ruby 16 | uses: ruby/setup-ruby@v1 17 | with: 18 | ruby-version: ${{ matrix.ruby }} 19 | - name: Install dependencies 20 | run: bundle install 21 | - name: Run test 22 | run: rake test 23 | -------------------------------------------------------------------------------- /test/lib/find_executable.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require "rbconfig" 3 | 4 | module EnvUtil 5 | def find_executable(cmd, *args) 6 | exts = RbConfig::CONFIG["EXECUTABLE_EXTS"].split | [RbConfig::CONFIG["EXEEXT"]] 7 | ENV["PATH"].split(File::PATH_SEPARATOR).each do |path| 8 | next if path.empty? 9 | path = File.join(path, cmd) 10 | exts.each do |ext| 11 | cmdline = [path + ext, *args] 12 | begin 13 | return cmdline if yield(IO.popen(cmdline, "r", err: [:child, :out], &:read)) 14 | rescue 15 | next 16 | end 17 | end 18 | end 19 | nil 20 | end 21 | module_function :find_executable 22 | end 23 | -------------------------------------------------------------------------------- /test/resolv/test_resource.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require 'test/unit' 3 | require 'resolv' 4 | 5 | class TestResolvResource < Test::Unit::TestCase 6 | def setup 7 | address = "192.168.0.1" 8 | @name1 = Resolv::DNS::Resource::IN::A.new(address) 9 | @name1.instance_variable_set(:@ttl, 100) 10 | @name2 = Resolv::DNS::Resource::IN::A.new(address) 11 | end 12 | 13 | def test_equality 14 | bug10857 = '[ruby-core:68128] [Bug #10857]' 15 | assert_equal(@name1, @name2, bug10857) 16 | end 17 | 18 | def test_hash 19 | bug10857 = '[ruby-core:68128] [Bug #10857]' 20 | assert_equal(@name1.hash, @name2.hash, bug10857) 21 | end 22 | 23 | def test_coord 24 | Resolv::LOC::Coord.create('1 2 1.1 N') 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /test/resolv/test_mdns.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require 'test/unit' 3 | require 'resolv' 4 | 5 | class TestResolvMDNS < Test::Unit::TestCase 6 | def test_mdns_each_address 7 | mdns = Resolv::MDNS.new 8 | def mdns.each_resource(name, typeclass) 9 | if typeclass == Resolv::DNS::Resource::IN::A 10 | yield typeclass.new("127.0.0.1") 11 | else 12 | yield typeclass.new("::1") 13 | end 14 | end 15 | addrs = mdns.__send__(:use_ipv6?) ? ["127.0.0.1", "::1"] : ["127.0.0.1"] 16 | [ 17 | ["example.com", []], 18 | ["foo.local", addrs], 19 | ].each do |name, expect| 20 | results = [] 21 | mdns.each_address(name) do |result| 22 | results << result.to_s 23 | end 24 | assert_equal expect, results.sort, "GH-1484" 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /resolv.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |spec| 2 | spec.name = "resolv" 3 | spec.version = "0.2.0" 4 | spec.authors = ["Tanaka Akira"] 5 | spec.email = ["akr@fsij.org"] 6 | 7 | spec.summary = %q{Thread-aware DNS resolver library in Ruby.} 8 | spec.description = %q{Thread-aware DNS resolver library in Ruby.} 9 | spec.homepage = "https://github.com/ruby/resolv" 10 | spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0") 11 | spec.licenses = ["Ruby", "BSD-2-Clause"] 12 | 13 | spec.metadata["homepage_uri"] = spec.homepage 14 | spec.metadata["source_code_uri"] = spec.homepage 15 | 16 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 17 | `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) } 18 | end 19 | spec.bindir = "exe" 20 | spec.executables = [] 21 | spec.require_paths = ["lib"] 22 | end 23 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 16 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 18 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 19 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 20 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 21 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 22 | SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Resolv 2 | 3 | Resolv is a thread-aware DNS resolver library written in Ruby. Resolv can 4 | handle multiple DNS requests concurrently without blocking the entire Ruby 5 | interpreter. 6 | 7 | ## Installation 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | ```ruby 12 | gem 'resolv' 13 | ``` 14 | 15 | And then execute: 16 | 17 | $ bundle install 18 | 19 | Or install it yourself as: 20 | 21 | $ gem install resolv 22 | 23 | ## Usage 24 | 25 | 26 | ```ruby 27 | p Resolv.getaddress "www.ruby-lang.org" 28 | p Resolv.getname "210.251.121.214" 29 | 30 | Resolv::DNS.open do |dns| 31 | ress = dns.getresources "www.ruby-lang.org", Resolv::DNS::Resource::IN::A 32 | p ress.map(&:address) 33 | ress = dns.getresources "ruby-lang.org", Resolv::DNS::Resource::IN::MX 34 | p ress.map { |r| [r.exchange.to_s, r.preference] } 35 | end 36 | ``` 37 | 38 | ## Development 39 | 40 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 41 | 42 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 43 | 44 | ## Contributing 45 | 46 | Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/resolv. 47 | 48 | -------------------------------------------------------------------------------- /test/resolv/test_addr.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: false 2 | require 'test/unit' 3 | require 'resolv' 4 | require 'socket' 5 | require 'tempfile' 6 | 7 | class TestResolvAddr < Test::Unit::TestCase 8 | def test_invalid_ipv4_address 9 | assert_not_match(Resolv::IPv4::Regex, "1.2.3.256", "[ruby-core:29501]") 10 | 1000.times {|i| 11 | if i < 256 12 | assert_match(Resolv::IPv4::Regex, "#{i}.#{i}.#{i}.#{i}") 13 | else 14 | assert_not_match(Resolv::IPv4::Regex, "#{i}.#{i}.#{i}.#{i}") 15 | end 16 | } 17 | end 18 | 19 | def test_valid_ipv6_link_local_address 20 | bug17112 = "[ruby-core:99539]" 21 | assert_not_match(Resolv::IPv6::Regex, "fe80::1%", bug17112) 22 | assert_not_match(Resolv::IPv6::Regex, "fe80:2:3:4:5:6:7:8%", bug17112) 23 | assert_not_match(Resolv::IPv6::Regex, "fe90::1%em1", bug17112) 24 | assert_not_match(Resolv::IPv6::Regex, "1:2:3:4:5:6:7:8%em1", bug17112) 25 | assert_match(Resolv::IPv6::Regex, "fe80:2:3:4:5:6:7:8%em1", bug17112) 26 | assert_match(Resolv::IPv6::Regex, "fe80::20d:3aff:fe7d:9760%eth0", bug17112) 27 | assert_match(Resolv::IPv6::Regex, "fe80::1%em1", bug17112) 28 | assert_match(Resolv::IPv6::Regex, "FE80:2:3:4:5:6:7:8%EM1", bug17112) 29 | assert_match(Resolv::IPv6::Regex, "FE80::20D:3AFF:FE7D:9760%ETH0", bug17112) 30 | assert_match(Resolv::IPv6::Regex, "FE80::1%EM1", bug17112) 31 | end 32 | 33 | def test_valid_socket_ip_address_list 34 | Socket.ip_address_list.each do |addr| 35 | ip = addr.ip_address 36 | assert_match(Resolv::AddressRegex, ip) 37 | assert_equal(ip, Resolv.getaddress(ip)) 38 | end 39 | end 40 | 41 | def test_invalid_byte_comment 42 | bug9273 = '[ruby-core:59239] [Bug #9273]' 43 | Tempfile.create('resolv_test_addr_') do |tmpfile| 44 | tmpfile.print("\xff\x00\x40") 45 | tmpfile.close 46 | hosts = Resolv::Hosts.new(tmpfile.path) 47 | assert_nothing_raised(ArgumentError, bug9273) do 48 | hosts.each_address("") {break} 49 | end 50 | end 51 | end 52 | 53 | def test_hosts_by_command 54 | Dir.mktmpdir do |dir| 55 | Dir.chdir(dir) do 56 | hosts = Resolv::Hosts.new("|echo error") 57 | assert_raise(Errno::ENOENT, Errno::EINVAL) do 58 | hosts.each_name("") {} 59 | end 60 | end 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /test/lib/envutil.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: us-ascii -*- 2 | # frozen_string_literal: true 3 | require "open3" 4 | require "timeout" 5 | require_relative "find_executable" 6 | begin 7 | require 'rbconfig' 8 | rescue LoadError 9 | end 10 | begin 11 | require "rbconfig/sizeof" 12 | rescue LoadError 13 | end 14 | 15 | module EnvUtil 16 | def rubybin 17 | if ruby = ENV["RUBY"] 18 | return ruby 19 | end 20 | ruby = "ruby" 21 | exeext = RbConfig::CONFIG["EXEEXT"] 22 | rubyexe = (ruby + exeext if exeext and !exeext.empty?) 23 | 3.times do 24 | if File.exist? ruby and File.executable? ruby and !File.directory? ruby 25 | return File.expand_path(ruby) 26 | end 27 | if rubyexe and File.exist? rubyexe and File.executable? rubyexe 28 | return File.expand_path(rubyexe) 29 | end 30 | ruby = File.join("..", ruby) 31 | end 32 | if defined?(RbConfig.ruby) 33 | RbConfig.ruby 34 | else 35 | "ruby" 36 | end 37 | end 38 | module_function :rubybin 39 | 40 | LANG_ENVS = %w"LANG LC_ALL LC_CTYPE" 41 | 42 | DEFAULT_SIGNALS = Signal.list 43 | DEFAULT_SIGNALS.delete("TERM") if /mswin|mingw/ =~ RUBY_PLATFORM 44 | 45 | RUBYLIB = ENV["RUBYLIB"] 46 | 47 | class << self 48 | attr_accessor :timeout_scale 49 | attr_reader :original_internal_encoding, :original_external_encoding, 50 | :original_verbose 51 | 52 | def capture_global_values 53 | @original_internal_encoding = Encoding.default_internal 54 | @original_external_encoding = Encoding.default_external 55 | @original_verbose = $VERBOSE 56 | end 57 | end 58 | 59 | def apply_timeout_scale(t) 60 | if scale = EnvUtil.timeout_scale 61 | t * scale 62 | else 63 | t 64 | end 65 | end 66 | module_function :apply_timeout_scale 67 | 68 | def timeout(sec, klass = nil, message = nil, &blk) 69 | return yield(sec) if sec == nil or sec.zero? 70 | sec = apply_timeout_scale(sec) 71 | Timeout.timeout(sec, klass, message, &blk) 72 | end 73 | module_function :timeout 74 | 75 | def terminate(pid, signal = :TERM, pgroup = nil, reprieve = 1) 76 | reprieve = apply_timeout_scale(reprieve) if reprieve 77 | 78 | signals = Array(signal).select do |sig| 79 | DEFAULT_SIGNALS[sig.to_s] or 80 | DEFAULT_SIGNALS[Signal.signame(sig)] rescue false 81 | end 82 | signals |= [:ABRT, :KILL] 83 | case pgroup 84 | when 0, true 85 | pgroup = -pid 86 | when nil, false 87 | pgroup = pid 88 | end 89 | while signal = signals.shift 90 | begin 91 | Process.kill signal, pgroup 92 | rescue Errno::EINVAL 93 | next 94 | rescue Errno::ESRCH 95 | break 96 | end 97 | if signals.empty? or !reprieve 98 | Process.wait(pid) 99 | else 100 | begin 101 | Timeout.timeout(reprieve) {Process.wait(pid)} 102 | rescue Timeout::Error 103 | end 104 | end 105 | end 106 | $? 107 | end 108 | module_function :terminate 109 | 110 | def invoke_ruby(args, stdin_data = "", capture_stdout = false, capture_stderr = false, 111 | encoding: nil, timeout: 10, reprieve: 1, timeout_error: Timeout::Error, 112 | stdout_filter: nil, stderr_filter: nil, 113 | signal: :TERM, 114 | rubybin: EnvUtil.rubybin, precommand: nil, 115 | **opt) 116 | timeout = apply_timeout_scale(timeout) 117 | 118 | in_c, in_p = IO.pipe 119 | out_p, out_c = IO.pipe if capture_stdout 120 | err_p, err_c = IO.pipe if capture_stderr && capture_stderr != :merge_to_stdout 121 | opt[:in] = in_c 122 | opt[:out] = out_c if capture_stdout 123 | opt[:err] = capture_stderr == :merge_to_stdout ? out_c : err_c if capture_stderr 124 | if encoding 125 | out_p.set_encoding(encoding) if out_p 126 | err_p.set_encoding(encoding) if err_p 127 | end 128 | c = "C" 129 | child_env = {} 130 | LANG_ENVS.each {|lc| child_env[lc] = c} 131 | if Array === args and Hash === args.first 132 | child_env.update(args.shift) 133 | end 134 | if RUBYLIB and lib = child_env["RUBYLIB"] 135 | child_env["RUBYLIB"] = [lib, RUBYLIB].join(File::PATH_SEPARATOR) 136 | end 137 | args = [args] if args.kind_of?(String) 138 | pid = spawn(child_env, *precommand, rubybin, *args, **opt) 139 | in_c.close 140 | out_c&.close 141 | out_c = nil 142 | err_c&.close 143 | err_c = nil 144 | if block_given? 145 | return yield in_p, out_p, err_p, pid 146 | else 147 | th_stdout = Thread.new { out_p.read } if capture_stdout 148 | th_stderr = Thread.new { err_p.read } if capture_stderr && capture_stderr != :merge_to_stdout 149 | in_p.write stdin_data.to_str unless stdin_data.empty? 150 | in_p.close 151 | if (!th_stdout || th_stdout.join(timeout)) && (!th_stderr || th_stderr.join(timeout)) 152 | timeout_error = nil 153 | else 154 | status = terminate(pid, signal, opt[:pgroup], reprieve) 155 | terminated = Time.now 156 | end 157 | stdout = th_stdout.value if capture_stdout 158 | stderr = th_stderr.value if capture_stderr && capture_stderr != :merge_to_stdout 159 | out_p.close if capture_stdout 160 | err_p.close if capture_stderr && capture_stderr != :merge_to_stdout 161 | status ||= Process.wait2(pid)[1] 162 | stdout = stdout_filter.call(stdout) if stdout_filter 163 | stderr = stderr_filter.call(stderr) if stderr_filter 164 | if timeout_error 165 | bt = caller_locations 166 | msg = "execution of #{bt.shift.label} expired timeout (#{timeout} sec)" 167 | msg = failure_description(status, terminated, msg, [stdout, stderr].join("\n")) 168 | raise timeout_error, msg, bt.map(&:to_s) 169 | end 170 | return stdout, stderr, status 171 | end 172 | ensure 173 | [th_stdout, th_stderr].each do |th| 174 | th.kill if th 175 | end 176 | [in_c, in_p, out_c, out_p, err_c, err_p].each do |io| 177 | io&.close 178 | end 179 | [th_stdout, th_stderr].each do |th| 180 | th.join if th 181 | end 182 | end 183 | module_function :invoke_ruby 184 | 185 | alias rubyexec invoke_ruby 186 | class << self 187 | alias rubyexec invoke_ruby 188 | end 189 | 190 | def verbose_warning 191 | class << (stderr = "".dup) 192 | alias write concat 193 | def flush; end 194 | end 195 | stderr, $stderr = $stderr, stderr 196 | $VERBOSE = true 197 | yield stderr 198 | return $stderr 199 | ensure 200 | stderr, $stderr = $stderr, stderr 201 | $VERBOSE = EnvUtil.original_verbose 202 | end 203 | module_function :verbose_warning 204 | 205 | def default_warning 206 | $VERBOSE = false 207 | yield 208 | ensure 209 | $VERBOSE = EnvUtil.original_verbose 210 | end 211 | module_function :default_warning 212 | 213 | def suppress_warning 214 | $VERBOSE = nil 215 | yield 216 | ensure 217 | $VERBOSE = EnvUtil.original_verbose 218 | end 219 | module_function :suppress_warning 220 | 221 | def under_gc_stress(stress = true) 222 | stress, GC.stress = GC.stress, stress 223 | yield 224 | ensure 225 | GC.stress = stress 226 | end 227 | module_function :under_gc_stress 228 | 229 | def with_default_external(enc) 230 | suppress_warning { Encoding.default_external = enc } 231 | yield 232 | ensure 233 | suppress_warning { Encoding.default_external = EnvUtil.original_external_encoding } 234 | end 235 | module_function :with_default_external 236 | 237 | def with_default_internal(enc) 238 | suppress_warning { Encoding.default_internal = enc } 239 | yield 240 | ensure 241 | suppress_warning { Encoding.default_internal = EnvUtil.original_internal_encoding } 242 | end 243 | module_function :with_default_internal 244 | 245 | def labeled_module(name, &block) 246 | Module.new do 247 | singleton_class.class_eval { 248 | define_method(:to_s) {name} 249 | alias inspect to_s 250 | alias name to_s 251 | } 252 | class_eval(&block) if block 253 | end 254 | end 255 | module_function :labeled_module 256 | 257 | def labeled_class(name, superclass = Object, &block) 258 | Class.new(superclass) do 259 | singleton_class.class_eval { 260 | define_method(:to_s) {name} 261 | alias inspect to_s 262 | alias name to_s 263 | } 264 | class_eval(&block) if block 265 | end 266 | end 267 | module_function :labeled_class 268 | 269 | if /darwin/ =~ RUBY_PLATFORM 270 | DIAGNOSTIC_REPORTS_PATH = File.expand_path("~/Library/Logs/DiagnosticReports") 271 | DIAGNOSTIC_REPORTS_TIMEFORMAT = '%Y-%m-%d-%H%M%S' 272 | @ruby_install_name = RbConfig::CONFIG['RUBY_INSTALL_NAME'] 273 | 274 | def self.diagnostic_reports(signame, pid, now) 275 | return unless %w[ABRT QUIT SEGV ILL TRAP].include?(signame) 276 | cmd = File.basename(rubybin) 277 | cmd = @ruby_install_name if "ruby-runner#{RbConfig::CONFIG["EXEEXT"]}" == cmd 278 | path = DIAGNOSTIC_REPORTS_PATH 279 | timeformat = DIAGNOSTIC_REPORTS_TIMEFORMAT 280 | pat = "#{path}/#{cmd}_#{now.strftime(timeformat)}[-_]*.crash" 281 | first = true 282 | 30.times do 283 | first ? (first = false) : sleep(0.1) 284 | Dir.glob(pat) do |name| 285 | log = File.read(name) rescue next 286 | if /\AProcess:\s+#{cmd} \[#{pid}\]$/ =~ log 287 | File.unlink(name) 288 | File.unlink("#{path}/.#{File.basename(name)}.plist") rescue nil 289 | return log 290 | end 291 | end 292 | end 293 | nil 294 | end 295 | else 296 | def self.diagnostic_reports(signame, pid, now) 297 | end 298 | end 299 | 300 | def self.failure_description(status, now, message = "", out = "") 301 | pid = status.pid 302 | if signo = status.termsig 303 | signame = Signal.signame(signo) 304 | sigdesc = "signal #{signo}" 305 | end 306 | log = diagnostic_reports(signame, pid, now) 307 | if signame 308 | sigdesc = "SIG#{signame} (#{sigdesc})" 309 | end 310 | if status.coredump? 311 | sigdesc = "#{sigdesc} (core dumped)" 312 | end 313 | full_message = ''.dup 314 | message = message.call if Proc === message 315 | if message and !message.empty? 316 | full_message << message << "\n" 317 | end 318 | full_message << "pid #{pid}" 319 | full_message << " exit #{status.exitstatus}" if status.exited? 320 | full_message << " killed by #{sigdesc}" if sigdesc 321 | if out and !out.empty? 322 | full_message << "\n" << out.b.gsub(/^/, '| ') 323 | full_message.sub!(/(? ['127.0.0.1'], :ndots => 2 31 | assert conf.single? 32 | 33 | candidates = [] 34 | conf.resolv('example.com') { |candidate, *args| 35 | candidates << candidate 36 | raise Resolv::DNS::Config::NXDomain 37 | } 38 | n = Resolv::DNS::Name.create 'example.com.' 39 | assert_equal n, candidates.last 40 | end 41 | 42 | def test_query_ipv4_address 43 | begin 44 | OpenSSL 45 | rescue LoadError 46 | skip 'autoload problem. see [ruby-dev:45021][Bug #5786]' 47 | end if defined?(OpenSSL) 48 | 49 | with_udp('127.0.0.1', 0) {|u| 50 | _, server_port, _, server_address = u.addr 51 | begin 52 | client_thread = Thread.new { 53 | Resolv::DNS.open(:nameserver_port => [[server_address, server_port]]) {|dns| 54 | dns.getresources("foo.example.org", Resolv::DNS::Resource::IN::A) 55 | } 56 | } 57 | server_thread = Thread.new { 58 | msg, (_, client_port, _, client_address) = Timeout.timeout(5) {u.recvfrom(4096)} 59 | id, word2, qdcount, ancount, nscount, arcount = msg.unpack("nnnnnn") 60 | qr = (word2 & 0x8000) >> 15 61 | opcode = (word2 & 0x7800) >> 11 62 | aa = (word2 & 0x0400) >> 10 63 | tc = (word2 & 0x0200) >> 9 64 | rd = (word2 & 0x0100) >> 8 65 | ra = (word2 & 0x0080) >> 7 66 | z = (word2 & 0x0070) >> 4 67 | rcode = word2 & 0x000f 68 | rest = msg[12..-1] 69 | assert_equal(0, qr) # 0:query 1:response 70 | assert_equal(0, opcode) # 0:QUERY 1:IQUERY 2:STATUS 71 | assert_equal(0, aa) # Authoritative Answer 72 | assert_equal(0, tc) # TrunCation 73 | assert_equal(1, rd) # Recursion Desired 74 | assert_equal(0, ra) # Recursion Available 75 | assert_equal(0, z) # Reserved for future use 76 | assert_equal(0, rcode) # 0:No-error 1:Format-error 2:Server-failure 3:Name-Error 4:Not-Implemented 5:Refused 77 | assert_equal(1, qdcount) # number of entries in the question section. 78 | assert_equal(0, ancount) # number of entries in the answer section. 79 | assert_equal(0, nscount) # number of entries in the authority records section. 80 | assert_equal(0, arcount) # number of entries in the additional records section. 81 | name = [3, "foo", 7, "example", 3, "org", 0].pack("Ca*Ca*Ca*C") 82 | assert_operator(rest, :start_with?, name) 83 | rest = rest[name.length..-1] 84 | assert_equal(4, rest.length) 85 | qtype, _ = rest.unpack("nn") 86 | assert_equal(1, qtype) # A 87 | assert_equal(1, qtype) # IN 88 | id = id 89 | qr = 1 90 | opcode = opcode 91 | aa = 0 92 | tc = 0 93 | rd = rd 94 | ra = 1 95 | z = 0 96 | rcode = 0 97 | qdcount = 0 98 | ancount = 1 99 | nscount = 0 100 | arcount = 0 101 | word2 = (qr << 15) | 102 | (opcode << 11) | 103 | (aa << 10) | 104 | (tc << 9) | 105 | (rd << 8) | 106 | (ra << 7) | 107 | (z << 4) | 108 | rcode 109 | msg = [id, word2, qdcount, ancount, nscount, arcount].pack("nnnnnn") 110 | type = 1 111 | klass = 1 112 | ttl = 3600 113 | rdlength = 4 114 | rdata = [192,0,2,1].pack("CCCC") # 192.0.2.1 (TEST-NET address) RFC 3330 115 | rr = [name, type, klass, ttl, rdlength, rdata].pack("a*nnNna*") 116 | msg << rr 117 | u.send(msg, 0, client_address, client_port) 118 | } 119 | result, _ = assert_join_threads([client_thread, server_thread]) 120 | assert_instance_of(Array, result) 121 | assert_equal(1, result.length) 122 | rr = result[0] 123 | assert_instance_of(Resolv::DNS::Resource::IN::A, rr) 124 | assert_instance_of(Resolv::IPv4, rr.address) 125 | assert_equal("192.0.2.1", rr.address.to_s) 126 | assert_equal(3600, rr.ttl) 127 | end 128 | } 129 | end 130 | 131 | def test_query_ipv4_duplicate_responses 132 | begin 133 | OpenSSL 134 | rescue LoadError 135 | skip 'autoload problem. see [ruby-dev:45021][Bug #5786]' 136 | end if defined?(OpenSSL) 137 | 138 | with_udp('127.0.0.1', 0) {|u| 139 | _, server_port, _, server_address = u.addr 140 | begin 141 | client_thread = Thread.new { 142 | Resolv::DNS.open(:nameserver_port => [[server_address, server_port]], :search => ['bad1.com', 'bad2.com', 'good.com'], ndots: 5) {|dns| 143 | dns.getaddress("example") 144 | } 145 | } 146 | server_thread = Thread.new { 147 | 3.times do 148 | msg, (_, client_port, _, client_address) = Timeout.timeout(5) {u.recvfrom(4096)} 149 | id, flags, qdcount, ancount, nscount, arcount = msg.unpack("nnnnnn") 150 | 151 | qr = (flags & 0x8000) >> 15 152 | opcode = (flags & 0x7800) >> 11 153 | aa = (flags & 0x0400) >> 10 154 | tc = (flags & 0x0200) >> 9 155 | rd = (flags & 0x0100) >> 8 156 | ra = (flags & 0x0080) >> 7 157 | z = (flags & 0x0070) >> 4 158 | rcode = flags & 0x000f 159 | _rest = msg[12..-1] 160 | 161 | questions = msg.bytes[12..-1] 162 | labels = [] 163 | idx = 0 164 | while idx < questions.length-5 165 | size = questions[idx] 166 | labels << questions[idx+1..idx+size].pack('c*') 167 | idx += size+1 168 | end 169 | hostname = labels.join('.') 170 | 171 | if hostname == "example.good.com" 172 | id = id 173 | qr = 1 174 | opcode = opcode 175 | aa = 0 176 | tc = 0 177 | rd = rd 178 | ra = 1 179 | z = 0 180 | rcode = 0 181 | qdcount = 1 182 | ancount = 1 183 | nscount = 0 184 | arcount = 0 185 | word2 = (qr << 15) | 186 | (opcode << 11) | 187 | (aa << 10) | 188 | (tc << 9) | 189 | (rd << 8) | 190 | (ra << 7) | 191 | (z << 4) | 192 | rcode 193 | msg = [id, word2, qdcount, ancount, nscount, arcount].pack("nnnnnn") 194 | msg << questions.pack('c*') 195 | type = 1 196 | klass = 1 197 | ttl = 3600 198 | rdlength = 4 199 | rdata = [52,0,2,1].pack("CCCC") 200 | rr = [0xc00c, type, klass, ttl, rdlength, rdata].pack("nnnNna*") 201 | msg << rr 202 | rdata = [52,0,2,2].pack("CCCC") 203 | rr = [0xc00c, type, klass, ttl, rdlength, rdata].pack("nnnNna*") 204 | msg << rr 205 | 206 | u.send(msg, 0, client_address, client_port) 207 | else 208 | id = id 209 | qr = 1 210 | opcode = opcode 211 | aa = 0 212 | tc = 0 213 | rd = rd 214 | ra = 1 215 | z = 0 216 | rcode = 3 217 | qdcount = 1 218 | ancount = 0 219 | nscount = 0 220 | arcount = 0 221 | word2 = (qr << 15) | 222 | (opcode << 11) | 223 | (aa << 10) | 224 | (tc << 9) | 225 | (rd << 8) | 226 | (ra << 7) | 227 | (z << 4) | 228 | rcode 229 | msg = [id, word2, qdcount, ancount, nscount, arcount].pack("nnnnnn") 230 | msg << questions.pack('c*') 231 | 232 | u.send(msg, 0, client_address, client_port) 233 | u.send(msg, 0, client_address, client_port) 234 | end 235 | end 236 | } 237 | result, _ = assert_join_threads([client_thread, server_thread]) 238 | assert_instance_of(Resolv::IPv4, result) 239 | assert_equal("52.0.2.1", result.to_s) 240 | end 241 | } 242 | end 243 | 244 | def test_query_ipv4_address_timeout 245 | with_udp('127.0.0.1', 0) {|u| 246 | _, port , _, host = u.addr 247 | start = nil 248 | rv = Resolv::DNS.open(:nameserver_port => [[host, port]]) {|dns| 249 | dns.timeouts = 0.1 250 | start = Time.now 251 | dns.getresources("foo.example.org", Resolv::DNS::Resource::IN::A) 252 | } 253 | t2 = Time.now 254 | diff = t2 - start 255 | assert rv.empty?, "unexpected: #{rv.inspect} (expected empty)" 256 | assert_operator 0.1, :<=, diff 257 | 258 | rv = Resolv::DNS.open(:nameserver_port => [[host, port]]) {|dns| 259 | dns.timeouts = [ 0.1, 0.2 ] 260 | start = Time.now 261 | dns.getresources("foo.example.org", Resolv::DNS::Resource::IN::A) 262 | } 263 | t2 = Time.now 264 | diff = t2 - start 265 | assert rv.empty?, "unexpected: #{rv.inspect} (expected empty)" 266 | assert_operator 0.3, :<=, diff 267 | } 268 | end 269 | 270 | def test_no_server 271 | u = UDPSocket.new 272 | u.bind("127.0.0.1", 0) 273 | _, port, _, host = u.addr 274 | u.close 275 | # A race condition here. 276 | # Another program may use the port. 277 | # But no way to prevent it. 278 | begin 279 | Timeout.timeout(5) do 280 | Resolv::DNS.open(:nameserver_port => [[host, port]]) {|dns| 281 | assert_equal([], dns.getresources("test-no-server.example.org", Resolv::DNS::Resource::IN::A)) 282 | } 283 | end 284 | rescue Timeout::Error 285 | if RUBY_PLATFORM.match?(/mingw/) 286 | # cannot repo locally 287 | skip 'Timeout Error on MinGW CI' 288 | else 289 | raise Timeout::Error 290 | end 291 | end 292 | end 293 | 294 | def test_invalid_byte_comment 295 | bug9273 = '[ruby-core:59239] [Bug #9273]' 296 | Tempfile.create('resolv_test_dns_') do |tmpfile| 297 | tmpfile.print("\xff\x00\x40") 298 | tmpfile.close 299 | assert_nothing_raised(ArgumentError, bug9273) do 300 | Resolv::DNS::Config.parse_resolv_conf(tmpfile.path) 301 | end 302 | end 303 | end 304 | 305 | def test_resolv_conf_by_command 306 | Dir.mktmpdir do |dir| 307 | Dir.chdir(dir) do 308 | assert_raise(Errno::ENOENT, Errno::EINVAL) do 309 | Resolv::DNS::Config.parse_resolv_conf("|echo foo") 310 | end 311 | end 312 | end 313 | end 314 | 315 | def test_dots_diffences 316 | name1 = Resolv::DNS::Name.create("example.org") 317 | name2 = Resolv::DNS::Name.create("ex.ampl.eo.rg") 318 | assert_not_equal(name1, name2, "different dots") 319 | end 320 | 321 | def test_case_insensitive_name 322 | bug10550 = '[ruby-core:66498] [Bug #10550]' 323 | lower = Resolv::DNS::Name.create("ruby-lang.org") 324 | upper = Resolv::DNS::Name.create("Ruby-Lang.org") 325 | assert_equal(lower, upper, bug10550) 326 | end 327 | 328 | def test_ipv6_name 329 | addr = Resolv::IPv6.new("\0"*16) 330 | labels = addr.to_name.to_a 331 | expected = (['0'] * 32 + ['ip6', 'arpa']).map {|label| Resolv::DNS::Label::Str.new(label) } 332 | assert_equal(expected, labels) 333 | end 334 | 335 | def test_ipv6_create 336 | ref = '[Bug #11910] [ruby-core:72559]' 337 | assert_instance_of Resolv::IPv6, Resolv::IPv6.create('::1'), ref 338 | assert_instance_of Resolv::IPv6, Resolv::IPv6.create('::1:127.0.0.1'), ref 339 | end 340 | 341 | def test_ipv6_to_s 342 | test_cases = [ 343 | ["2001::abcd:abcd:abcd", "2001::ABcd:abcd:ABCD"], 344 | ["2001:db8::1", "2001:db8::0:1"], 345 | ["::", "0:0:0:0:0:0:0:0"], 346 | ["2001::", "2001::0"], 347 | ["2001:db8::1:1:1:1:1", "2001:db8:0:1:1:1:1:1"], 348 | ["1::1:0:0:0:1", "1:0:0:1:0:0:0:1"], 349 | ["1::1:0:0:1", "1:0:0:0:1:0:0:1"], 350 | ] 351 | 352 | test_cases.each do |expected, ipv6| 353 | assert_equal expected, Resolv::IPv6.create(ipv6).to_s 354 | end 355 | end 356 | 357 | def test_ipv6_should_be_16 358 | ref = '[rubygems:1626]' 359 | 360 | broken_message = 361 | "\0\0\0\0\0\0\0\0\0\0\0\1" \ 362 | "\x03ns2\bdnsimple\x03com\x00" \ 363 | "\x00\x1C\x00\x01\x00\x02OD" \ 364 | "\x00\x10$\x00\xCB\x00 I\x00\x01\x00\x00\x00\x00" 365 | 366 | e = assert_raise_with_message(Resolv::DNS::DecodeError, /IPv6 address must be 16 bytes/, ref) do 367 | Resolv::DNS::Message.decode broken_message 368 | end 369 | assert_kind_of(ArgumentError, e.cause) 370 | end 371 | 372 | def test_too_big_label_address 373 | n = 2000 374 | m = Resolv::DNS::Message::MessageEncoder.new {|msg| 375 | 2.times { 376 | n.times {|i| msg.put_labels(["foo#{i}"]) } 377 | } 378 | } 379 | Resolv::DNS::Message::MessageDecoder.new(m.to_s) {|msg| 380 | 2.times { 381 | n.times {|i| 382 | assert_equal(["foo#{i}"], msg.get_labels.map {|label| label.to_s }) 383 | } 384 | } 385 | } 386 | assert_operator(2**14, :<, m.to_s.length) 387 | end 388 | 389 | def assert_no_fd_leak 390 | socket = assert_throw(self) do |tag| 391 | Resolv::DNS.stub(:bind_random_port, ->(s, *) {throw(tag, s)}) do 392 | yield.getname("8.8.8.8") 393 | end 394 | end 395 | 396 | assert_predicate(socket, :closed?, "file descriptor leaked") 397 | end 398 | 399 | def test_no_fd_leak_connected 400 | assert_no_fd_leak {Resolv::DNS.new(nameserver_port: [['127.0.0.1', 53]])} 401 | end 402 | 403 | def test_no_fd_leak_unconnected 404 | assert_no_fd_leak {Resolv::DNS.new} 405 | end 406 | 407 | def test_each_name 408 | dns = Resolv::DNS.new 409 | def dns.each_resource(name, typeclass) 410 | yield typeclass.new(name) 411 | end 412 | 413 | dns.each_name('127.0.0.1') do |ptr| 414 | assert_equal('1.0.0.127.in-addr.arpa', ptr.to_s) 415 | end 416 | dns.each_name(Resolv::IPv4.create('127.0.0.1')) do |ptr| 417 | assert_equal('1.0.0.127.in-addr.arpa', ptr.to_s) 418 | end 419 | dns.each_name('::1') do |ptr| 420 | 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) 421 | end 422 | dns.each_name(Resolv::IPv6.create('::1')) do |ptr| 423 | 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) 424 | end 425 | dns.each_name(Resolv::DNS::Name.create('1.0.0.127.in-addr.arpa.')) do |ptr| 426 | assert_equal('1.0.0.127.in-addr.arpa', ptr.to_s) 427 | end 428 | assert_raise(Resolv::ResolvError) { dns.each_name('example.com') } 429 | end 430 | end 431 | -------------------------------------------------------------------------------- /test/lib/core_assertions.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Test 4 | module Unit 5 | module Assertions 6 | def _assertions= n # :nodoc: 7 | @_assertions = n 8 | end 9 | 10 | def _assertions # :nodoc: 11 | @_assertions ||= 0 12 | end 13 | 14 | ## 15 | # Returns a proc that will output +msg+ along with the default message. 16 | 17 | def message msg = nil, ending = nil, &default 18 | proc { 19 | msg = msg.call.chomp(".") if Proc === msg 20 | custom_message = "#{msg}.\n" unless msg.nil? or msg.to_s.empty? 21 | "#{custom_message}#{default.call}#{ending || "."}" 22 | } 23 | end 24 | end 25 | 26 | module CoreAssertions 27 | if defined?(MiniTest) 28 | require_relative '../../envutil' 29 | # for ruby core testing 30 | include MiniTest::Assertions 31 | 32 | # Compatibility hack for assert_raise 33 | Test::Unit::AssertionFailedError = MiniTest::Assertion 34 | else 35 | module MiniTest 36 | class Assertion < Exception; end 37 | class Skip < Assertion; end 38 | end 39 | 40 | require 'pp' 41 | require_relative 'envutil' 42 | include Test::Unit::Assertions 43 | end 44 | 45 | def mu_pp(obj) #:nodoc: 46 | obj.pretty_inspect.chomp 47 | end 48 | 49 | def assert_file 50 | AssertFile 51 | end 52 | 53 | FailDesc = proc do |status, message = "", out = ""| 54 | now = Time.now 55 | proc do 56 | EnvUtil.failure_description(status, now, message, out) 57 | end 58 | end 59 | 60 | def assert_in_out_err(args, test_stdin = "", test_stdout = [], test_stderr = [], message = nil, 61 | success: nil, **opt) 62 | args = Array(args).dup 63 | args.insert((Hash === args[0] ? 1 : 0), '--disable=gems') 64 | stdout, stderr, status = EnvUtil.invoke_ruby(args, test_stdin, true, true, **opt) 65 | desc = FailDesc[status, message, stderr] 66 | if block_given? 67 | raise "test_stdout ignored, use block only or without block" if test_stdout != [] 68 | raise "test_stderr ignored, use block only or without block" if test_stderr != [] 69 | yield(stdout.lines.map {|l| l.chomp }, stderr.lines.map {|l| l.chomp }, status) 70 | else 71 | all_assertions(desc) do |a| 72 | [["stdout", test_stdout, stdout], ["stderr", test_stderr, stderr]].each do |key, exp, act| 73 | a.for(key) do 74 | if exp.is_a?(Regexp) 75 | assert_match(exp, act) 76 | elsif exp.all? {|e| String === e} 77 | assert_equal(exp, act.lines.map {|l| l.chomp }) 78 | else 79 | assert_pattern_list(exp, act) 80 | end 81 | end 82 | end 83 | unless success.nil? 84 | a.for("success?") do 85 | if success 86 | assert_predicate(status, :success?) 87 | else 88 | assert_not_predicate(status, :success?) 89 | end 90 | end 91 | end 92 | end 93 | status 94 | end 95 | end 96 | 97 | if defined?(RubyVM::InstructionSequence) 98 | def syntax_check(code, fname, line) 99 | code = code.dup.force_encoding(Encoding::UTF_8) 100 | RubyVM::InstructionSequence.compile(code, fname, fname, line) 101 | :ok 102 | ensure 103 | raise if SyntaxError === $! 104 | end 105 | else 106 | def syntax_check(code, fname, line) 107 | code = code.b 108 | code.sub!(/\A(?:\xef\xbb\xbf)?(\s*\#.*$)*(\n)?/n) { 109 | "#$&#{"\n" if $1 && !$2}BEGIN{throw tag, :ok}\n" 110 | } 111 | code = code.force_encoding(Encoding::UTF_8) 112 | catch {|tag| eval(code, binding, fname, line - 1)} 113 | end 114 | end 115 | 116 | def assert_no_memory_leak(args, prepare, code, message=nil, limit: 2.0, rss: false, **opt) 117 | # TODO: consider choosing some appropriate limit for MJIT and stop skipping this once it does not randomly fail 118 | pend 'assert_no_memory_leak may consider MJIT memory usage as leak' if defined?(RubyVM::MJIT) && RubyVM::MJIT.enabled? 119 | 120 | require_relative '../../memory_status' 121 | raise MiniTest::Skip, "unsupported platform" unless defined?(Memory::Status) 122 | 123 | token = "\e[7;1m#{$$.to_s}:#{Time.now.strftime('%s.%L')}:#{rand(0x10000).to_s(16)}:\e[m" 124 | token_dump = token.dump 125 | token_re = Regexp.quote(token) 126 | envs = args.shift if Array === args and Hash === args.first 127 | args = [ 128 | "--disable=gems", 129 | "-r", File.expand_path("../../../memory_status", __FILE__), 130 | *args, 131 | "-v", "-", 132 | ] 133 | if defined? Memory::NO_MEMORY_LEAK_ENVS then 134 | envs ||= {} 135 | newenvs = envs.merge(Memory::NO_MEMORY_LEAK_ENVS) { |_, _, _| break } 136 | envs = newenvs if newenvs 137 | end 138 | args.unshift(envs) if envs 139 | cmd = [ 140 | 'END {STDERR.puts '"#{token_dump}"'"FINAL=#{Memory::Status.new}"}', 141 | prepare, 142 | 'STDERR.puts('"#{token_dump}"'"START=#{$initial_status = Memory::Status.new}")', 143 | '$initial_size = $initial_status.size', 144 | code, 145 | 'GC.start', 146 | ].join("\n") 147 | _, err, status = EnvUtil.invoke_ruby(args, cmd, true, true, **opt) 148 | before = err.sub!(/^#{token_re}START=(\{.*\})\n/, '') && Memory::Status.parse($1) 149 | after = err.sub!(/^#{token_re}FINAL=(\{.*\})\n/, '') && Memory::Status.parse($1) 150 | assert(status.success?, FailDesc[status, message, err]) 151 | ([:size, (rss && :rss)] & after.members).each do |n| 152 | b = before[n] 153 | a = after[n] 154 | next unless a > 0 and b > 0 155 | assert_operator(a.fdiv(b), :<, limit, message(message) {"#{n}: #{b} => #{a}"}) 156 | end 157 | rescue LoadError 158 | pend 159 | end 160 | 161 | # :call-seq: 162 | # assert_nothing_raised( *args, &block ) 163 | # 164 | #If any exceptions are given as arguments, the assertion will 165 | #fail if one of those exceptions are raised. Otherwise, the test fails 166 | #if any exceptions are raised. 167 | # 168 | #The final argument may be a failure message. 169 | # 170 | # assert_nothing_raised RuntimeError do 171 | # raise Exception #Assertion passes, Exception is not a RuntimeError 172 | # end 173 | # 174 | # assert_nothing_raised do 175 | # raise Exception #Assertion fails 176 | # end 177 | def assert_nothing_raised(*args) 178 | self._assertions += 1 179 | if Module === args.last 180 | msg = nil 181 | else 182 | msg = args.pop 183 | end 184 | begin 185 | line = __LINE__; yield 186 | rescue MiniTest::Skip 187 | raise 188 | rescue Exception => e 189 | bt = e.backtrace 190 | as = e.instance_of?(MiniTest::Assertion) 191 | if as 192 | ans = /\A#{Regexp.quote(__FILE__)}:#{line}:in /o 193 | bt.reject! {|ln| ans =~ ln} 194 | end 195 | if ((args.empty? && !as) || 196 | args.any? {|a| a.instance_of?(Module) ? e.is_a?(a) : e.class == a }) 197 | msg = message(msg) { 198 | "Exception raised:\n<#{mu_pp(e)}>\n" + 199 | "Backtrace:\n" + 200 | e.backtrace.map{|frame| " #{frame}"}.join("\n") 201 | } 202 | raise MiniTest::Assertion, msg.call, bt 203 | else 204 | raise 205 | end 206 | end 207 | end 208 | 209 | def prepare_syntax_check(code, fname = nil, mesg = nil, verbose: nil) 210 | fname ||= caller_locations(2, 1)[0] 211 | mesg ||= fname.to_s 212 | verbose, $VERBOSE = $VERBOSE, verbose 213 | case 214 | when Array === fname 215 | fname, line = *fname 216 | when defined?(fname.path) && defined?(fname.lineno) 217 | fname, line = fname.path, fname.lineno 218 | else 219 | line = 1 220 | end 221 | yield(code, fname, line, message(mesg) { 222 | if code.end_with?("\n") 223 | "```\n#{code}```\n" 224 | else 225 | "```\n#{code}\n```\n""no-newline" 226 | end 227 | }) 228 | ensure 229 | $VERBOSE = verbose 230 | end 231 | 232 | def assert_valid_syntax(code, *args, **opt) 233 | prepare_syntax_check(code, *args, **opt) do |src, fname, line, mesg| 234 | yield if defined?(yield) 235 | assert_nothing_raised(SyntaxError, mesg) do 236 | assert_equal(:ok, syntax_check(src, fname, line), mesg) 237 | end 238 | end 239 | end 240 | 241 | def assert_normal_exit(testsrc, message = '', child_env: nil, **opt) 242 | assert_valid_syntax(testsrc, caller_locations(1, 1)[0]) 243 | if child_env 244 | child_env = [child_env] 245 | else 246 | child_env = [] 247 | end 248 | out, _, status = EnvUtil.invoke_ruby(child_env + %W'-W0', testsrc, true, :merge_to_stdout, **opt) 249 | assert !status.signaled?, FailDesc[status, message, out] 250 | end 251 | 252 | def assert_ruby_status(args, test_stdin="", message=nil, **opt) 253 | out, _, status = EnvUtil.invoke_ruby(args, test_stdin, true, :merge_to_stdout, **opt) 254 | desc = FailDesc[status, message, out] 255 | assert(!status.signaled?, desc) 256 | message ||= "ruby exit status is not success:" 257 | assert(status.success?, desc) 258 | end 259 | 260 | ABORT_SIGNALS = Signal.list.values_at(*%w"ILL ABRT BUS SEGV TERM") 261 | 262 | def separated_runner(out = nil) 263 | out = out ? IO.new(out, 'w') : STDOUT 264 | at_exit { 265 | out.puts [Marshal.dump($!)].pack('m'), "assertions=\#{self._assertions}" 266 | } 267 | Test::Unit::Runner.class_variable_set(:@@stop_auto_run, true) 268 | end 269 | 270 | def assert_separately(args, file = nil, line = nil, src, ignore_stderr: nil, **opt) 271 | unless file and line 272 | loc, = caller_locations(1,1) 273 | file ||= loc.path 274 | line ||= loc.lineno 275 | end 276 | capture_stdout = true 277 | unless /mswin|mingw/ =~ RUBY_PLATFORM 278 | capture_stdout = false 279 | opt[:out] = MiniTest::Unit.output 280 | res_p, res_c = IO.pipe 281 | opt[res_c.fileno] = res_c.fileno 282 | end 283 | src = < marshal_error 309 | ignore_stderr = nil 310 | res = nil 311 | end 312 | if res and !(SystemExit === res) 313 | if bt = res.backtrace 314 | bt.each do |l| 315 | l.sub!(/\A-:(\d+)/){"#{file}:#{line + $1.to_i}"} 316 | end 317 | bt.concat(caller) 318 | else 319 | res.set_backtrace(caller) 320 | end 321 | raise res 322 | end 323 | 324 | # really is it succeed? 325 | unless ignore_stderr 326 | # the body of assert_separately must not output anything to detect error 327 | assert(stderr.empty?, FailDesc[status, "assert_separately failed with error message", stderr]) 328 | end 329 | assert(status.success?, FailDesc[status, "assert_separately failed", stderr]) 330 | raise marshal_error if marshal_error 331 | end 332 | 333 | # :call-seq: 334 | # assert_throw( tag, failure_message = nil, &block ) 335 | # 336 | #Fails unless the given block throws +tag+, returns the caught 337 | #value otherwise. 338 | # 339 | #An optional failure message may be provided as the final argument. 340 | # 341 | # tag = Object.new 342 | # assert_throw(tag, "#{tag} was not thrown!") do 343 | # throw tag 344 | # end 345 | def assert_throw(tag, msg = nil) 346 | ret = catch(tag) do 347 | begin 348 | yield(tag) 349 | rescue UncaughtThrowError => e 350 | thrown = e.tag 351 | end 352 | msg = message(msg) { 353 | "Expected #{mu_pp(tag)} to have been thrown"\ 354 | "#{%Q[, not #{thrown}] if thrown}" 355 | } 356 | assert(false, msg) 357 | end 358 | assert(true) 359 | ret 360 | end 361 | 362 | # :call-seq: 363 | # assert_raise( *args, &block ) 364 | # 365 | #Tests if the given block raises an exception. Acceptable exception 366 | #types may be given as optional arguments. If the last argument is a 367 | #String, it will be used as the error message. 368 | # 369 | # assert_raise do #Fails, no Exceptions are raised 370 | # end 371 | # 372 | # assert_raise NameError do 373 | # puts x #Raises NameError, so assertion succeeds 374 | # end 375 | def assert_raise(*exp, &b) 376 | case exp.last 377 | when String, Proc 378 | msg = exp.pop 379 | end 380 | 381 | begin 382 | yield 383 | rescue MiniTest::Skip => e 384 | return e if exp.include? MiniTest::Skip 385 | raise e 386 | rescue Exception => e 387 | expected = exp.any? { |ex| 388 | if ex.instance_of? Module then 389 | e.kind_of? ex 390 | else 391 | e.instance_of? ex 392 | end 393 | } 394 | 395 | assert expected, proc { 396 | flunk(message(msg) {"#{mu_pp(exp)} exception expected, not #{mu_pp(e)}"}) 397 | } 398 | 399 | return e 400 | ensure 401 | unless e 402 | exp = exp.first if exp.size == 1 403 | 404 | flunk(message(msg) {"#{mu_pp(exp)} expected but nothing was raised"}) 405 | end 406 | end 407 | end 408 | 409 | # :call-seq: 410 | # assert_raise_with_message(exception, expected, msg = nil, &block) 411 | # 412 | #Tests if the given block raises an exception with the expected 413 | #message. 414 | # 415 | # assert_raise_with_message(RuntimeError, "foo") do 416 | # nil #Fails, no Exceptions are raised 417 | # end 418 | # 419 | # assert_raise_with_message(RuntimeError, "foo") do 420 | # raise ArgumentError, "foo" #Fails, different Exception is raised 421 | # end 422 | # 423 | # assert_raise_with_message(RuntimeError, "foo") do 424 | # raise "bar" #Fails, RuntimeError is raised but the message differs 425 | # end 426 | # 427 | # assert_raise_with_message(RuntimeError, "foo") do 428 | # raise "foo" #Raises RuntimeError with the message, so assertion succeeds 429 | # end 430 | def assert_raise_with_message(exception, expected, msg = nil, &block) 431 | case expected 432 | when String 433 | assert = :assert_equal 434 | when Regexp 435 | assert = :assert_match 436 | else 437 | raise TypeError, "Expected #{expected.inspect} to be a kind of String or Regexp, not #{expected.class}" 438 | end 439 | 440 | ex = m = nil 441 | EnvUtil.with_default_internal(expected.encoding) do 442 | ex = assert_raise(exception, msg || proc {"Exception(#{exception}) with message matches to #{expected.inspect}"}) do 443 | yield 444 | end 445 | m = ex.message 446 | end 447 | msg = message(msg, "") {"Expected Exception(#{exception}) was raised, but the message doesn't match"} 448 | 449 | if assert == :assert_equal 450 | assert_equal(expected, m, msg) 451 | else 452 | msg = message(msg) { "Expected #{mu_pp expected} to match #{mu_pp m}" } 453 | assert expected =~ m, msg 454 | block.binding.eval("proc{|_|$~=_}").call($~) 455 | end 456 | ex 457 | end 458 | 459 | def assert_warning(pat, msg = nil) 460 | result = nil 461 | stderr = EnvUtil.with_default_internal(pat.encoding) { 462 | EnvUtil.verbose_warning { 463 | result = yield 464 | } 465 | } 466 | msg = message(msg) {diff pat, stderr} 467 | assert(pat === stderr, msg) 468 | result 469 | end 470 | 471 | def assert_warn(*args) 472 | assert_warning(*args) {$VERBOSE = false; yield} 473 | end 474 | 475 | class << (AssertFile = Struct.new(:failure_message).new) 476 | include CoreAssertions 477 | def assert_file_predicate(predicate, *args) 478 | if /\Anot_/ =~ predicate 479 | predicate = $' 480 | neg = " not" 481 | end 482 | result = File.__send__(predicate, *args) 483 | result = !result if neg 484 | mesg = "Expected file ".dup << args.shift.inspect 485 | mesg << "#{neg} to be #{predicate}" 486 | mesg << mu_pp(args).sub(/\A\[(.*)\]\z/m, '(\1)') unless args.empty? 487 | mesg << " #{failure_message}" if failure_message 488 | assert(result, mesg) 489 | end 490 | alias method_missing assert_file_predicate 491 | 492 | def for(message) 493 | clone.tap {|a| a.failure_message = message} 494 | end 495 | end 496 | 497 | class AllFailures 498 | attr_reader :failures 499 | 500 | def initialize 501 | @count = 0 502 | @failures = {} 503 | end 504 | 505 | def for(key) 506 | @count += 1 507 | yield 508 | rescue Exception => e 509 | @failures[key] = [@count, e] 510 | end 511 | 512 | def foreach(*keys) 513 | keys.each do |key| 514 | @count += 1 515 | begin 516 | yield key 517 | rescue Exception => e 518 | @failures[key] = [@count, e] 519 | end 520 | end 521 | end 522 | 523 | def message 524 | i = 0 525 | total = @count.to_s 526 | fmt = "%#{total.size}d" 527 | @failures.map {|k, (n, v)| 528 | v = v.message 529 | "\n#{i+=1}. [#{fmt%n}/#{total}] Assertion for #{k.inspect}\n#{v.b.gsub(/^/, ' | ').force_encoding(v.encoding)}" 530 | }.join("\n") 531 | end 532 | 533 | def pass? 534 | @failures.empty? 535 | end 536 | end 537 | 538 | # threads should respond to shift method. 539 | # Array can be used. 540 | def assert_join_threads(threads, message = nil) 541 | errs = [] 542 | values = [] 543 | while th = threads.shift 544 | begin 545 | values << th.value 546 | rescue Exception 547 | errs << [th, $!] 548 | th = nil 549 | end 550 | end 551 | values 552 | ensure 553 | if th&.alive? 554 | th.raise(Timeout::Error.new) 555 | th.join rescue errs << [th, $!] 556 | end 557 | if !errs.empty? 558 | msg = "exceptions on #{errs.length} threads:\n" + 559 | errs.map {|t, err| 560 | "#{t.inspect}:\n" + 561 | RUBY_VERSION >= "2.5.0" ? err.full_message(highlight: false, order: :top) : err.message 562 | }.join("\n---\n") 563 | if message 564 | msg = "#{message}\n#{msg}" 565 | end 566 | raise MiniTest::Assertion, msg 567 | end 568 | end 569 | 570 | def assert_all_assertions(msg = nil) 571 | all = AllFailures.new 572 | yield all 573 | ensure 574 | assert(all.pass?, message(msg) {all.message.chomp(".")}) 575 | end 576 | alias all_assertions assert_all_assertions 577 | 578 | def message(msg = nil, *args, &default) # :nodoc: 579 | if Proc === msg 580 | super(nil, *args) do 581 | ary = [msg.call, (default.call if default)].compact.reject(&:empty?) 582 | if 1 < ary.length 583 | ary[0...-1] = ary[0...-1].map {|str| str.sub(/(? '8.8.8.8' 317 | # - :nameserver => ['8.8.8.8', '8.8.4.4'] 318 | # 319 | # The value of :nameserver_port should be an array of 320 | # pair of nameserver address and port number. 321 | # - :nameserver_port => [['8.8.8.8', 53], ['8.8.4.4', 53]] 322 | # 323 | # Example: 324 | # 325 | # Resolv::DNS.new(:nameserver => ['210.251.121.21'], 326 | # :search => ['ruby-lang.org'], 327 | # :ndots => 1) 328 | 329 | def initialize(config_info=nil) 330 | @mutex = Thread::Mutex.new 331 | @config = Config.new(config_info) 332 | @initialized = nil 333 | end 334 | 335 | # Sets the resolver timeouts. This may be a single positive number 336 | # or an array of positive numbers representing timeouts in seconds. 337 | # If an array is specified, a DNS request will retry and wait for 338 | # each successive interval in the array until a successful response 339 | # is received. Specifying +nil+ reverts to the default timeouts: 340 | # [ 5, second = 5 * 2 / nameserver_count, 2 * second, 4 * second ] 341 | # 342 | # Example: 343 | # 344 | # dns.timeouts = 3 345 | # 346 | def timeouts=(values) 347 | @config.timeouts = values 348 | end 349 | 350 | def lazy_initialize # :nodoc: 351 | @mutex.synchronize { 352 | unless @initialized 353 | @config.lazy_initialize 354 | @initialized = true 355 | end 356 | } 357 | self 358 | end 359 | 360 | ## 361 | # Closes the DNS resolver. 362 | 363 | def close 364 | @mutex.synchronize { 365 | if @initialized 366 | @initialized = false 367 | end 368 | } 369 | end 370 | 371 | ## 372 | # Gets the IP address of +name+ from the DNS resolver. 373 | # 374 | # +name+ can be a Resolv::DNS::Name or a String. Retrieved address will 375 | # be a Resolv::IPv4 or Resolv::IPv6 376 | 377 | def getaddress(name) 378 | each_address(name) {|address| return address} 379 | raise ResolvError.new("DNS result has no information for #{name}") 380 | end 381 | 382 | ## 383 | # Gets all IP addresses for +name+ from the DNS resolver. 384 | # 385 | # +name+ can be a Resolv::DNS::Name or a String. Retrieved addresses will 386 | # be a Resolv::IPv4 or Resolv::IPv6 387 | 388 | def getaddresses(name) 389 | ret = [] 390 | each_address(name) {|address| ret << address} 391 | return ret 392 | end 393 | 394 | ## 395 | # Iterates over all IP addresses for +name+ retrieved from the DNS 396 | # resolver. 397 | # 398 | # +name+ can be a Resolv::DNS::Name or a String. Retrieved addresses will 399 | # be a Resolv::IPv4 or Resolv::IPv6 400 | 401 | def each_address(name) 402 | each_resource(name, Resource::IN::A) {|resource| yield resource.address} 403 | if use_ipv6? 404 | each_resource(name, Resource::IN::AAAA) {|resource| yield resource.address} 405 | end 406 | end 407 | 408 | def use_ipv6? # :nodoc: 409 | begin 410 | list = Socket.ip_address_list 411 | rescue NotImplementedError 412 | return true 413 | end 414 | list.any? {|a| a.ipv6? && !a.ipv6_loopback? && !a.ipv6_linklocal? } 415 | end 416 | private :use_ipv6? 417 | 418 | ## 419 | # Gets the hostname for +address+ from the DNS resolver. 420 | # 421 | # +address+ must be a Resolv::IPv4, Resolv::IPv6 or a String. Retrieved 422 | # name will be a Resolv::DNS::Name. 423 | 424 | def getname(address) 425 | each_name(address) {|name| return name} 426 | raise ResolvError.new("DNS result has no information for #{address}") 427 | end 428 | 429 | ## 430 | # Gets all hostnames for +address+ from the DNS resolver. 431 | # 432 | # +address+ must be a Resolv::IPv4, Resolv::IPv6 or a String. Retrieved 433 | # names will be Resolv::DNS::Name instances. 434 | 435 | def getnames(address) 436 | ret = [] 437 | each_name(address) {|name| ret << name} 438 | return ret 439 | end 440 | 441 | ## 442 | # Iterates over all hostnames for +address+ retrieved from the DNS 443 | # resolver. 444 | # 445 | # +address+ must be a Resolv::IPv4, Resolv::IPv6 or a String. Retrieved 446 | # names will be Resolv::DNS::Name instances. 447 | 448 | def each_name(address) 449 | case address 450 | when Name 451 | ptr = address 452 | when IPv4, IPv6 453 | ptr = address.to_name 454 | when IPv4::Regex 455 | ptr = IPv4.create(address).to_name 456 | when IPv6::Regex 457 | ptr = IPv6.create(address).to_name 458 | else 459 | raise ResolvError.new("cannot interpret as address: #{address}") 460 | end 461 | each_resource(ptr, Resource::IN::PTR) {|resource| yield resource.name} 462 | end 463 | 464 | ## 465 | # Look up the +typeclass+ DNS resource of +name+. 466 | # 467 | # +name+ must be a Resolv::DNS::Name or a String. 468 | # 469 | # +typeclass+ should be one of the following: 470 | # 471 | # * Resolv::DNS::Resource::IN::A 472 | # * Resolv::DNS::Resource::IN::AAAA 473 | # * Resolv::DNS::Resource::IN::ANY 474 | # * Resolv::DNS::Resource::IN::CNAME 475 | # * Resolv::DNS::Resource::IN::HINFO 476 | # * Resolv::DNS::Resource::IN::MINFO 477 | # * Resolv::DNS::Resource::IN::MX 478 | # * Resolv::DNS::Resource::IN::NS 479 | # * Resolv::DNS::Resource::IN::PTR 480 | # * Resolv::DNS::Resource::IN::SOA 481 | # * Resolv::DNS::Resource::IN::TXT 482 | # * Resolv::DNS::Resource::IN::WKS 483 | # 484 | # Returned resource is represented as a Resolv::DNS::Resource instance, 485 | # i.e. Resolv::DNS::Resource::IN::A. 486 | 487 | def getresource(name, typeclass) 488 | each_resource(name, typeclass) {|resource| return resource} 489 | raise ResolvError.new("DNS result has no information for #{name}") 490 | end 491 | 492 | ## 493 | # Looks up all +typeclass+ DNS resources for +name+. See #getresource for 494 | # argument details. 495 | 496 | def getresources(name, typeclass) 497 | ret = [] 498 | each_resource(name, typeclass) {|resource| ret << resource} 499 | return ret 500 | end 501 | 502 | ## 503 | # Iterates over all +typeclass+ DNS resources for +name+. See 504 | # #getresource for argument details. 505 | 506 | def each_resource(name, typeclass, &proc) 507 | fetch_resource(name, typeclass) {|reply, reply_name| 508 | extract_resources(reply, reply_name, typeclass, &proc) 509 | } 510 | end 511 | 512 | def fetch_resource(name, typeclass) 513 | lazy_initialize 514 | begin 515 | requester = make_udp_requester 516 | rescue Errno::EACCES 517 | # fall back to TCP 518 | end 519 | senders = {} 520 | begin 521 | @config.resolv(name) {|candidate, tout, nameserver, port| 522 | requester ||= make_tcp_requester(nameserver, port) 523 | msg = Message.new 524 | msg.rd = 1 525 | msg.add_question(candidate, typeclass) 526 | unless sender = senders[[candidate, nameserver, port]] 527 | sender = requester.sender(msg, candidate, nameserver, port) 528 | next if !sender 529 | senders[[candidate, nameserver, port]] = sender 530 | end 531 | reply, reply_name = requester.request(sender, tout) 532 | case reply.rcode 533 | when RCode::NoError 534 | if reply.tc == 1 and not Requester::TCP === requester 535 | requester.close 536 | # Retry via TCP: 537 | requester = make_tcp_requester(nameserver, port) 538 | senders = {} 539 | # This will use TCP for all remaining candidates (assuming the 540 | # current candidate does not already respond successfully via 541 | # TCP). This makes sense because we already know the full 542 | # response will not fit in an untruncated UDP packet. 543 | redo 544 | else 545 | yield(reply, reply_name) 546 | end 547 | return 548 | when RCode::NXDomain 549 | raise Config::NXDomain.new(reply_name.to_s) 550 | else 551 | raise Config::OtherResolvError.new(reply_name.to_s) 552 | end 553 | } 554 | ensure 555 | requester&.close 556 | end 557 | end 558 | 559 | def make_udp_requester # :nodoc: 560 | nameserver_port = @config.nameserver_port 561 | if nameserver_port.length == 1 562 | Requester::ConnectedUDP.new(*nameserver_port[0]) 563 | else 564 | Requester::UnconnectedUDP.new(*nameserver_port) 565 | end 566 | end 567 | 568 | def make_tcp_requester(host, port) # :nodoc: 569 | return Requester::TCP.new(host, port) 570 | end 571 | 572 | def extract_resources(msg, name, typeclass) # :nodoc: 573 | if typeclass < Resource::ANY 574 | n0 = Name.create(name) 575 | msg.each_resource {|n, ttl, data| 576 | yield data if n0 == n 577 | } 578 | end 579 | yielded = false 580 | n0 = Name.create(name) 581 | msg.each_resource {|n, ttl, data| 582 | if n0 == n 583 | case data 584 | when typeclass 585 | yield data 586 | yielded = true 587 | when Resource::CNAME 588 | n0 = data.name 589 | end 590 | end 591 | } 592 | return if yielded 593 | msg.each_resource {|n, ttl, data| 594 | if n0 == n 595 | case data 596 | when typeclass 597 | yield data 598 | end 599 | end 600 | } 601 | end 602 | 603 | if defined? SecureRandom 604 | def self.random(arg) # :nodoc: 605 | begin 606 | SecureRandom.random_number(arg) 607 | rescue NotImplementedError 608 | rand(arg) 609 | end 610 | end 611 | else 612 | def self.random(arg) # :nodoc: 613 | rand(arg) 614 | end 615 | end 616 | 617 | RequestID = {} # :nodoc: 618 | RequestIDMutex = Thread::Mutex.new # :nodoc: 619 | 620 | def self.allocate_request_id(host, port) # :nodoc: 621 | id = nil 622 | RequestIDMutex.synchronize { 623 | h = (RequestID[[host, port]] ||= {}) 624 | begin 625 | id = random(0x0000..0xffff) 626 | end while h[id] 627 | h[id] = true 628 | } 629 | id 630 | end 631 | 632 | def self.free_request_id(host, port, id) # :nodoc: 633 | RequestIDMutex.synchronize { 634 | key = [host, port] 635 | if h = RequestID[key] 636 | h.delete id 637 | if h.empty? 638 | RequestID.delete key 639 | end 640 | end 641 | } 642 | end 643 | 644 | def self.bind_random_port(udpsock, bind_host="0.0.0.0") # :nodoc: 645 | begin 646 | port = random(1024..65535) 647 | udpsock.bind(bind_host, port) 648 | rescue Errno::EADDRINUSE, # POSIX 649 | Errno::EACCES, # SunOS: See PRIV_SYS_NFS in privileges(5) 650 | Errno::EPERM # FreeBSD: security.mac.portacl.port_high is configurable. See mac_portacl(4). 651 | retry 652 | end 653 | end 654 | 655 | class Requester # :nodoc: 656 | def initialize 657 | @senders = {} 658 | @socks = nil 659 | end 660 | 661 | def request(sender, tout) 662 | start = Process.clock_gettime(Process::CLOCK_MONOTONIC) 663 | timelimit = start + tout 664 | begin 665 | sender.send 666 | rescue Errno::EHOSTUNREACH, # multi-homed IPv6 may generate this 667 | Errno::ENETUNREACH 668 | raise ResolvTimeout 669 | end 670 | while true 671 | before_select = Process.clock_gettime(Process::CLOCK_MONOTONIC) 672 | timeout = timelimit - before_select 673 | if timeout <= 0 674 | raise ResolvTimeout 675 | end 676 | if @socks.size == 1 677 | select_result = @socks[0].wait_readable(timeout) ? [ @socks ] : nil 678 | else 679 | select_result = IO.select(@socks, nil, nil, timeout) 680 | end 681 | if !select_result 682 | after_select = Process.clock_gettime(Process::CLOCK_MONOTONIC) 683 | next if after_select < timelimit 684 | raise ResolvTimeout 685 | end 686 | begin 687 | reply, from = recv_reply(select_result[0]) 688 | rescue Errno::ECONNREFUSED, # GNU/Linux, FreeBSD 689 | Errno::ECONNRESET # Windows 690 | # No name server running on the server? 691 | # Don't wait anymore. 692 | raise ResolvTimeout 693 | end 694 | begin 695 | msg = Message.decode(reply) 696 | rescue DecodeError 697 | next # broken DNS message ignored 698 | end 699 | if sender == sender_for(from, msg) 700 | break 701 | else 702 | # unexpected DNS message ignored 703 | end 704 | end 705 | return msg, sender.data 706 | end 707 | 708 | def sender_for(addr, msg) 709 | @senders[[addr,msg.id]] 710 | end 711 | 712 | def close 713 | socks = @socks 714 | @socks = nil 715 | socks&.each(&:close) 716 | end 717 | 718 | class Sender # :nodoc: 719 | def initialize(msg, data, sock) 720 | @msg = msg 721 | @data = data 722 | @sock = sock 723 | end 724 | end 725 | 726 | class UnconnectedUDP < Requester # :nodoc: 727 | def initialize(*nameserver_port) 728 | super() 729 | @nameserver_port = nameserver_port 730 | @initialized = false 731 | @mutex = Thread::Mutex.new 732 | end 733 | 734 | def lazy_initialize 735 | @mutex.synchronize { 736 | next if @initialized 737 | @initialized = true 738 | @socks_hash = {} 739 | @socks = [] 740 | @nameserver_port.each {|host, port| 741 | if host.index(':') 742 | bind_host = "::" 743 | af = Socket::AF_INET6 744 | else 745 | bind_host = "0.0.0.0" 746 | af = Socket::AF_INET 747 | end 748 | next if @socks_hash[bind_host] 749 | begin 750 | sock = UDPSocket.new(af) 751 | rescue Errno::EAFNOSUPPORT 752 | next # The kernel doesn't support the address family. 753 | end 754 | @socks << sock 755 | @socks_hash[bind_host] = sock 756 | sock.do_not_reverse_lookup = true 757 | DNS.bind_random_port(sock, bind_host) 758 | } 759 | } 760 | self 761 | end 762 | 763 | def recv_reply(readable_socks) 764 | lazy_initialize 765 | reply, from = readable_socks[0].recvfrom(UDPSize) 766 | return reply, [from[3],from[1]] 767 | end 768 | 769 | def sender(msg, data, host, port=Port) 770 | host = Addrinfo.ip(host).ip_address 771 | lazy_initialize 772 | sock = @socks_hash[host.index(':') ? "::" : "0.0.0.0"] 773 | return nil if !sock 774 | service = [host, port] 775 | id = DNS.allocate_request_id(host, port) 776 | request = msg.encode 777 | request[0,2] = [id].pack('n') 778 | return @senders[[service, id]] = 779 | Sender.new(request, data, sock, host, port) 780 | end 781 | 782 | def close 783 | @mutex.synchronize { 784 | if @initialized 785 | super 786 | @senders.each_key {|service, id| 787 | DNS.free_request_id(service[0], service[1], id) 788 | } 789 | @initialized = false 790 | end 791 | } 792 | end 793 | 794 | class Sender < Requester::Sender # :nodoc: 795 | def initialize(msg, data, sock, host, port) 796 | super(msg, data, sock) 797 | @host = host 798 | @port = port 799 | end 800 | attr_reader :data 801 | 802 | def send 803 | raise "@sock is nil." if @sock.nil? 804 | @sock.send(@msg, 0, @host, @port) 805 | end 806 | end 807 | end 808 | 809 | class ConnectedUDP < Requester # :nodoc: 810 | def initialize(host, port=Port) 811 | super() 812 | @host = host 813 | @port = port 814 | @mutex = Thread::Mutex.new 815 | @initialized = false 816 | end 817 | 818 | def lazy_initialize 819 | @mutex.synchronize { 820 | next if @initialized 821 | @initialized = true 822 | is_ipv6 = @host.index(':') 823 | sock = UDPSocket.new(is_ipv6 ? Socket::AF_INET6 : Socket::AF_INET) 824 | @socks = [sock] 825 | sock.do_not_reverse_lookup = true 826 | DNS.bind_random_port(sock, is_ipv6 ? "::" : "0.0.0.0") 827 | sock.connect(@host, @port) 828 | } 829 | self 830 | end 831 | 832 | def recv_reply(readable_socks) 833 | lazy_initialize 834 | reply = readable_socks[0].recv(UDPSize) 835 | return reply, nil 836 | end 837 | 838 | def sender(msg, data, host=@host, port=@port) 839 | lazy_initialize 840 | unless host == @host && port == @port 841 | raise RequestError.new("host/port don't match: #{host}:#{port}") 842 | end 843 | id = DNS.allocate_request_id(@host, @port) 844 | request = msg.encode 845 | request[0,2] = [id].pack('n') 846 | return @senders[[nil,id]] = Sender.new(request, data, @socks[0]) 847 | end 848 | 849 | def close 850 | @mutex.synchronize do 851 | if @initialized 852 | super 853 | @senders.each_key {|from, id| 854 | DNS.free_request_id(@host, @port, id) 855 | } 856 | @initialized = false 857 | end 858 | end 859 | end 860 | 861 | class Sender < Requester::Sender # :nodoc: 862 | def send 863 | raise "@sock is nil." if @sock.nil? 864 | @sock.send(@msg, 0) 865 | end 866 | attr_reader :data 867 | end 868 | end 869 | 870 | class MDNSOneShot < UnconnectedUDP # :nodoc: 871 | def sender(msg, data, host, port=Port) 872 | lazy_initialize 873 | id = DNS.allocate_request_id(host, port) 874 | request = msg.encode 875 | request[0,2] = [id].pack('n') 876 | sock = @socks_hash[host.index(':') ? "::" : "0.0.0.0"] 877 | return @senders[id] = 878 | UnconnectedUDP::Sender.new(request, data, sock, host, port) 879 | end 880 | 881 | def sender_for(addr, msg) 882 | lazy_initialize 883 | @senders[msg.id] 884 | end 885 | end 886 | 887 | class TCP < Requester # :nodoc: 888 | def initialize(host, port=Port) 889 | super() 890 | @host = host 891 | @port = port 892 | sock = TCPSocket.new(@host, @port) 893 | @socks = [sock] 894 | @senders = {} 895 | end 896 | 897 | def recv_reply(readable_socks) 898 | len = readable_socks[0].read(2).unpack('n')[0] 899 | reply = @socks[0].read(len) 900 | return reply, nil 901 | end 902 | 903 | def sender(msg, data, host=@host, port=@port) 904 | unless host == @host && port == @port 905 | raise RequestError.new("host/port don't match: #{host}:#{port}") 906 | end 907 | id = DNS.allocate_request_id(@host, @port) 908 | request = msg.encode 909 | request[0,2] = [request.length, id].pack('nn') 910 | return @senders[[nil,id]] = Sender.new(request, data, @socks[0]) 911 | end 912 | 913 | class Sender < Requester::Sender # :nodoc: 914 | def send 915 | @sock.print(@msg) 916 | @sock.flush 917 | end 918 | attr_reader :data 919 | end 920 | 921 | def close 922 | super 923 | @senders.each_key {|from,id| 924 | DNS.free_request_id(@host, @port, id) 925 | } 926 | end 927 | end 928 | 929 | ## 930 | # Indicates a problem with the DNS request. 931 | 932 | class RequestError < StandardError 933 | end 934 | end 935 | 936 | class Config # :nodoc: 937 | def initialize(config_info=nil) 938 | @mutex = Thread::Mutex.new 939 | @config_info = config_info 940 | @initialized = nil 941 | @timeouts = nil 942 | end 943 | 944 | def timeouts=(values) 945 | if values 946 | values = Array(values) 947 | values.each do |t| 948 | Numeric === t or raise ArgumentError, "#{t.inspect} is not numeric" 949 | t > 0.0 or raise ArgumentError, "timeout=#{t} must be positive" 950 | end 951 | @timeouts = values 952 | else 953 | @timeouts = nil 954 | end 955 | end 956 | 957 | def Config.parse_resolv_conf(filename) 958 | nameserver = [] 959 | search = nil 960 | ndots = 1 961 | File.open(filename, 'rb') {|f| 962 | f.each {|line| 963 | line.sub!(/[#;].*/, '') 964 | keyword, *args = line.split(/\s+/) 965 | next unless keyword 966 | case keyword 967 | when 'nameserver' 968 | nameserver += args 969 | when 'domain' 970 | next if args.empty? 971 | search = [args[0]] 972 | when 'search' 973 | next if args.empty? 974 | search = args 975 | when 'options' 976 | args.each {|arg| 977 | case arg 978 | when /\Andots:(\d+)\z/ 979 | ndots = $1.to_i 980 | end 981 | } 982 | end 983 | } 984 | } 985 | return { :nameserver => nameserver, :search => search, :ndots => ndots } 986 | end 987 | 988 | def Config.default_config_hash(filename="/etc/resolv.conf") 989 | if File.exist? filename 990 | config_hash = Config.parse_resolv_conf(filename) 991 | else 992 | if /mswin|cygwin|mingw|bccwin/ =~ RUBY_PLATFORM 993 | require 'win32/resolv' 994 | search, nameserver = Win32::Resolv.get_resolv_info 995 | config_hash = {} 996 | config_hash[:nameserver] = nameserver if nameserver 997 | config_hash[:search] = [search].flatten if search 998 | end 999 | end 1000 | config_hash || {} 1001 | end 1002 | 1003 | def lazy_initialize 1004 | @mutex.synchronize { 1005 | unless @initialized 1006 | @nameserver_port = [] 1007 | @search = nil 1008 | @ndots = 1 1009 | case @config_info 1010 | when nil 1011 | config_hash = Config.default_config_hash 1012 | when String 1013 | config_hash = Config.parse_resolv_conf(@config_info) 1014 | when Hash 1015 | config_hash = @config_info.dup 1016 | if String === config_hash[:nameserver] 1017 | config_hash[:nameserver] = [config_hash[:nameserver]] 1018 | end 1019 | if String === config_hash[:search] 1020 | config_hash[:search] = [config_hash[:search]] 1021 | end 1022 | else 1023 | raise ArgumentError.new("invalid resolv configuration: #{@config_info.inspect}") 1024 | end 1025 | if config_hash.include? :nameserver 1026 | @nameserver_port = config_hash[:nameserver].map {|ns| [ns, Port] } 1027 | end 1028 | if config_hash.include? :nameserver_port 1029 | @nameserver_port = config_hash[:nameserver_port].map {|ns, port| [ns, (port || Port)] } 1030 | end 1031 | @search = config_hash[:search] if config_hash.include? :search 1032 | @ndots = config_hash[:ndots] if config_hash.include? :ndots 1033 | 1034 | if @nameserver_port.empty? 1035 | @nameserver_port << ['0.0.0.0', Port] 1036 | end 1037 | if @search 1038 | @search = @search.map {|arg| Label.split(arg) } 1039 | else 1040 | hostname = Socket.gethostname 1041 | if /\./ =~ hostname 1042 | @search = [Label.split($')] 1043 | else 1044 | @search = [[]] 1045 | end 1046 | end 1047 | 1048 | if !@nameserver_port.kind_of?(Array) || 1049 | @nameserver_port.any? {|ns_port| 1050 | !(Array === ns_port) || 1051 | ns_port.length != 2 1052 | !(String === ns_port[0]) || 1053 | !(Integer === ns_port[1]) 1054 | } 1055 | raise ArgumentError.new("invalid nameserver config: #{@nameserver_port.inspect}") 1056 | end 1057 | 1058 | if !@search.kind_of?(Array) || 1059 | !@search.all? {|ls| ls.all? {|l| Label::Str === l } } 1060 | raise ArgumentError.new("invalid search config: #{@search.inspect}") 1061 | end 1062 | 1063 | if !@ndots.kind_of?(Integer) 1064 | raise ArgumentError.new("invalid ndots config: #{@ndots.inspect}") 1065 | end 1066 | 1067 | @initialized = true 1068 | end 1069 | } 1070 | self 1071 | end 1072 | 1073 | def single? 1074 | lazy_initialize 1075 | if @nameserver_port.length == 1 1076 | return @nameserver_port[0] 1077 | else 1078 | return nil 1079 | end 1080 | end 1081 | 1082 | def nameserver_port 1083 | @nameserver_port 1084 | end 1085 | 1086 | def generate_candidates(name) 1087 | candidates = nil 1088 | name = Name.create(name) 1089 | if name.absolute? 1090 | candidates = [name] 1091 | else 1092 | if @ndots <= name.length - 1 1093 | candidates = [Name.new(name.to_a)] 1094 | else 1095 | candidates = [] 1096 | end 1097 | candidates.concat(@search.map {|domain| Name.new(name.to_a + domain)}) 1098 | fname = Name.create("#{name}.") 1099 | if !candidates.include?(fname) 1100 | candidates << fname 1101 | end 1102 | end 1103 | return candidates 1104 | end 1105 | 1106 | InitialTimeout = 5 1107 | 1108 | def generate_timeouts 1109 | ts = [InitialTimeout] 1110 | ts << ts[-1] * 2 / @nameserver_port.length 1111 | ts << ts[-1] * 2 1112 | ts << ts[-1] * 2 1113 | return ts 1114 | end 1115 | 1116 | def resolv(name) 1117 | candidates = generate_candidates(name) 1118 | timeouts = @timeouts || generate_timeouts 1119 | begin 1120 | candidates.each {|candidate| 1121 | begin 1122 | timeouts.each {|tout| 1123 | @nameserver_port.each {|nameserver, port| 1124 | begin 1125 | yield candidate, tout, nameserver, port 1126 | rescue ResolvTimeout 1127 | end 1128 | } 1129 | } 1130 | raise ResolvError.new("DNS resolv timeout: #{name}") 1131 | rescue NXDomain 1132 | end 1133 | } 1134 | rescue ResolvError 1135 | end 1136 | end 1137 | 1138 | ## 1139 | # Indicates no such domain was found. 1140 | 1141 | class NXDomain < ResolvError 1142 | end 1143 | 1144 | ## 1145 | # Indicates some other unhandled resolver error was encountered. 1146 | 1147 | class OtherResolvError < ResolvError 1148 | end 1149 | end 1150 | 1151 | module OpCode # :nodoc: 1152 | Query = 0 1153 | IQuery = 1 1154 | Status = 2 1155 | Notify = 4 1156 | Update = 5 1157 | end 1158 | 1159 | module RCode # :nodoc: 1160 | NoError = 0 1161 | FormErr = 1 1162 | ServFail = 2 1163 | NXDomain = 3 1164 | NotImp = 4 1165 | Refused = 5 1166 | YXDomain = 6 1167 | YXRRSet = 7 1168 | NXRRSet = 8 1169 | NotAuth = 9 1170 | NotZone = 10 1171 | BADVERS = 16 1172 | BADSIG = 16 1173 | BADKEY = 17 1174 | BADTIME = 18 1175 | BADMODE = 19 1176 | BADNAME = 20 1177 | BADALG = 21 1178 | end 1179 | 1180 | ## 1181 | # Indicates that the DNS response was unable to be decoded. 1182 | 1183 | class DecodeError < StandardError 1184 | end 1185 | 1186 | ## 1187 | # Indicates that the DNS request was unable to be encoded. 1188 | 1189 | class EncodeError < StandardError 1190 | end 1191 | 1192 | module Label # :nodoc: 1193 | def self.split(arg) 1194 | labels = [] 1195 | arg.scan(/[^\.]+/) {labels << Str.new($&)} 1196 | return labels 1197 | end 1198 | 1199 | class Str # :nodoc: 1200 | def initialize(string) 1201 | @string = string 1202 | # case insensivity of DNS labels doesn't apply non-ASCII characters. [RFC 4343] 1203 | # This assumes @string is given in ASCII compatible encoding. 1204 | @downcase = string.b.downcase 1205 | end 1206 | attr_reader :string, :downcase 1207 | 1208 | def to_s 1209 | return @string 1210 | end 1211 | 1212 | def inspect 1213 | return "#<#{self.class} #{self}>" 1214 | end 1215 | 1216 | def ==(other) 1217 | return self.class == other.class && @downcase == other.downcase 1218 | end 1219 | 1220 | def eql?(other) 1221 | return self == other 1222 | end 1223 | 1224 | def hash 1225 | return @downcase.hash 1226 | end 1227 | end 1228 | end 1229 | 1230 | ## 1231 | # A representation of a DNS name. 1232 | 1233 | class Name 1234 | 1235 | ## 1236 | # Creates a new DNS name from +arg+. +arg+ can be: 1237 | # 1238 | # Name:: returns +arg+. 1239 | # String:: Creates a new Name. 1240 | 1241 | def self.create(arg) 1242 | case arg 1243 | when Name 1244 | return arg 1245 | when String 1246 | return Name.new(Label.split(arg), /\.\z/ =~ arg ? true : false) 1247 | else 1248 | raise ArgumentError.new("cannot interpret as DNS name: #{arg.inspect}") 1249 | end 1250 | end 1251 | 1252 | def initialize(labels, absolute=true) # :nodoc: 1253 | labels = labels.map {|label| 1254 | case label 1255 | when String then Label::Str.new(label) 1256 | when Label::Str then label 1257 | else 1258 | raise ArgumentError, "unexpected label: #{label.inspect}" 1259 | end 1260 | } 1261 | @labels = labels 1262 | @absolute = absolute 1263 | end 1264 | 1265 | def inspect # :nodoc: 1266 | "#<#{self.class}: #{self}#{@absolute ? '.' : ''}>" 1267 | end 1268 | 1269 | ## 1270 | # True if this name is absolute. 1271 | 1272 | def absolute? 1273 | return @absolute 1274 | end 1275 | 1276 | def ==(other) # :nodoc: 1277 | return false unless Name === other 1278 | return false unless @absolute == other.absolute? 1279 | return @labels == other.to_a 1280 | end 1281 | 1282 | alias eql? == # :nodoc: 1283 | 1284 | ## 1285 | # Returns true if +other+ is a subdomain. 1286 | # 1287 | # Example: 1288 | # 1289 | # domain = Resolv::DNS::Name.create("y.z") 1290 | # p Resolv::DNS::Name.create("w.x.y.z").subdomain_of?(domain) #=> true 1291 | # p Resolv::DNS::Name.create("x.y.z").subdomain_of?(domain) #=> true 1292 | # p Resolv::DNS::Name.create("y.z").subdomain_of?(domain) #=> false 1293 | # p Resolv::DNS::Name.create("z").subdomain_of?(domain) #=> false 1294 | # p Resolv::DNS::Name.create("x.y.z.").subdomain_of?(domain) #=> false 1295 | # p Resolv::DNS::Name.create("w.z").subdomain_of?(domain) #=> false 1296 | # 1297 | 1298 | def subdomain_of?(other) 1299 | raise ArgumentError, "not a domain name: #{other.inspect}" unless Name === other 1300 | return false if @absolute != other.absolute? 1301 | other_len = other.length 1302 | return false if @labels.length <= other_len 1303 | return @labels[-other_len, other_len] == other.to_a 1304 | end 1305 | 1306 | def hash # :nodoc: 1307 | return @labels.hash ^ @absolute.hash 1308 | end 1309 | 1310 | def to_a # :nodoc: 1311 | return @labels 1312 | end 1313 | 1314 | def length # :nodoc: 1315 | return @labels.length 1316 | end 1317 | 1318 | def [](i) # :nodoc: 1319 | return @labels[i] 1320 | end 1321 | 1322 | ## 1323 | # returns the domain name as a string. 1324 | # 1325 | # The domain name doesn't have a trailing dot even if the name object is 1326 | # absolute. 1327 | # 1328 | # Example: 1329 | # 1330 | # p Resolv::DNS::Name.create("x.y.z.").to_s #=> "x.y.z" 1331 | # p Resolv::DNS::Name.create("x.y.z").to_s #=> "x.y.z" 1332 | 1333 | def to_s 1334 | return @labels.join('.') 1335 | end 1336 | end 1337 | 1338 | class Message # :nodoc: 1339 | @@identifier = -1 1340 | 1341 | def initialize(id = (@@identifier += 1) & 0xffff) 1342 | @id = id 1343 | @qr = 0 1344 | @opcode = 0 1345 | @aa = 0 1346 | @tc = 0 1347 | @rd = 0 # recursion desired 1348 | @ra = 0 # recursion available 1349 | @rcode = 0 1350 | @question = [] 1351 | @answer = [] 1352 | @authority = [] 1353 | @additional = [] 1354 | end 1355 | 1356 | attr_accessor :id, :qr, :opcode, :aa, :tc, :rd, :ra, :rcode 1357 | attr_reader :question, :answer, :authority, :additional 1358 | 1359 | def ==(other) 1360 | return @id == other.id && 1361 | @qr == other.qr && 1362 | @opcode == other.opcode && 1363 | @aa == other.aa && 1364 | @tc == other.tc && 1365 | @rd == other.rd && 1366 | @ra == other.ra && 1367 | @rcode == other.rcode && 1368 | @question == other.question && 1369 | @answer == other.answer && 1370 | @authority == other.authority && 1371 | @additional == other.additional 1372 | end 1373 | 1374 | def add_question(name, typeclass) 1375 | @question << [Name.create(name), typeclass] 1376 | end 1377 | 1378 | def each_question 1379 | @question.each {|name, typeclass| 1380 | yield name, typeclass 1381 | } 1382 | end 1383 | 1384 | def add_answer(name, ttl, data) 1385 | @answer << [Name.create(name), ttl, data] 1386 | end 1387 | 1388 | def each_answer 1389 | @answer.each {|name, ttl, data| 1390 | yield name, ttl, data 1391 | } 1392 | end 1393 | 1394 | def add_authority(name, ttl, data) 1395 | @authority << [Name.create(name), ttl, data] 1396 | end 1397 | 1398 | def each_authority 1399 | @authority.each {|name, ttl, data| 1400 | yield name, ttl, data 1401 | } 1402 | end 1403 | 1404 | def add_additional(name, ttl, data) 1405 | @additional << [Name.create(name), ttl, data] 1406 | end 1407 | 1408 | def each_additional 1409 | @additional.each {|name, ttl, data| 1410 | yield name, ttl, data 1411 | } 1412 | end 1413 | 1414 | def each_resource 1415 | each_answer {|name, ttl, data| yield name, ttl, data} 1416 | each_authority {|name, ttl, data| yield name, ttl, data} 1417 | each_additional {|name, ttl, data| yield name, ttl, data} 1418 | end 1419 | 1420 | def encode 1421 | return MessageEncoder.new {|msg| 1422 | msg.put_pack('nnnnnn', 1423 | @id, 1424 | (@qr & 1) << 15 | 1425 | (@opcode & 15) << 11 | 1426 | (@aa & 1) << 10 | 1427 | (@tc & 1) << 9 | 1428 | (@rd & 1) << 8 | 1429 | (@ra & 1) << 7 | 1430 | (@rcode & 15), 1431 | @question.length, 1432 | @answer.length, 1433 | @authority.length, 1434 | @additional.length) 1435 | @question.each {|q| 1436 | name, typeclass = q 1437 | msg.put_name(name) 1438 | msg.put_pack('nn', typeclass::TypeValue, typeclass::ClassValue) 1439 | } 1440 | [@answer, @authority, @additional].each {|rr| 1441 | rr.each {|r| 1442 | name, ttl, data = r 1443 | msg.put_name(name) 1444 | msg.put_pack('nnN', data.class::TypeValue, data.class::ClassValue, ttl) 1445 | msg.put_length16 {data.encode_rdata(msg)} 1446 | } 1447 | } 1448 | }.to_s 1449 | end 1450 | 1451 | class MessageEncoder # :nodoc: 1452 | def initialize 1453 | @data = ''.dup 1454 | @names = {} 1455 | yield self 1456 | end 1457 | 1458 | def to_s 1459 | return @data 1460 | end 1461 | 1462 | def put_bytes(d) 1463 | @data << d 1464 | end 1465 | 1466 | def put_pack(template, *d) 1467 | @data << d.pack(template) 1468 | end 1469 | 1470 | def put_length16 1471 | length_index = @data.length 1472 | @data << "\0\0" 1473 | data_start = @data.length 1474 | yield 1475 | data_end = @data.length 1476 | @data[length_index, 2] = [data_end - data_start].pack("n") 1477 | end 1478 | 1479 | def put_string(d) 1480 | self.put_pack("C", d.length) 1481 | @data << d 1482 | end 1483 | 1484 | def put_string_list(ds) 1485 | ds.each {|d| 1486 | self.put_string(d) 1487 | } 1488 | end 1489 | 1490 | def put_name(d) 1491 | put_labels(d.to_a) 1492 | end 1493 | 1494 | def put_labels(d) 1495 | d.each_index {|i| 1496 | domain = d[i..-1] 1497 | if idx = @names[domain] 1498 | self.put_pack("n", 0xc000 | idx) 1499 | return 1500 | else 1501 | if @data.length < 0x4000 1502 | @names[domain] = @data.length 1503 | end 1504 | self.put_label(d[i]) 1505 | end 1506 | } 1507 | @data << "\0" 1508 | end 1509 | 1510 | def put_label(d) 1511 | self.put_string(d.to_s) 1512 | end 1513 | end 1514 | 1515 | def Message.decode(m) 1516 | o = Message.new(0) 1517 | MessageDecoder.new(m) {|msg| 1518 | id, flag, qdcount, ancount, nscount, arcount = 1519 | msg.get_unpack('nnnnnn') 1520 | o.id = id 1521 | o.qr = (flag >> 15) & 1 1522 | o.opcode = (flag >> 11) & 15 1523 | o.aa = (flag >> 10) & 1 1524 | o.tc = (flag >> 9) & 1 1525 | o.rd = (flag >> 8) & 1 1526 | o.ra = (flag >> 7) & 1 1527 | o.rcode = flag & 15 1528 | (1..qdcount).each { 1529 | name, typeclass = msg.get_question 1530 | o.add_question(name, typeclass) 1531 | } 1532 | (1..ancount).each { 1533 | name, ttl, data = msg.get_rr 1534 | o.add_answer(name, ttl, data) 1535 | } 1536 | (1..nscount).each { 1537 | name, ttl, data = msg.get_rr 1538 | o.add_authority(name, ttl, data) 1539 | } 1540 | (1..arcount).each { 1541 | name, ttl, data = msg.get_rr 1542 | o.add_additional(name, ttl, data) 1543 | } 1544 | } 1545 | return o 1546 | end 1547 | 1548 | class MessageDecoder # :nodoc: 1549 | def initialize(data) 1550 | @data = data 1551 | @index = 0 1552 | @limit = data.bytesize 1553 | yield self 1554 | end 1555 | 1556 | def inspect 1557 | "\#<#{self.class}: #{@data.byteslice(0, @index).inspect} #{@data.byteslice(@index..-1).inspect}>" 1558 | end 1559 | 1560 | def get_length16 1561 | len, = self.get_unpack('n') 1562 | save_limit = @limit 1563 | @limit = @index + len 1564 | d = yield(len) 1565 | if @index < @limit 1566 | raise DecodeError.new("junk exists") 1567 | elsif @limit < @index 1568 | raise DecodeError.new("limit exceeded") 1569 | end 1570 | @limit = save_limit 1571 | return d 1572 | end 1573 | 1574 | def get_bytes(len = @limit - @index) 1575 | raise DecodeError.new("limit exceeded") if @limit < @index + len 1576 | d = @data.byteslice(@index, len) 1577 | @index += len 1578 | return d 1579 | end 1580 | 1581 | def get_unpack(template) 1582 | len = 0 1583 | template.each_byte {|byte| 1584 | byte = "%c" % byte 1585 | case byte 1586 | when ?c, ?C 1587 | len += 1 1588 | when ?n 1589 | len += 2 1590 | when ?N 1591 | len += 4 1592 | else 1593 | raise StandardError.new("unsupported template: '#{byte.chr}' in '#{template}'") 1594 | end 1595 | } 1596 | raise DecodeError.new("limit exceeded") if @limit < @index + len 1597 | arr = @data.unpack("@#{@index}#{template}") 1598 | @index += len 1599 | return arr 1600 | end 1601 | 1602 | def get_string 1603 | raise DecodeError.new("limit exceeded") if @limit <= @index 1604 | len = @data.getbyte(@index) 1605 | raise DecodeError.new("limit exceeded") if @limit < @index + 1 + len 1606 | d = @data.byteslice(@index + 1, len) 1607 | @index += 1 + len 1608 | return d 1609 | end 1610 | 1611 | def get_string_list 1612 | strings = [] 1613 | while @index < @limit 1614 | strings << self.get_string 1615 | end 1616 | strings 1617 | end 1618 | 1619 | def get_name 1620 | return Name.new(self.get_labels) 1621 | end 1622 | 1623 | def get_labels 1624 | prev_index = @index 1625 | save_index = nil 1626 | d = [] 1627 | while true 1628 | raise DecodeError.new("limit exceeded") if @limit <= @index 1629 | case @data.getbyte(@index) 1630 | when 0 1631 | @index += 1 1632 | if save_index 1633 | @index = save_index 1634 | end 1635 | return d 1636 | when 192..255 1637 | idx = self.get_unpack('n')[0] & 0x3fff 1638 | if prev_index <= idx 1639 | raise DecodeError.new("non-backward name pointer") 1640 | end 1641 | prev_index = idx 1642 | if !save_index 1643 | save_index = @index 1644 | end 1645 | @index = idx 1646 | else 1647 | d << self.get_label 1648 | end 1649 | end 1650 | end 1651 | 1652 | def get_label 1653 | return Label::Str.new(self.get_string) 1654 | end 1655 | 1656 | def get_question 1657 | name = self.get_name 1658 | type, klass = self.get_unpack("nn") 1659 | return name, Resource.get_class(type, klass) 1660 | end 1661 | 1662 | def get_rr 1663 | name = self.get_name 1664 | type, klass, ttl = self.get_unpack('nnN') 1665 | typeclass = Resource.get_class(type, klass) 1666 | res = self.get_length16 do 1667 | begin 1668 | typeclass.decode_rdata self 1669 | rescue => e 1670 | raise DecodeError, e.message, e.backtrace 1671 | end 1672 | end 1673 | res.instance_variable_set :@ttl, ttl 1674 | return name, ttl, res 1675 | end 1676 | end 1677 | end 1678 | 1679 | ## 1680 | # A DNS query abstract class. 1681 | 1682 | class Query 1683 | def encode_rdata(msg) # :nodoc: 1684 | raise EncodeError.new("#{self.class} is query.") 1685 | end 1686 | 1687 | def self.decode_rdata(msg) # :nodoc: 1688 | raise DecodeError.new("#{self.class} is query.") 1689 | end 1690 | end 1691 | 1692 | ## 1693 | # A DNS resource abstract class. 1694 | 1695 | class Resource < Query 1696 | 1697 | ## 1698 | # Remaining Time To Live for this Resource. 1699 | 1700 | attr_reader :ttl 1701 | 1702 | ClassHash = {} # :nodoc: 1703 | 1704 | def encode_rdata(msg) # :nodoc: 1705 | raise NotImplementedError.new 1706 | end 1707 | 1708 | def self.decode_rdata(msg) # :nodoc: 1709 | raise NotImplementedError.new 1710 | end 1711 | 1712 | def ==(other) # :nodoc: 1713 | return false unless self.class == other.class 1714 | s_ivars = self.instance_variables 1715 | s_ivars.sort! 1716 | s_ivars.delete :@ttl 1717 | o_ivars = other.instance_variables 1718 | o_ivars.sort! 1719 | o_ivars.delete :@ttl 1720 | return s_ivars == o_ivars && 1721 | s_ivars.collect {|name| self.instance_variable_get name} == 1722 | o_ivars.collect {|name| other.instance_variable_get name} 1723 | end 1724 | 1725 | def eql?(other) # :nodoc: 1726 | return self == other 1727 | end 1728 | 1729 | def hash # :nodoc: 1730 | h = 0 1731 | vars = self.instance_variables 1732 | vars.delete :@ttl 1733 | vars.each {|name| 1734 | h ^= self.instance_variable_get(name).hash 1735 | } 1736 | return h 1737 | end 1738 | 1739 | def self.get_class(type_value, class_value) # :nodoc: 1740 | return ClassHash[[type_value, class_value]] || 1741 | Generic.create(type_value, class_value) 1742 | end 1743 | 1744 | ## 1745 | # A generic resource abstract class. 1746 | 1747 | class Generic < Resource 1748 | 1749 | ## 1750 | # Creates a new generic resource. 1751 | 1752 | def initialize(data) 1753 | @data = data 1754 | end 1755 | 1756 | ## 1757 | # Data for this generic resource. 1758 | 1759 | attr_reader :data 1760 | 1761 | def encode_rdata(msg) # :nodoc: 1762 | msg.put_bytes(data) 1763 | end 1764 | 1765 | def self.decode_rdata(msg) # :nodoc: 1766 | return self.new(msg.get_bytes) 1767 | end 1768 | 1769 | def self.create(type_value, class_value) # :nodoc: 1770 | c = Class.new(Generic) 1771 | c.const_set(:TypeValue, type_value) 1772 | c.const_set(:ClassValue, class_value) 1773 | Generic.const_set("Type#{type_value}_Class#{class_value}", c) 1774 | ClassHash[[type_value, class_value]] = c 1775 | return c 1776 | end 1777 | end 1778 | 1779 | ## 1780 | # Domain Name resource abstract class. 1781 | 1782 | class DomainName < Resource 1783 | 1784 | ## 1785 | # Creates a new DomainName from +name+. 1786 | 1787 | def initialize(name) 1788 | @name = name 1789 | end 1790 | 1791 | ## 1792 | # The name of this DomainName. 1793 | 1794 | attr_reader :name 1795 | 1796 | def encode_rdata(msg) # :nodoc: 1797 | msg.put_name(@name) 1798 | end 1799 | 1800 | def self.decode_rdata(msg) # :nodoc: 1801 | return self.new(msg.get_name) 1802 | end 1803 | end 1804 | 1805 | # Standard (class generic) RRs 1806 | 1807 | ClassValue = nil # :nodoc: 1808 | 1809 | ## 1810 | # An authoritative name server. 1811 | 1812 | class NS < DomainName 1813 | TypeValue = 2 # :nodoc: 1814 | end 1815 | 1816 | ## 1817 | # The canonical name for an alias. 1818 | 1819 | class CNAME < DomainName 1820 | TypeValue = 5 # :nodoc: 1821 | end 1822 | 1823 | ## 1824 | # Start Of Authority resource. 1825 | 1826 | class SOA < Resource 1827 | 1828 | TypeValue = 6 # :nodoc: 1829 | 1830 | ## 1831 | # Creates a new SOA record. See the attr documentation for the 1832 | # details of each argument. 1833 | 1834 | def initialize(mname, rname, serial, refresh, retry_, expire, minimum) 1835 | @mname = mname 1836 | @rname = rname 1837 | @serial = serial 1838 | @refresh = refresh 1839 | @retry = retry_ 1840 | @expire = expire 1841 | @minimum = minimum 1842 | end 1843 | 1844 | ## 1845 | # Name of the host where the master zone file for this zone resides. 1846 | 1847 | attr_reader :mname 1848 | 1849 | ## 1850 | # The person responsible for this domain name. 1851 | 1852 | attr_reader :rname 1853 | 1854 | ## 1855 | # The version number of the zone file. 1856 | 1857 | attr_reader :serial 1858 | 1859 | ## 1860 | # How often, in seconds, a secondary name server is to check for 1861 | # updates from the primary name server. 1862 | 1863 | attr_reader :refresh 1864 | 1865 | ## 1866 | # How often, in seconds, a secondary name server is to retry after a 1867 | # failure to check for a refresh. 1868 | 1869 | attr_reader :retry 1870 | 1871 | ## 1872 | # Time in seconds that a secondary name server is to use the data 1873 | # before refreshing from the primary name server. 1874 | 1875 | attr_reader :expire 1876 | 1877 | ## 1878 | # The minimum number of seconds to be used for TTL values in RRs. 1879 | 1880 | attr_reader :minimum 1881 | 1882 | def encode_rdata(msg) # :nodoc: 1883 | msg.put_name(@mname) 1884 | msg.put_name(@rname) 1885 | msg.put_pack('NNNNN', @serial, @refresh, @retry, @expire, @minimum) 1886 | end 1887 | 1888 | def self.decode_rdata(msg) # :nodoc: 1889 | mname = msg.get_name 1890 | rname = msg.get_name 1891 | serial, refresh, retry_, expire, minimum = msg.get_unpack('NNNNN') 1892 | return self.new( 1893 | mname, rname, serial, refresh, retry_, expire, minimum) 1894 | end 1895 | end 1896 | 1897 | ## 1898 | # A Pointer to another DNS name. 1899 | 1900 | class PTR < DomainName 1901 | TypeValue = 12 # :nodoc: 1902 | end 1903 | 1904 | ## 1905 | # Host Information resource. 1906 | 1907 | class HINFO < Resource 1908 | 1909 | TypeValue = 13 # :nodoc: 1910 | 1911 | ## 1912 | # Creates a new HINFO running +os+ on +cpu+. 1913 | 1914 | def initialize(cpu, os) 1915 | @cpu = cpu 1916 | @os = os 1917 | end 1918 | 1919 | ## 1920 | # CPU architecture for this resource. 1921 | 1922 | attr_reader :cpu 1923 | 1924 | ## 1925 | # Operating system for this resource. 1926 | 1927 | attr_reader :os 1928 | 1929 | def encode_rdata(msg) # :nodoc: 1930 | msg.put_string(@cpu) 1931 | msg.put_string(@os) 1932 | end 1933 | 1934 | def self.decode_rdata(msg) # :nodoc: 1935 | cpu = msg.get_string 1936 | os = msg.get_string 1937 | return self.new(cpu, os) 1938 | end 1939 | end 1940 | 1941 | ## 1942 | # Mailing list or mailbox information. 1943 | 1944 | class MINFO < Resource 1945 | 1946 | TypeValue = 14 # :nodoc: 1947 | 1948 | def initialize(rmailbx, emailbx) 1949 | @rmailbx = rmailbx 1950 | @emailbx = emailbx 1951 | end 1952 | 1953 | ## 1954 | # Domain name responsible for this mail list or mailbox. 1955 | 1956 | attr_reader :rmailbx 1957 | 1958 | ## 1959 | # Mailbox to use for error messages related to the mail list or mailbox. 1960 | 1961 | attr_reader :emailbx 1962 | 1963 | def encode_rdata(msg) # :nodoc: 1964 | msg.put_name(@rmailbx) 1965 | msg.put_name(@emailbx) 1966 | end 1967 | 1968 | def self.decode_rdata(msg) # :nodoc: 1969 | rmailbx = msg.get_string 1970 | emailbx = msg.get_string 1971 | return self.new(rmailbx, emailbx) 1972 | end 1973 | end 1974 | 1975 | ## 1976 | # Mail Exchanger resource. 1977 | 1978 | class MX < Resource 1979 | 1980 | TypeValue= 15 # :nodoc: 1981 | 1982 | ## 1983 | # Creates a new MX record with +preference+, accepting mail at 1984 | # +exchange+. 1985 | 1986 | def initialize(preference, exchange) 1987 | @preference = preference 1988 | @exchange = exchange 1989 | end 1990 | 1991 | ## 1992 | # The preference for this MX. 1993 | 1994 | attr_reader :preference 1995 | 1996 | ## 1997 | # The host of this MX. 1998 | 1999 | attr_reader :exchange 2000 | 2001 | def encode_rdata(msg) # :nodoc: 2002 | msg.put_pack('n', @preference) 2003 | msg.put_name(@exchange) 2004 | end 2005 | 2006 | def self.decode_rdata(msg) # :nodoc: 2007 | preference, = msg.get_unpack('n') 2008 | exchange = msg.get_name 2009 | return self.new(preference, exchange) 2010 | end 2011 | end 2012 | 2013 | ## 2014 | # Unstructured text resource. 2015 | 2016 | class TXT < Resource 2017 | 2018 | TypeValue = 16 # :nodoc: 2019 | 2020 | def initialize(first_string, *rest_strings) 2021 | @strings = [first_string, *rest_strings] 2022 | end 2023 | 2024 | ## 2025 | # Returns an Array of Strings for this TXT record. 2026 | 2027 | attr_reader :strings 2028 | 2029 | ## 2030 | # Returns the concatenated string from +strings+. 2031 | 2032 | def data 2033 | @strings.join("") 2034 | end 2035 | 2036 | def encode_rdata(msg) # :nodoc: 2037 | msg.put_string_list(@strings) 2038 | end 2039 | 2040 | def self.decode_rdata(msg) # :nodoc: 2041 | strings = msg.get_string_list 2042 | return self.new(*strings) 2043 | end 2044 | end 2045 | 2046 | ## 2047 | # Location resource 2048 | 2049 | class LOC < Resource 2050 | 2051 | TypeValue = 29 # :nodoc: 2052 | 2053 | def initialize(version, ssize, hprecision, vprecision, latitude, longitude, altitude) 2054 | @version = version 2055 | @ssize = Resolv::LOC::Size.create(ssize) 2056 | @hprecision = Resolv::LOC::Size.create(hprecision) 2057 | @vprecision = Resolv::LOC::Size.create(vprecision) 2058 | @latitude = Resolv::LOC::Coord.create(latitude) 2059 | @longitude = Resolv::LOC::Coord.create(longitude) 2060 | @altitude = Resolv::LOC::Alt.create(altitude) 2061 | end 2062 | 2063 | ## 2064 | # Returns the version value for this LOC record which should always be 00 2065 | 2066 | attr_reader :version 2067 | 2068 | ## 2069 | # The spherical size of this LOC 2070 | # in meters using scientific notation as 2 integers of XeY 2071 | 2072 | attr_reader :ssize 2073 | 2074 | ## 2075 | # The horizontal precision using ssize type values 2076 | # in meters using scientific notation as 2 integers of XeY 2077 | # for precision use value/2 e.g. 2m = +/-1m 2078 | 2079 | attr_reader :hprecision 2080 | 2081 | ## 2082 | # The vertical precision using ssize type values 2083 | # in meters using scientific notation as 2 integers of XeY 2084 | # for precision use value/2 e.g. 2m = +/-1m 2085 | 2086 | attr_reader :vprecision 2087 | 2088 | ## 2089 | # The latitude for this LOC where 2**31 is the equator 2090 | # in thousandths of an arc second as an unsigned 32bit integer 2091 | 2092 | attr_reader :latitude 2093 | 2094 | ## 2095 | # The longitude for this LOC where 2**31 is the prime meridian 2096 | # in thousandths of an arc second as an unsigned 32bit integer 2097 | 2098 | attr_reader :longitude 2099 | 2100 | ## 2101 | # The altitude of the LOC above a reference sphere whose surface sits 100km below the WGS84 spheroid 2102 | # in centimeters as an unsigned 32bit integer 2103 | 2104 | attr_reader :altitude 2105 | 2106 | 2107 | def encode_rdata(msg) # :nodoc: 2108 | msg.put_bytes(@version) 2109 | msg.put_bytes(@ssize.scalar) 2110 | msg.put_bytes(@hprecision.scalar) 2111 | msg.put_bytes(@vprecision.scalar) 2112 | msg.put_bytes(@latitude.coordinates) 2113 | msg.put_bytes(@longitude.coordinates) 2114 | msg.put_bytes(@altitude.altitude) 2115 | end 2116 | 2117 | def self.decode_rdata(msg) # :nodoc: 2118 | version = msg.get_bytes(1) 2119 | ssize = msg.get_bytes(1) 2120 | hprecision = msg.get_bytes(1) 2121 | vprecision = msg.get_bytes(1) 2122 | latitude = msg.get_bytes(4) 2123 | longitude = msg.get_bytes(4) 2124 | altitude = msg.get_bytes(4) 2125 | return self.new( 2126 | version, 2127 | Resolv::LOC::Size.new(ssize), 2128 | Resolv::LOC::Size.new(hprecision), 2129 | Resolv::LOC::Size.new(vprecision), 2130 | Resolv::LOC::Coord.new(latitude,"lat"), 2131 | Resolv::LOC::Coord.new(longitude,"lon"), 2132 | Resolv::LOC::Alt.new(altitude) 2133 | ) 2134 | end 2135 | end 2136 | 2137 | ## 2138 | # A Query type requesting any RR. 2139 | 2140 | class ANY < Query 2141 | TypeValue = 255 # :nodoc: 2142 | end 2143 | 2144 | ClassInsensitiveTypes = [ # :nodoc: 2145 | NS, CNAME, SOA, PTR, HINFO, MINFO, MX, TXT, LOC, ANY 2146 | ] 2147 | 2148 | ## 2149 | # module IN contains ARPA Internet specific RRs. 2150 | 2151 | module IN 2152 | 2153 | ClassValue = 1 # :nodoc: 2154 | 2155 | ClassInsensitiveTypes.each {|s| 2156 | c = Class.new(s) 2157 | c.const_set(:TypeValue, s::TypeValue) 2158 | c.const_set(:ClassValue, ClassValue) 2159 | ClassHash[[s::TypeValue, ClassValue]] = c 2160 | self.const_set(s.name.sub(/.*::/, ''), c) 2161 | } 2162 | 2163 | ## 2164 | # IPv4 Address resource 2165 | 2166 | class A < Resource 2167 | TypeValue = 1 2168 | ClassValue = IN::ClassValue 2169 | ClassHash[[TypeValue, ClassValue]] = self # :nodoc: 2170 | 2171 | ## 2172 | # Creates a new A for +address+. 2173 | 2174 | def initialize(address) 2175 | @address = IPv4.create(address) 2176 | end 2177 | 2178 | ## 2179 | # The Resolv::IPv4 address for this A. 2180 | 2181 | attr_reader :address 2182 | 2183 | def encode_rdata(msg) # :nodoc: 2184 | msg.put_bytes(@address.address) 2185 | end 2186 | 2187 | def self.decode_rdata(msg) # :nodoc: 2188 | return self.new(IPv4.new(msg.get_bytes(4))) 2189 | end 2190 | end 2191 | 2192 | ## 2193 | # Well Known Service resource. 2194 | 2195 | class WKS < Resource 2196 | TypeValue = 11 2197 | ClassValue = IN::ClassValue 2198 | ClassHash[[TypeValue, ClassValue]] = self # :nodoc: 2199 | 2200 | def initialize(address, protocol, bitmap) 2201 | @address = IPv4.create(address) 2202 | @protocol = protocol 2203 | @bitmap = bitmap 2204 | end 2205 | 2206 | ## 2207 | # The host these services run on. 2208 | 2209 | attr_reader :address 2210 | 2211 | ## 2212 | # IP protocol number for these services. 2213 | 2214 | attr_reader :protocol 2215 | 2216 | ## 2217 | # A bit map of enabled services on this host. 2218 | # 2219 | # If protocol is 6 (TCP) then the 26th bit corresponds to the SMTP 2220 | # service (port 25). If this bit is set, then an SMTP server should 2221 | # be listening on TCP port 25; if zero, SMTP service is not 2222 | # supported. 2223 | 2224 | attr_reader :bitmap 2225 | 2226 | def encode_rdata(msg) # :nodoc: 2227 | msg.put_bytes(@address.address) 2228 | msg.put_pack("n", @protocol) 2229 | msg.put_bytes(@bitmap) 2230 | end 2231 | 2232 | def self.decode_rdata(msg) # :nodoc: 2233 | address = IPv4.new(msg.get_bytes(4)) 2234 | protocol, = msg.get_unpack("n") 2235 | bitmap = msg.get_bytes 2236 | return self.new(address, protocol, bitmap) 2237 | end 2238 | end 2239 | 2240 | ## 2241 | # An IPv6 address record. 2242 | 2243 | class AAAA < Resource 2244 | TypeValue = 28 2245 | ClassValue = IN::ClassValue 2246 | ClassHash[[TypeValue, ClassValue]] = self # :nodoc: 2247 | 2248 | ## 2249 | # Creates a new AAAA for +address+. 2250 | 2251 | def initialize(address) 2252 | @address = IPv6.create(address) 2253 | end 2254 | 2255 | ## 2256 | # The Resolv::IPv6 address for this AAAA. 2257 | 2258 | attr_reader :address 2259 | 2260 | def encode_rdata(msg) # :nodoc: 2261 | msg.put_bytes(@address.address) 2262 | end 2263 | 2264 | def self.decode_rdata(msg) # :nodoc: 2265 | return self.new(IPv6.new(msg.get_bytes(16))) 2266 | end 2267 | end 2268 | 2269 | ## 2270 | # SRV resource record defined in RFC 2782 2271 | # 2272 | # These records identify the hostname and port that a service is 2273 | # available at. 2274 | 2275 | class SRV < Resource 2276 | TypeValue = 33 2277 | ClassValue = IN::ClassValue 2278 | ClassHash[[TypeValue, ClassValue]] = self # :nodoc: 2279 | 2280 | # Create a SRV resource record. 2281 | # 2282 | # See the documentation for #priority, #weight, #port and #target 2283 | # for +priority+, +weight+, +port and +target+ respectively. 2284 | 2285 | def initialize(priority, weight, port, target) 2286 | @priority = priority.to_int 2287 | @weight = weight.to_int 2288 | @port = port.to_int 2289 | @target = Name.create(target) 2290 | end 2291 | 2292 | # The priority of this target host. 2293 | # 2294 | # A client MUST attempt to contact the target host with the 2295 | # lowest-numbered priority it can reach; target hosts with the same 2296 | # priority SHOULD be tried in an order defined by the weight field. 2297 | # The range is 0-65535. Note that it is not widely implemented and 2298 | # should be set to zero. 2299 | 2300 | attr_reader :priority 2301 | 2302 | # A server selection mechanism. 2303 | # 2304 | # The weight field specifies a relative weight for entries with the 2305 | # same priority. Larger weights SHOULD be given a proportionately 2306 | # higher probability of being selected. The range of this number is 2307 | # 0-65535. Domain administrators SHOULD use Weight 0 when there 2308 | # isn't any server selection to do, to make the RR easier to read 2309 | # for humans (less noisy). Note that it is not widely implemented 2310 | # and should be set to zero. 2311 | 2312 | attr_reader :weight 2313 | 2314 | # The port on this target host of this service. 2315 | # 2316 | # The range is 0-65535. 2317 | 2318 | attr_reader :port 2319 | 2320 | # The domain name of the target host. 2321 | # 2322 | # A target of "." means that the service is decidedly not available 2323 | # at this domain. 2324 | 2325 | attr_reader :target 2326 | 2327 | def encode_rdata(msg) # :nodoc: 2328 | msg.put_pack("n", @priority) 2329 | msg.put_pack("n", @weight) 2330 | msg.put_pack("n", @port) 2331 | msg.put_name(@target) 2332 | end 2333 | 2334 | def self.decode_rdata(msg) # :nodoc: 2335 | priority, = msg.get_unpack("n") 2336 | weight, = msg.get_unpack("n") 2337 | port, = msg.get_unpack("n") 2338 | target = msg.get_name 2339 | return self.new(priority, weight, port, target) 2340 | end 2341 | end 2342 | end 2343 | end 2344 | end 2345 | 2346 | ## 2347 | # A Resolv::DNS IPv4 address. 2348 | 2349 | class IPv4 2350 | 2351 | ## 2352 | # Regular expression IPv4 addresses must match. 2353 | 2354 | Regex256 = /0 2355 | |1(?:[0-9][0-9]?)? 2356 | |2(?:[0-4][0-9]?|5[0-5]?|[6-9])? 2357 | |[3-9][0-9]?/x 2358 | Regex = /\A(#{Regex256})\.(#{Regex256})\.(#{Regex256})\.(#{Regex256})\z/ 2359 | 2360 | def self.create(arg) 2361 | case arg 2362 | when IPv4 2363 | return arg 2364 | when Regex 2365 | if (0..255) === (a = $1.to_i) && 2366 | (0..255) === (b = $2.to_i) && 2367 | (0..255) === (c = $3.to_i) && 2368 | (0..255) === (d = $4.to_i) 2369 | return self.new([a, b, c, d].pack("CCCC")) 2370 | else 2371 | raise ArgumentError.new("IPv4 address with invalid value: " + arg) 2372 | end 2373 | else 2374 | raise ArgumentError.new("cannot interpret as IPv4 address: #{arg.inspect}") 2375 | end 2376 | end 2377 | 2378 | def initialize(address) # :nodoc: 2379 | unless address.kind_of?(String) 2380 | raise ArgumentError, 'IPv4 address must be a string' 2381 | end 2382 | unless address.length == 4 2383 | raise ArgumentError, "IPv4 address expects 4 bytes but #{address.length} bytes" 2384 | end 2385 | @address = address 2386 | end 2387 | 2388 | ## 2389 | # A String representation of this IPv4 address. 2390 | 2391 | ## 2392 | # The raw IPv4 address as a String. 2393 | 2394 | attr_reader :address 2395 | 2396 | def to_s # :nodoc: 2397 | return sprintf("%d.%d.%d.%d", *@address.unpack("CCCC")) 2398 | end 2399 | 2400 | def inspect # :nodoc: 2401 | return "#<#{self.class} #{self}>" 2402 | end 2403 | 2404 | ## 2405 | # Turns this IPv4 address into a Resolv::DNS::Name. 2406 | 2407 | def to_name 2408 | return DNS::Name.create( 2409 | '%d.%d.%d.%d.in-addr.arpa.' % @address.unpack('CCCC').reverse) 2410 | end 2411 | 2412 | def ==(other) # :nodoc: 2413 | return @address == other.address 2414 | end 2415 | 2416 | def eql?(other) # :nodoc: 2417 | return self == other 2418 | end 2419 | 2420 | def hash # :nodoc: 2421 | return @address.hash 2422 | end 2423 | end 2424 | 2425 | ## 2426 | # A Resolv::DNS IPv6 address. 2427 | 2428 | class IPv6 2429 | 2430 | ## 2431 | # IPv6 address format a:b:c:d:e:f:g:h 2432 | Regex_8Hex = /\A 2433 | (?:[0-9A-Fa-f]{1,4}:){7} 2434 | [0-9A-Fa-f]{1,4} 2435 | \z/x 2436 | 2437 | ## 2438 | # Compressed IPv6 address format a::b 2439 | 2440 | Regex_CompressedHex = /\A 2441 | ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) :: 2442 | ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) 2443 | \z/x 2444 | 2445 | ## 2446 | # IPv4 mapped IPv6 address format a:b:c:d:e:f:w.x.y.z 2447 | 2448 | Regex_6Hex4Dec = /\A 2449 | ((?:[0-9A-Fa-f]{1,4}:){6,6}) 2450 | (\d+)\.(\d+)\.(\d+)\.(\d+) 2451 | \z/x 2452 | 2453 | ## 2454 | # Compressed IPv4 mapped IPv6 address format a::b:w.x.y.z 2455 | 2456 | Regex_CompressedHex4Dec = /\A 2457 | ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) :: 2458 | ((?:[0-9A-Fa-f]{1,4}:)*) 2459 | (\d+)\.(\d+)\.(\d+)\.(\d+) 2460 | \z/x 2461 | 2462 | ## 2463 | # IPv6 link local address format fe80:b:c:d:e:f:g:h%em1 2464 | Regex_8HexLinkLocal = /\A 2465 | [Ff][Ee]80 2466 | (?::[0-9A-Fa-f]{1,4}){7} 2467 | %[0-9A-Za-z]+ 2468 | \z/x 2469 | 2470 | ## 2471 | # Compressed IPv6 link local address format fe80::b%em1 2472 | 2473 | Regex_CompressedHexLinkLocal = /\A 2474 | [Ff][Ee]80: 2475 | (?: 2476 | ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) :: 2477 | ((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) 2478 | | 2479 | :((?:[0-9A-Fa-f]{1,4}(?::[0-9A-Fa-f]{1,4})*)?) 2480 | )? 2481 | :[0-9A-Fa-f]{1,4}%[0-9A-Za-z.]+ 2482 | \z/x 2483 | 2484 | ## 2485 | # A composite IPv6 address Regexp. 2486 | 2487 | Regex = / 2488 | (?:#{Regex_8Hex}) | 2489 | (?:#{Regex_CompressedHex}) | 2490 | (?:#{Regex_6Hex4Dec}) | 2491 | (?:#{Regex_CompressedHex4Dec}) | 2492 | (?:#{Regex_8HexLinkLocal}) | 2493 | (?:#{Regex_CompressedHexLinkLocal}) 2494 | /x 2495 | 2496 | ## 2497 | # Creates a new IPv6 address from +arg+ which may be: 2498 | # 2499 | # IPv6:: returns +arg+. 2500 | # String:: +arg+ must match one of the IPv6::Regex* constants 2501 | 2502 | def self.create(arg) 2503 | case arg 2504 | when IPv6 2505 | return arg 2506 | when String 2507 | address = ''.b 2508 | if Regex_8Hex =~ arg 2509 | arg.scan(/[0-9A-Fa-f]+/) {|hex| address << [hex.hex].pack('n')} 2510 | elsif Regex_CompressedHex =~ arg 2511 | prefix = $1 2512 | suffix = $2 2513 | a1 = ''.b 2514 | a2 = ''.b 2515 | prefix.scan(/[0-9A-Fa-f]+/) {|hex| a1 << [hex.hex].pack('n')} 2516 | suffix.scan(/[0-9A-Fa-f]+/) {|hex| a2 << [hex.hex].pack('n')} 2517 | omitlen = 16 - a1.length - a2.length 2518 | address << a1 << "\0" * omitlen << a2 2519 | elsif Regex_6Hex4Dec =~ arg 2520 | prefix, a, b, c, d = $1, $2.to_i, $3.to_i, $4.to_i, $5.to_i 2521 | if (0..255) === a && (0..255) === b && (0..255) === c && (0..255) === d 2522 | prefix.scan(/[0-9A-Fa-f]+/) {|hex| address << [hex.hex].pack('n')} 2523 | address << [a, b, c, d].pack('CCCC') 2524 | else 2525 | raise ArgumentError.new("not numeric IPv6 address: " + arg) 2526 | end 2527 | elsif Regex_CompressedHex4Dec =~ arg 2528 | prefix, suffix, a, b, c, d = $1, $2, $3.to_i, $4.to_i, $5.to_i, $6.to_i 2529 | if (0..255) === a && (0..255) === b && (0..255) === c && (0..255) === d 2530 | a1 = ''.b 2531 | a2 = ''.b 2532 | prefix.scan(/[0-9A-Fa-f]+/) {|hex| a1 << [hex.hex].pack('n')} 2533 | suffix.scan(/[0-9A-Fa-f]+/) {|hex| a2 << [hex.hex].pack('n')} 2534 | omitlen = 12 - a1.length - a2.length 2535 | address << a1 << "\0" * omitlen << a2 << [a, b, c, d].pack('CCCC') 2536 | else 2537 | raise ArgumentError.new("not numeric IPv6 address: " + arg) 2538 | end 2539 | else 2540 | raise ArgumentError.new("not numeric IPv6 address: " + arg) 2541 | end 2542 | return IPv6.new(address) 2543 | else 2544 | raise ArgumentError.new("cannot interpret as IPv6 address: #{arg.inspect}") 2545 | end 2546 | end 2547 | 2548 | def initialize(address) # :nodoc: 2549 | unless address.kind_of?(String) && address.length == 16 2550 | raise ArgumentError.new('IPv6 address must be 16 bytes') 2551 | end 2552 | @address = address 2553 | end 2554 | 2555 | ## 2556 | # The raw IPv6 address as a String. 2557 | 2558 | attr_reader :address 2559 | 2560 | def to_s # :nodoc: 2561 | address = sprintf("%x:%x:%x:%x:%x:%x:%x:%x", *@address.unpack("nnnnnnnn")) 2562 | unless address.sub!(/(^|:)0(:0)+(:|$)/, '::') 2563 | address.sub!(/(^|:)0(:|$)/, '::') 2564 | end 2565 | return address 2566 | end 2567 | 2568 | def inspect # :nodoc: 2569 | return "#<#{self.class} #{self}>" 2570 | end 2571 | 2572 | ## 2573 | # Turns this IPv6 address into a Resolv::DNS::Name. 2574 | #-- 2575 | # ip6.arpa should be searched too. [RFC3152] 2576 | 2577 | def to_name 2578 | return DNS::Name.new( 2579 | @address.unpack("H32")[0].split(//).reverse + ['ip6', 'arpa']) 2580 | end 2581 | 2582 | def ==(other) # :nodoc: 2583 | return @address == other.address 2584 | end 2585 | 2586 | def eql?(other) # :nodoc: 2587 | return self == other 2588 | end 2589 | 2590 | def hash # :nodoc: 2591 | return @address.hash 2592 | end 2593 | end 2594 | 2595 | ## 2596 | # Resolv::MDNS is a one-shot Multicast DNS (mDNS) resolver. It blindly 2597 | # makes queries to the mDNS addresses without understanding anything about 2598 | # multicast ports. 2599 | # 2600 | # Information taken form the following places: 2601 | # 2602 | # * RFC 6762 2603 | 2604 | class MDNS < DNS 2605 | 2606 | ## 2607 | # Default mDNS Port 2608 | 2609 | Port = 5353 2610 | 2611 | ## 2612 | # Default IPv4 mDNS address 2613 | 2614 | AddressV4 = '224.0.0.251' 2615 | 2616 | ## 2617 | # Default IPv6 mDNS address 2618 | 2619 | AddressV6 = 'ff02::fb' 2620 | 2621 | ## 2622 | # Default mDNS addresses 2623 | 2624 | Addresses = [ 2625 | [AddressV4, Port], 2626 | [AddressV6, Port], 2627 | ] 2628 | 2629 | ## 2630 | # Creates a new one-shot Multicast DNS (mDNS) resolver. 2631 | # 2632 | # +config_info+ can be: 2633 | # 2634 | # nil:: 2635 | # Uses the default mDNS addresses 2636 | # 2637 | # Hash:: 2638 | # Must contain :nameserver or :nameserver_port like 2639 | # Resolv::DNS#initialize. 2640 | 2641 | def initialize(config_info=nil) 2642 | if config_info then 2643 | super({ nameserver_port: Addresses }.merge(config_info)) 2644 | else 2645 | super(nameserver_port: Addresses) 2646 | end 2647 | end 2648 | 2649 | ## 2650 | # Iterates over all IP addresses for +name+ retrieved from the mDNS 2651 | # resolver, provided name ends with "local". If the name does not end in 2652 | # "local" no records will be returned. 2653 | # 2654 | # +name+ can be a Resolv::DNS::Name or a String. Retrieved addresses will 2655 | # be a Resolv::IPv4 or Resolv::IPv6 2656 | 2657 | def each_address(name) 2658 | name = Resolv::DNS::Name.create(name) 2659 | 2660 | return unless name[-1].to_s == 'local' 2661 | 2662 | super(name) 2663 | end 2664 | 2665 | def make_udp_requester # :nodoc: 2666 | nameserver_port = @config.nameserver_port 2667 | Requester::MDNSOneShot.new(*nameserver_port) 2668 | end 2669 | 2670 | end 2671 | 2672 | module LOC 2673 | 2674 | ## 2675 | # A Resolv::LOC::Size 2676 | 2677 | class Size 2678 | 2679 | Regex = /^(\d+\.*\d*)[m]$/ 2680 | 2681 | ## 2682 | # Creates a new LOC::Size from +arg+ which may be: 2683 | # 2684 | # LOC::Size:: returns +arg+. 2685 | # String:: +arg+ must match the LOC::Size::Regex constant 2686 | 2687 | def self.create(arg) 2688 | case arg 2689 | when Size 2690 | return arg 2691 | when String 2692 | scalar = '' 2693 | if Regex =~ arg 2694 | scalar = [(($1.to_f*(1e2)).to_i.to_s[0].to_i*(2**4)+(($1.to_f*(1e2)).to_i.to_s.length-1))].pack("C") 2695 | else 2696 | raise ArgumentError.new("not a properly formed Size string: " + arg) 2697 | end 2698 | return Size.new(scalar) 2699 | else 2700 | raise ArgumentError.new("cannot interpret as Size: #{arg.inspect}") 2701 | end 2702 | end 2703 | 2704 | def initialize(scalar) 2705 | @scalar = scalar 2706 | end 2707 | 2708 | ## 2709 | # The raw size 2710 | 2711 | attr_reader :scalar 2712 | 2713 | def to_s # :nodoc: 2714 | s = @scalar.unpack("H2").join.to_s 2715 | return ((s[0].to_i)*(10**(s[1].to_i-2))).to_s << "m" 2716 | end 2717 | 2718 | def inspect # :nodoc: 2719 | return "#<#{self.class} #{self}>" 2720 | end 2721 | 2722 | def ==(other) # :nodoc: 2723 | return @scalar == other.scalar 2724 | end 2725 | 2726 | def eql?(other) # :nodoc: 2727 | return self == other 2728 | end 2729 | 2730 | def hash # :nodoc: 2731 | return @scalar.hash 2732 | end 2733 | 2734 | end 2735 | 2736 | ## 2737 | # A Resolv::LOC::Coord 2738 | 2739 | class Coord 2740 | 2741 | Regex = /^(\d+)\s(\d+)\s(\d+\.\d+)\s([NESW])$/ 2742 | 2743 | ## 2744 | # Creates a new LOC::Coord from +arg+ which may be: 2745 | # 2746 | # LOC::Coord:: returns +arg+. 2747 | # String:: +arg+ must match the LOC::Coord::Regex constant 2748 | 2749 | def self.create(arg) 2750 | case arg 2751 | when Coord 2752 | return arg 2753 | when String 2754 | coordinates = '' 2755 | if Regex =~ arg && $1.to_f < 180 2756 | m = $~ 2757 | hemi = (m[4][/[NE]/]) || (m[4][/[SW]/]) ? 1 : -1 2758 | coordinates = [ ((m[1].to_i*(36e5)) + (m[2].to_i*(6e4)) + 2759 | (m[3].to_f*(1e3))) * hemi+(2**31) ].pack("N") 2760 | orientation = m[4][/[NS]/] ? 'lat' : 'lon' 2761 | else 2762 | raise ArgumentError.new("not a properly formed Coord string: " + arg) 2763 | end 2764 | return Coord.new(coordinates,orientation) 2765 | else 2766 | raise ArgumentError.new("cannot interpret as Coord: #{arg.inspect}") 2767 | end 2768 | end 2769 | 2770 | def initialize(coordinates,orientation) 2771 | unless coordinates.kind_of?(String) 2772 | raise ArgumentError.new("Coord must be a 32bit unsigned integer in hex format: #{coordinates.inspect}") 2773 | end 2774 | unless orientation.kind_of?(String) && orientation[/^lon$|^lat$/] 2775 | raise ArgumentError.new('Coord expects orientation to be a String argument of "lat" or "lon"') 2776 | end 2777 | @coordinates = coordinates 2778 | @orientation = orientation 2779 | end 2780 | 2781 | ## 2782 | # The raw coordinates 2783 | 2784 | attr_reader :coordinates 2785 | 2786 | ## The orientation of the hemisphere as 'lat' or 'lon' 2787 | 2788 | attr_reader :orientation 2789 | 2790 | def to_s # :nodoc: 2791 | c = @coordinates.unpack("N").join.to_i 2792 | val = (c - (2**31)).abs 2793 | fracsecs = (val % 1e3).to_i.to_s 2794 | val = val / 1e3 2795 | secs = (val % 60).to_i.to_s 2796 | val = val / 60 2797 | mins = (val % 60).to_i.to_s 2798 | degs = (val / 60).to_i.to_s 2799 | posi = (c >= 2**31) 2800 | case posi 2801 | when true 2802 | hemi = @orientation[/^lat$/] ? "N" : "E" 2803 | else 2804 | hemi = @orientation[/^lon$/] ? "W" : "S" 2805 | end 2806 | return degs << " " << mins << " " << secs << "." << fracsecs << " " << hemi 2807 | end 2808 | 2809 | def inspect # :nodoc: 2810 | return "#<#{self.class} #{self}>" 2811 | end 2812 | 2813 | def ==(other) # :nodoc: 2814 | return @coordinates == other.coordinates 2815 | end 2816 | 2817 | def eql?(other) # :nodoc: 2818 | return self == other 2819 | end 2820 | 2821 | def hash # :nodoc: 2822 | return @coordinates.hash 2823 | end 2824 | 2825 | end 2826 | 2827 | ## 2828 | # A Resolv::LOC::Alt 2829 | 2830 | class Alt 2831 | 2832 | Regex = /^([+-]*\d+\.*\d*)[m]$/ 2833 | 2834 | ## 2835 | # Creates a new LOC::Alt from +arg+ which may be: 2836 | # 2837 | # LOC::Alt:: returns +arg+. 2838 | # String:: +arg+ must match the LOC::Alt::Regex constant 2839 | 2840 | def self.create(arg) 2841 | case arg 2842 | when Alt 2843 | return arg 2844 | when String 2845 | altitude = '' 2846 | if Regex =~ arg 2847 | altitude = [($1.to_f*(1e2))+(1e7)].pack("N") 2848 | else 2849 | raise ArgumentError.new("not a properly formed Alt string: " + arg) 2850 | end 2851 | return Alt.new(altitude) 2852 | else 2853 | raise ArgumentError.new("cannot interpret as Alt: #{arg.inspect}") 2854 | end 2855 | end 2856 | 2857 | def initialize(altitude) 2858 | @altitude = altitude 2859 | end 2860 | 2861 | ## 2862 | # The raw altitude 2863 | 2864 | attr_reader :altitude 2865 | 2866 | def to_s # :nodoc: 2867 | a = @altitude.unpack("N").join.to_i 2868 | return ((a.to_f/1e2)-1e5).to_s + "m" 2869 | end 2870 | 2871 | def inspect # :nodoc: 2872 | return "#<#{self.class} #{self}>" 2873 | end 2874 | 2875 | def ==(other) # :nodoc: 2876 | return @altitude == other.altitude 2877 | end 2878 | 2879 | def eql?(other) # :nodoc: 2880 | return self == other 2881 | end 2882 | 2883 | def hash # :nodoc: 2884 | return @altitude.hash 2885 | end 2886 | 2887 | end 2888 | 2889 | end 2890 | 2891 | ## 2892 | # Default resolver to use for Resolv class methods. 2893 | 2894 | DefaultResolver = self.new 2895 | 2896 | ## 2897 | # Replaces the resolvers in the default resolver with +new_resolvers+. This 2898 | # allows resolvers to be changed for resolv-replace. 2899 | 2900 | def DefaultResolver.replace_resolvers new_resolvers 2901 | @resolvers = new_resolvers 2902 | end 2903 | 2904 | ## 2905 | # Address Regexp to use for matching IP addresses. 2906 | 2907 | AddressRegex = /(?:#{IPv4::Regex})|(?:#{IPv6::Regex})/ 2908 | 2909 | end 2910 | 2911 | --------------------------------------------------------------------------------